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,286 @@
1
+ /**
2
+ * Base Command Registrar
3
+ *
4
+ * Abstract base class for command registration to eliminate duplication in:
5
+ * - Command setup patterns
6
+ * - Error handling
7
+ * - Daemon client management
8
+ * - Output formatting
9
+ *
10
+ * Usage:
11
+ * ```typescript
12
+ * class MyCommandRegistrar extends BaseCommandRegistrar {
13
+ * constructor() {
14
+ * super('MyService');
15
+ * }
16
+ *
17
+ * async register(program: Command): Promise<void> {
18
+ * const cmd = this.createCommand(program, 'mycommand', 'My command description');
19
+ *
20
+ * this.addSubcommand(cmd, {
21
+ * name: 'list',
22
+ * description: 'List items',
23
+ * action: async () => {
24
+ * await this.withDaemonAction(async (client) => {
25
+ * const items = await client.listItems();
26
+ * this.logSuccess('Items:', items);
27
+ * });
28
+ * }
29
+ * });
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ import CronJobManager from './cron-job-manager.js';
35
+ import { createLogger } from './logger.js';
36
+ import { withDaemonClient, withDaemonClientForUser, isDaemonRunning } from './daemon-client-helper.js';
37
+ /**
38
+ * Base class for command registrars
39
+ */
40
+ export class BaseCommandRegistrar {
41
+ logger;
42
+ serviceName;
43
+ constructor(serviceName) {
44
+ this.serviceName = serviceName;
45
+ this.logger = createLogger(serviceName);
46
+ }
47
+ /**
48
+ * Create a top-level command
49
+ */
50
+ createCommand(program, name, description) {
51
+ return program
52
+ .command(name)
53
+ .description(description);
54
+ }
55
+ /**
56
+ * Add a subcommand with automatic error handling
57
+ */
58
+ addSubcommand(parent, config) {
59
+ let cmd = parent.command(config.name).description(config.description);
60
+ // Add arguments
61
+ if (config.arguments) {
62
+ config.arguments.forEach(arg => {
63
+ const argStr = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
64
+ if (arg.description) {
65
+ cmd = cmd.argument(argStr, arg.description);
66
+ }
67
+ else {
68
+ cmd = cmd.argument(argStr);
69
+ }
70
+ });
71
+ }
72
+ // Add options
73
+ if (config.options) {
74
+ config.options.forEach(opt => {
75
+ if (opt.defaultValue !== undefined) {
76
+ cmd = cmd.option(opt.flags, opt.description, opt.defaultValue);
77
+ }
78
+ else {
79
+ cmd = cmd.option(opt.flags, opt.description);
80
+ }
81
+ });
82
+ }
83
+ // Wrap action with error handling
84
+ cmd.action(async (...args) => {
85
+ try {
86
+ await config.action(...args);
87
+ }
88
+ catch (error) {
89
+ this.logError('Command failed', error);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ return cmd;
94
+ }
95
+ /**
96
+ * Execute an action with daemon client
97
+ */
98
+ async withDaemonAction(action, config = {}) {
99
+ const { requireRunning = true, exitOnError = true, forUser = false } = config;
100
+ const helper = forUser ? withDaemonClientForUser : withDaemonClient;
101
+ return await helper(action, { requireRunning, exitOnError });
102
+ }
103
+ /**
104
+ * Execute an action with CronJobManager
105
+ */
106
+ async withCronManager(action, config = {}) {
107
+ const { requireRunning = true } = config;
108
+ const manager = new CronJobManager();
109
+ if (requireRunning && !manager.isDaemonRunning()) {
110
+ this.logError('Daemon is not running. Start it with: lsh daemon start');
111
+ process.exit(1);
112
+ }
113
+ try {
114
+ await manager.connect();
115
+ const result = await action(manager);
116
+ manager.disconnect();
117
+ return result;
118
+ }
119
+ catch (error) {
120
+ manager.disconnect();
121
+ throw error;
122
+ }
123
+ }
124
+ /**
125
+ * Check if daemon is running
126
+ */
127
+ isDaemonRunning() {
128
+ return isDaemonRunning();
129
+ }
130
+ /**
131
+ * Log success message
132
+ */
133
+ logSuccess(message, data) {
134
+ this.logger.info(message);
135
+ if (data !== undefined) {
136
+ if (typeof data === 'object' && !Array.isArray(data)) {
137
+ Object.entries(data).forEach(([key, value]) => {
138
+ this.logger.info(` ${key}: ${value}`);
139
+ });
140
+ }
141
+ else if (Array.isArray(data)) {
142
+ data.forEach(item => {
143
+ if (typeof item === 'object') {
144
+ this.logger.info(` ${JSON.stringify(item, null, 2)}`);
145
+ }
146
+ else {
147
+ this.logger.info(` ${item}`);
148
+ }
149
+ });
150
+ }
151
+ else {
152
+ this.logger.info(` ${data}`);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Log error message
158
+ */
159
+ logError(message, error) {
160
+ if (error instanceof Error) {
161
+ this.logger.error(message, error);
162
+ }
163
+ else if (error) {
164
+ this.logger.error(`${message}: ${error}`);
165
+ }
166
+ else {
167
+ this.logger.error(message);
168
+ }
169
+ }
170
+ /**
171
+ * Log info message
172
+ */
173
+ logInfo(message) {
174
+ this.logger.info(message);
175
+ }
176
+ /**
177
+ * Log warning message
178
+ */
179
+ logWarning(message) {
180
+ this.logger.warn(message);
181
+ }
182
+ /**
183
+ * Parse JSON from string with error handling
184
+ */
185
+ parseJSON(jsonString, context = 'JSON') {
186
+ try {
187
+ return JSON.parse(jsonString);
188
+ }
189
+ catch (error) {
190
+ throw new Error(`Invalid ${context}: ${error instanceof Error ? error.message : String(error)}`);
191
+ }
192
+ }
193
+ /**
194
+ * Parse comma-separated tags
195
+ */
196
+ parseTags(tagsString) {
197
+ return tagsString.split(',').map(t => t.trim()).filter(t => t.length > 0);
198
+ }
199
+ /**
200
+ * Format job schedule for display
201
+ */
202
+ formatSchedule(schedule) {
203
+ if (schedule?.cron) {
204
+ return schedule.cron;
205
+ }
206
+ if (schedule?.interval) {
207
+ return `${schedule.interval}ms interval`;
208
+ }
209
+ return 'No schedule';
210
+ }
211
+ /**
212
+ * Validate required options
213
+ */
214
+ validateRequired(options, required, commandName = 'command') {
215
+ const missing = required.filter(key => !options[key]);
216
+ if (missing.length > 0) {
217
+ throw new Error(`Missing required options for ${commandName}: ${missing.map(k => `--${k}`).join(', ')}`);
218
+ }
219
+ }
220
+ /**
221
+ * Create a standardized job specification from options
222
+ */
223
+ createJobSpec(options) {
224
+ return {
225
+ id: options.id || `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
226
+ name: options.name,
227
+ description: options.description,
228
+ command: options.command,
229
+ schedule: {
230
+ cron: options.schedule,
231
+ interval: options.interval ? parseInt(options.interval) : undefined,
232
+ },
233
+ workingDirectory: options.workingDir,
234
+ environment: options.env ? this.parseJSON(options.env, 'environment variables') : {},
235
+ tags: options.tags ? this.parseTags(options.tags) : [],
236
+ priority: options.priority ? parseInt(options.priority) : 5,
237
+ maxRetries: options.maxRetries ? parseInt(options.maxRetries) : 3,
238
+ timeout: options.timeout ? parseInt(options.timeout) : 0,
239
+ databaseSync: options.databaseSync !== false,
240
+ };
241
+ }
242
+ /**
243
+ * Display job information
244
+ */
245
+ displayJob(job) {
246
+ this.logInfo(` ${job.id}: ${job.name}`);
247
+ this.logInfo(` Command: ${job.command}`);
248
+ this.logInfo(` Schedule: ${this.formatSchedule(job.schedule)}`);
249
+ this.logInfo(` Status: ${job.status}`);
250
+ this.logInfo(` Priority: ${job.priority}`);
251
+ if (job.tags && job.tags.length > 0) {
252
+ this.logInfo(` Tags: ${job.tags.join(', ')}`);
253
+ }
254
+ }
255
+ /**
256
+ * Display multiple jobs
257
+ */
258
+ displayJobs(jobs) {
259
+ this.logInfo(`Jobs (${jobs.length} total):`);
260
+ jobs.forEach(job => {
261
+ this.displayJob(job);
262
+ this.logInfo('');
263
+ });
264
+ }
265
+ /**
266
+ * Display job report
267
+ */
268
+ displayJobReport(report) {
269
+ this.logInfo(`Job Report: ${report.jobId || 'N/A'}`);
270
+ this.logInfo(` Executions: ${report.executions}`);
271
+ this.logInfo(` Successes: ${report.successes}`);
272
+ this.logInfo(` Failures: ${report.failures}`);
273
+ this.logInfo(` Success Rate: ${report.successRate.toFixed(1)}%`);
274
+ this.logInfo(` Average Duration: ${Math.round(report.averageDuration)}ms`);
275
+ this.logInfo(` Last Execution: ${report.lastExecution?.toISOString() || 'Never'}`);
276
+ this.logInfo(` Last Success: ${report.lastSuccess?.toISOString() || 'Never'}`);
277
+ this.logInfo(` Last Failure: ${report.lastFailure?.toISOString() || 'Never'}`);
278
+ if (report.commonErrors && report.commonErrors.length > 0) {
279
+ this.logInfo('\n Common Errors:');
280
+ report.commonErrors.forEach((error) => {
281
+ this.logInfo(` - ${error.error} (${error.count} times)`);
282
+ });
283
+ }
284
+ }
285
+ }
286
+ export default BaseCommandRegistrar;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Base Job Manager
3
+ * Abstract base class for all job management systems to eliminate duplication in:
4
+ * - Job lifecycle management (create, start, stop, pause, resume, remove)
5
+ * - Job status tracking and updates
6
+ * - Event emission and handling
7
+ * - Statistics and reporting
8
+ * - Storage abstraction
9
+ *
10
+ * Subclasses implement storage-specific operations (memory, database, filesystem)
11
+ */
12
+ import { EventEmitter } from 'events';
13
+ import { createLogger } from './logger.js';
14
+ /**
15
+ * Abstract base class for job managers
16
+ */
17
+ export class BaseJobManager extends EventEmitter {
18
+ logger;
19
+ storage;
20
+ jobs = new Map();
21
+ constructor(storage, loggerName = 'JobManager') {
22
+ super();
23
+ this.storage = storage;
24
+ this.logger = createLogger(loggerName);
25
+ }
26
+ /**
27
+ * Generate unique job ID
28
+ */
29
+ generateJobId(prefix = 'job') {
30
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
31
+ }
32
+ /**
33
+ * Validate job specification
34
+ */
35
+ validateJobSpec(spec) {
36
+ if (!spec.name) {
37
+ throw new Error('Job name is required');
38
+ }
39
+ if (!spec.command) {
40
+ throw new Error('Job command is required');
41
+ }
42
+ }
43
+ /**
44
+ * Create a new job
45
+ */
46
+ async createJob(spec) {
47
+ this.validateJobSpec(spec);
48
+ const job = {
49
+ id: spec.id || this.generateJobId(),
50
+ name: spec.name,
51
+ command: spec.command,
52
+ args: spec.args,
53
+ status: 'created',
54
+ createdAt: new Date(),
55
+ env: spec.env,
56
+ cwd: spec.cwd || process.cwd(),
57
+ user: spec.user || process.env.USER,
58
+ schedule: spec.schedule,
59
+ tags: spec.tags || [],
60
+ description: spec.description,
61
+ priority: spec.priority ?? 5,
62
+ timeout: spec.timeout,
63
+ maxRetries: spec.maxRetries ?? 3,
64
+ retryCount: 0,
65
+ databaseSync: spec.databaseSync !== false,
66
+ };
67
+ await this.storage.save(job);
68
+ this.jobs.set(job.id, job);
69
+ this.emit('job:created', job);
70
+ this.logger.info(`Job created: ${job.id} (${job.name})`);
71
+ return job;
72
+ }
73
+ /**
74
+ * Get job by ID
75
+ */
76
+ async getJob(jobId) {
77
+ // Check memory cache first
78
+ const job = this.jobs.get(jobId);
79
+ if (job) {
80
+ return job;
81
+ }
82
+ // Check storage
83
+ const storedJob = await this.storage.get(jobId);
84
+ if (storedJob) {
85
+ this.jobs.set(jobId, storedJob);
86
+ return storedJob;
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * List jobs with optional filtering
92
+ */
93
+ async listJobs(filter) {
94
+ let jobs = await this.storage.list(filter);
95
+ // Apply additional filters
96
+ if (filter) {
97
+ jobs = this.applyFilters(jobs, filter);
98
+ }
99
+ return jobs;
100
+ }
101
+ /**
102
+ * Apply filters to job list
103
+ */
104
+ applyFilters(jobs, filter) {
105
+ return jobs.filter(job => {
106
+ // Status filter
107
+ if (filter.status) {
108
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
109
+ if (!statuses.includes(job.status)) {
110
+ return false;
111
+ }
112
+ }
113
+ // Tags filter
114
+ if (filter.tags && filter.tags.length > 0) {
115
+ const hasTag = filter.tags.some(tag => job.tags?.includes(tag));
116
+ if (!hasTag) {
117
+ return false;
118
+ }
119
+ }
120
+ // User filter
121
+ if (filter.user && job.user !== filter.user) {
122
+ return false;
123
+ }
124
+ // Name pattern filter
125
+ if (filter.namePattern) {
126
+ const pattern = typeof filter.namePattern === 'string'
127
+ ? new RegExp(filter.namePattern)
128
+ : filter.namePattern;
129
+ if (!pattern.test(job.name)) {
130
+ return false;
131
+ }
132
+ }
133
+ // Date filters
134
+ if (filter.createdAfter && job.createdAt < filter.createdAfter) {
135
+ return false;
136
+ }
137
+ if (filter.createdBefore && job.createdAt > filter.createdBefore) {
138
+ return false;
139
+ }
140
+ return true;
141
+ });
142
+ }
143
+ /**
144
+ * Update job
145
+ */
146
+ async updateJob(jobId, updates) {
147
+ const job = await this.getJob(jobId);
148
+ if (!job) {
149
+ throw new Error(`Job ${jobId} not found`);
150
+ }
151
+ // Apply updates
152
+ if (updates.name)
153
+ job.name = updates.name;
154
+ if (updates.description)
155
+ job.description = updates.description;
156
+ if (updates.priority !== undefined)
157
+ job.priority = updates.priority;
158
+ if (updates.tags)
159
+ job.tags = updates.tags;
160
+ if (updates.schedule)
161
+ job.schedule = updates.schedule;
162
+ if (updates.env)
163
+ job.env = { ...job.env, ...updates.env };
164
+ if (updates.timeout !== undefined)
165
+ job.timeout = updates.timeout;
166
+ if (updates.maxRetries !== undefined)
167
+ job.maxRetries = updates.maxRetries;
168
+ await this.storage.update(jobId, job);
169
+ this.jobs.set(jobId, job);
170
+ this.emit('job:updated', job);
171
+ this.logger.info(`Job updated: ${jobId}`);
172
+ return job;
173
+ }
174
+ /**
175
+ * Update job status
176
+ */
177
+ async updateJobStatus(jobId, status, additionalUpdates) {
178
+ const job = await this.getJob(jobId);
179
+ if (!job) {
180
+ throw new Error(`Job ${jobId} not found`);
181
+ }
182
+ job.status = status;
183
+ // Apply additional updates
184
+ if (additionalUpdates) {
185
+ Object.assign(job, additionalUpdates);
186
+ }
187
+ // Update timestamps
188
+ if (status === 'running' && !job.startedAt) {
189
+ job.startedAt = new Date();
190
+ }
191
+ if (status === 'completed' || status === 'failed' || status === 'killed') {
192
+ job.completedAt = new Date();
193
+ }
194
+ await this.storage.update(jobId, job);
195
+ this.jobs.set(jobId, job);
196
+ this.emit(`job:${status}`, job);
197
+ this.logger.info(`Job ${status}: ${jobId}`);
198
+ return job;
199
+ }
200
+ /**
201
+ * Remove job
202
+ */
203
+ async removeJob(jobId, force = false) {
204
+ const job = await this.getJob(jobId);
205
+ if (!job) {
206
+ throw new Error(`Job ${jobId} not found`);
207
+ }
208
+ // Check if job is running
209
+ if (job.status === 'running' && !force) {
210
+ throw new Error(`Job ${jobId} is running. Use force=true to remove.`);
211
+ }
212
+ // Stop job if running
213
+ if (job.status === 'running') {
214
+ await this.stopJob(jobId);
215
+ }
216
+ await this.storage.delete(jobId);
217
+ this.jobs.delete(jobId);
218
+ this.emit('job:removed', job);
219
+ this.logger.info(`Job removed: ${jobId}`);
220
+ return true;
221
+ }
222
+ /**
223
+ * Get job execution history
224
+ */
225
+ async getJobHistory(jobId, limit = 50) {
226
+ return await this.storage.getExecutions(jobId, limit);
227
+ }
228
+ /**
229
+ * Calculate job statistics
230
+ */
231
+ async getJobStatistics(jobId) {
232
+ const executions = await this.getJobHistory(jobId);
233
+ const job = await this.getJob(jobId);
234
+ if (!job) {
235
+ throw new Error(`Job ${jobId} not found`);
236
+ }
237
+ const totalExecutions = executions.length;
238
+ const successfulExecutions = executions.filter(e => e.status === 'completed').length;
239
+ const failedExecutions = executions.filter(e => e.status === 'failed').length;
240
+ const completedExecutions = executions.filter(e => e.duration);
241
+ const averageDuration = completedExecutions.length > 0
242
+ ? completedExecutions.reduce((sum, e) => sum + (e.duration || 0), 0) / completedExecutions.length
243
+ : 0;
244
+ const lastExecution = executions[0]?.startTime;
245
+ const lastSuccess = executions.find(e => e.status === 'completed')?.startTime;
246
+ const lastFailure = executions.find(e => e.status === 'failed')?.startTime;
247
+ return {
248
+ jobId: job.id,
249
+ jobName: job.name,
250
+ totalExecutions,
251
+ successfulExecutions,
252
+ failedExecutions,
253
+ successRate: totalExecutions > 0 ? (successfulExecutions / totalExecutions) * 100 : 0,
254
+ averageDuration,
255
+ lastExecution,
256
+ lastSuccess,
257
+ lastFailure,
258
+ };
259
+ }
260
+ /**
261
+ * Record job execution
262
+ */
263
+ async recordExecution(job, status, details = {}) {
264
+ const execution = {
265
+ executionId: this.generateJobId('exec'),
266
+ jobId: job.id,
267
+ jobName: job.name,
268
+ command: job.command,
269
+ startTime: details.startTime || new Date(),
270
+ endTime: details.endTime,
271
+ duration: details.duration,
272
+ status,
273
+ exitCode: details.exitCode,
274
+ stdout: details.stdout,
275
+ stderr: details.stderr,
276
+ errorMessage: details.errorMessage,
277
+ };
278
+ await this.storage.saveExecution(execution);
279
+ this.emit('job:execution', execution);
280
+ return execution;
281
+ }
282
+ /**
283
+ * Cleanup - optional override
284
+ */
285
+ async cleanup() {
286
+ if (this.storage.cleanup) {
287
+ await this.storage.cleanup();
288
+ }
289
+ this.jobs.clear();
290
+ this.removeAllListeners();
291
+ }
292
+ }
293
+ export default BaseJobManager;