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.
- package/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- 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;
|