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,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH Daemon Client
|
|
3
|
+
* Provides communication interface between LSH and the job daemon
|
|
4
|
+
*/
|
|
5
|
+
import * as net from 'net';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import DatabasePersistence from './database-persistence.js';
|
|
9
|
+
import { createLogger } from './logger.js';
|
|
10
|
+
export class DaemonClient extends EventEmitter {
|
|
11
|
+
socketPath;
|
|
12
|
+
socket;
|
|
13
|
+
connected = false;
|
|
14
|
+
messageId = 0;
|
|
15
|
+
pendingMessages = new Map();
|
|
16
|
+
databasePersistence;
|
|
17
|
+
userId;
|
|
18
|
+
sessionId;
|
|
19
|
+
logger = createLogger('DaemonClient');
|
|
20
|
+
constructor(socketPath, userId) {
|
|
21
|
+
super();
|
|
22
|
+
// Use user-specific socket path if not provided
|
|
23
|
+
this.socketPath = socketPath || `/tmp/lsh-job-daemon-${process.env.USER || 'default'}.sock`;
|
|
24
|
+
this.userId = userId;
|
|
25
|
+
this.sessionId = `lsh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
26
|
+
if (userId) {
|
|
27
|
+
this.databasePersistence = new DatabasePersistence(userId);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Connect to the daemon
|
|
32
|
+
*/
|
|
33
|
+
async connect() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
if (this.connected) {
|
|
36
|
+
resolve(true);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Check if socket file exists
|
|
40
|
+
if (!fs.existsSync(this.socketPath)) {
|
|
41
|
+
reject(new Error(`Daemon socket not found at ${this.socketPath}. Is the daemon running?`));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Check socket permissions
|
|
45
|
+
try {
|
|
46
|
+
fs.accessSync(this.socketPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
47
|
+
}
|
|
48
|
+
catch (_err) {
|
|
49
|
+
const stats = fs.statSync(this.socketPath);
|
|
50
|
+
const currentUid = process.getuid?.();
|
|
51
|
+
const owner = currentUid !== undefined && stats.uid === currentUid ? 'you' : 'another user';
|
|
52
|
+
reject(new Error(`Permission denied to access socket at ${this.socketPath}. Socket is owned by ${owner}. You may need to start your own daemon with: lsh daemon start`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.socket = new net.Socket();
|
|
56
|
+
this.socket.on('connect', () => {
|
|
57
|
+
this.connected = true;
|
|
58
|
+
this.emit('connected');
|
|
59
|
+
resolve(true);
|
|
60
|
+
});
|
|
61
|
+
let buffer = '';
|
|
62
|
+
const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB limit
|
|
63
|
+
this.socket.on('data', (data) => {
|
|
64
|
+
try {
|
|
65
|
+
buffer += data.toString();
|
|
66
|
+
// Prevent buffer from growing too large
|
|
67
|
+
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
68
|
+
this.logger.error('Daemon response too large, truncating buffer');
|
|
69
|
+
buffer = buffer.substring(buffer.length - MAX_BUFFER_SIZE / 2);
|
|
70
|
+
}
|
|
71
|
+
// Try to parse complete JSON messages
|
|
72
|
+
// Messages might be split across multiple data events
|
|
73
|
+
while (buffer.length > 0) {
|
|
74
|
+
try {
|
|
75
|
+
// Try to find complete JSON message boundaries
|
|
76
|
+
let jsonStart = -1;
|
|
77
|
+
let braceCount = 0;
|
|
78
|
+
let inString = false;
|
|
79
|
+
let escaped = false;
|
|
80
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
81
|
+
const char = buffer[i];
|
|
82
|
+
if (escaped) {
|
|
83
|
+
escaped = false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === '\\') {
|
|
87
|
+
escaped = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (char === '"' && !escaped) {
|
|
91
|
+
inString = !inString;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inString)
|
|
95
|
+
continue;
|
|
96
|
+
if (char === '{') {
|
|
97
|
+
if (jsonStart === -1)
|
|
98
|
+
jsonStart = i;
|
|
99
|
+
braceCount++;
|
|
100
|
+
}
|
|
101
|
+
else if (char === '}') {
|
|
102
|
+
braceCount--;
|
|
103
|
+
if (braceCount === 0 && jsonStart !== -1) {
|
|
104
|
+
// Found complete JSON object
|
|
105
|
+
const jsonStr = buffer.substring(jsonStart, i + 1);
|
|
106
|
+
try {
|
|
107
|
+
const response = JSON.parse(jsonStr);
|
|
108
|
+
this.handleResponse(response);
|
|
109
|
+
buffer = buffer.substring(i + 1); // Remove processed part
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
catch (parseError) {
|
|
113
|
+
this.logger.error('Invalid JSON in daemon response', parseError, { jsonContent: jsonStr.substring(0, 200) });
|
|
114
|
+
buffer = buffer.substring(i + 1); // Skip this invalid JSON
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// If we didn't find a complete JSON object, wait for more data
|
|
121
|
+
if (braceCount > 0 || jsonStart === -1) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (parseError) {
|
|
126
|
+
this.logger.error('JSON parsing error', parseError);
|
|
127
|
+
// Try to find the start of the next JSON object
|
|
128
|
+
const nextStart = buffer.indexOf('{', 1);
|
|
129
|
+
if (nextStart > 0) {
|
|
130
|
+
buffer = buffer.substring(nextStart);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
buffer = '';
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.logger.error('Failed to process daemon response', error);
|
|
141
|
+
buffer = ''; // Reset buffer on error
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.socket.on('error', (error) => {
|
|
145
|
+
this.connected = false;
|
|
146
|
+
// Enhance error messages for common issues
|
|
147
|
+
if (error.code === 'EACCES') {
|
|
148
|
+
error.message = `Permission denied to access daemon socket at ${this.socketPath}. The socket may be owned by another user. Try starting your own daemon with: lsh daemon start`;
|
|
149
|
+
}
|
|
150
|
+
else if (error.code === 'ECONNREFUSED') {
|
|
151
|
+
error.message = `Daemon is not responding at ${this.socketPath}. The daemon may have crashed. Try restarting with: lsh daemon restart`;
|
|
152
|
+
}
|
|
153
|
+
else if (error.code === 'ENOENT') {
|
|
154
|
+
error.message = `Daemon socket not found at ${this.socketPath}. Start the daemon with: lsh daemon start`;
|
|
155
|
+
}
|
|
156
|
+
this.emit('error', error);
|
|
157
|
+
reject(error);
|
|
158
|
+
});
|
|
159
|
+
this.socket.on('close', () => {
|
|
160
|
+
this.connected = false;
|
|
161
|
+
this.emit('disconnected');
|
|
162
|
+
});
|
|
163
|
+
this.socket.connect(this.socketPath);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Disconnect from the daemon
|
|
168
|
+
*/
|
|
169
|
+
disconnect() {
|
|
170
|
+
if (this.socket) {
|
|
171
|
+
this.socket.destroy();
|
|
172
|
+
this.socket = undefined;
|
|
173
|
+
}
|
|
174
|
+
this.connected = false;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Send a message to the daemon
|
|
178
|
+
*/
|
|
179
|
+
async sendMessage(message) {
|
|
180
|
+
if (!this.connected || !this.socket) {
|
|
181
|
+
throw new Error('Not connected to daemon');
|
|
182
|
+
}
|
|
183
|
+
const id = (++this.messageId).toString();
|
|
184
|
+
message.id = id;
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
// Set timeout for response (reduced for faster failure detection)
|
|
187
|
+
const timeoutId = setTimeout(() => {
|
|
188
|
+
if (this.pendingMessages.has(id)) {
|
|
189
|
+
this.pendingMessages.delete(id);
|
|
190
|
+
reject(new Error(`Request timeout after 10 seconds for command: ${message.command}`));
|
|
191
|
+
}
|
|
192
|
+
}, 10000); // 10 second timeout
|
|
193
|
+
// Store timeout ID for cleanup
|
|
194
|
+
this.pendingMessages.set(id, {
|
|
195
|
+
resolve: (data) => {
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
resolve(data);
|
|
198
|
+
},
|
|
199
|
+
reject: (error) => {
|
|
200
|
+
clearTimeout(timeoutId);
|
|
201
|
+
reject(error);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
this.socket.write(JSON.stringify(message));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Handle response from daemon
|
|
209
|
+
*/
|
|
210
|
+
handleResponse(response) {
|
|
211
|
+
if (response.id && this.pendingMessages.has(response.id)) {
|
|
212
|
+
const { resolve, reject } = this.pendingMessages.get(response.id);
|
|
213
|
+
this.pendingMessages.delete(response.id);
|
|
214
|
+
if (response.success) {
|
|
215
|
+
resolve(response.data);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
reject(new Error(response.error || 'Unknown error'));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get daemon status
|
|
224
|
+
*/
|
|
225
|
+
async getStatus() {
|
|
226
|
+
return await this.sendMessage({ command: 'status' });
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Add a simple job to the daemon
|
|
230
|
+
*/
|
|
231
|
+
async addJob(jobSpec) {
|
|
232
|
+
return await this.sendMessage({
|
|
233
|
+
command: 'addJob',
|
|
234
|
+
args: { jobSpec }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Create a cron job
|
|
239
|
+
*/
|
|
240
|
+
async createCronJob(jobSpec) {
|
|
241
|
+
const daemonJobSpec = {
|
|
242
|
+
id: jobSpec.id,
|
|
243
|
+
name: jobSpec.name,
|
|
244
|
+
command: jobSpec.command,
|
|
245
|
+
type: 'scheduled',
|
|
246
|
+
schedule: jobSpec.schedule,
|
|
247
|
+
env: jobSpec.environment || {},
|
|
248
|
+
cwd: jobSpec.workingDirectory || process.cwd(),
|
|
249
|
+
user: jobSpec.user || process.env.USER,
|
|
250
|
+
priority: jobSpec.priority || 0,
|
|
251
|
+
tags: jobSpec.tags || [],
|
|
252
|
+
enabled: jobSpec.enabled !== false,
|
|
253
|
+
maxRetries: jobSpec.maxRetries || 3,
|
|
254
|
+
timeout: jobSpec.timeout || 0,
|
|
255
|
+
};
|
|
256
|
+
const result = await this.sendMessage({
|
|
257
|
+
command: 'addJob',
|
|
258
|
+
args: { jobSpec: daemonJobSpec }
|
|
259
|
+
});
|
|
260
|
+
// Sync to database if enabled
|
|
261
|
+
if (jobSpec.databaseSync && this.databasePersistence) {
|
|
262
|
+
await this.syncJobToDatabase(jobSpec, 'created');
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Start a job
|
|
268
|
+
*/
|
|
269
|
+
async startJob(jobId) {
|
|
270
|
+
const result = await this.sendMessage({
|
|
271
|
+
command: 'startJob',
|
|
272
|
+
args: { jobId }
|
|
273
|
+
});
|
|
274
|
+
// Sync to database
|
|
275
|
+
if (this.databasePersistence) {
|
|
276
|
+
await this.syncJobToDatabase({ id: jobId }, 'running');
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Trigger a job to run immediately (bypass schedule)
|
|
282
|
+
*/
|
|
283
|
+
async triggerJob(jobId) {
|
|
284
|
+
const result = await this.sendMessage({
|
|
285
|
+
command: 'triggerJob',
|
|
286
|
+
args: { jobId }
|
|
287
|
+
});
|
|
288
|
+
// Record job execution in database
|
|
289
|
+
if (this.databasePersistence) {
|
|
290
|
+
try {
|
|
291
|
+
await this.databasePersistence.saveJob({
|
|
292
|
+
user_id: this.userId,
|
|
293
|
+
session_id: this.sessionId,
|
|
294
|
+
job_id: jobId,
|
|
295
|
+
command: `Triggered execution of ${jobId}`,
|
|
296
|
+
status: result.success ? 'completed' : 'failed',
|
|
297
|
+
working_directory: process.cwd(),
|
|
298
|
+
started_at: new Date().toISOString(),
|
|
299
|
+
completed_at: new Date().toISOString(),
|
|
300
|
+
exit_code: result.success ? 0 : 1,
|
|
301
|
+
output: result.output,
|
|
302
|
+
error: result.error
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
// Don't fail the trigger if database save fails
|
|
307
|
+
this.logger.warn(`Failed to save job execution to database: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Stop a job
|
|
314
|
+
*/
|
|
315
|
+
async stopJob(jobId, signal = 'SIGTERM') {
|
|
316
|
+
const result = await this.sendMessage({
|
|
317
|
+
command: 'stopJob',
|
|
318
|
+
args: { jobId, signal }
|
|
319
|
+
});
|
|
320
|
+
// Sync to database
|
|
321
|
+
if (this.databasePersistence) {
|
|
322
|
+
await this.syncJobToDatabase({ id: jobId }, 'stopped');
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* List all jobs
|
|
328
|
+
*/
|
|
329
|
+
async listJobs(filter) {
|
|
330
|
+
try {
|
|
331
|
+
const result = await this.sendMessage({
|
|
332
|
+
command: 'listJobs',
|
|
333
|
+
args: {
|
|
334
|
+
filter,
|
|
335
|
+
limit: 50 // Limit results to prevent oversized responses
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// Ensure we return an array
|
|
339
|
+
if (Array.isArray(result)) {
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
else if (result && typeof result === 'object' && Array.isArray(result.jobs)) {
|
|
343
|
+
return result.jobs;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.logger.warn('Unexpected job list format', { resultType: typeof result });
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
this.logger.error('Failed to list jobs', error);
|
|
352
|
+
// Return empty array instead of throwing to prevent crashes
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get job details
|
|
358
|
+
*/
|
|
359
|
+
async getJob(jobId) {
|
|
360
|
+
return await this.sendMessage({
|
|
361
|
+
command: 'getJob',
|
|
362
|
+
args: { jobId }
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Remove a job
|
|
367
|
+
*/
|
|
368
|
+
async removeJob(jobId, force = false) {
|
|
369
|
+
const result = await this.sendMessage({
|
|
370
|
+
command: 'removeJob',
|
|
371
|
+
args: { jobId, force }
|
|
372
|
+
});
|
|
373
|
+
// Remove from database
|
|
374
|
+
if (this.databasePersistence) {
|
|
375
|
+
// Note: DatabasePersistence doesn't have a removeJob method yet
|
|
376
|
+
// This would need to be implemented
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Restart the daemon
|
|
382
|
+
*/
|
|
383
|
+
async restartDaemon() {
|
|
384
|
+
await this.sendMessage({ command: 'restart' });
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Stop the daemon
|
|
388
|
+
*/
|
|
389
|
+
async stopDaemon() {
|
|
390
|
+
await this.sendMessage({ command: 'stop' });
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Sync job status to Supabase database
|
|
394
|
+
*/
|
|
395
|
+
async syncJobToDatabase(jobSpec, status) {
|
|
396
|
+
if (!this.databasePersistence)
|
|
397
|
+
return;
|
|
398
|
+
try {
|
|
399
|
+
await this.databasePersistence.saveJob({
|
|
400
|
+
session_id: this.databasePersistence.getSessionId(),
|
|
401
|
+
job_id: jobSpec.id,
|
|
402
|
+
command: jobSpec.command,
|
|
403
|
+
status: status,
|
|
404
|
+
working_directory: jobSpec.workingDirectory || process.cwd(),
|
|
405
|
+
started_at: new Date().toISOString(),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
this.logger.error('Failed to sync job to database', error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Create a database-backed cron job
|
|
414
|
+
*/
|
|
415
|
+
async createDatabaseCronJob(jobSpec) {
|
|
416
|
+
// Create job in daemon
|
|
417
|
+
const daemonResult = await this.createCronJob({
|
|
418
|
+
...jobSpec,
|
|
419
|
+
databaseSync: true
|
|
420
|
+
});
|
|
421
|
+
// Create initial database record
|
|
422
|
+
if (this.databasePersistence) {
|
|
423
|
+
await this.databasePersistence.saveJob({
|
|
424
|
+
session_id: this.databasePersistence.getSessionId(),
|
|
425
|
+
job_id: jobSpec.id,
|
|
426
|
+
command: jobSpec.command,
|
|
427
|
+
status: 'running',
|
|
428
|
+
working_directory: jobSpec.workingDirectory || process.cwd(),
|
|
429
|
+
started_at: new Date().toISOString(),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return daemonResult;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get job execution history from database
|
|
436
|
+
*/
|
|
437
|
+
async getJobHistory(jobId, limit = 100) {
|
|
438
|
+
if (!this.databasePersistence) {
|
|
439
|
+
throw new Error('Database persistence not configured');
|
|
440
|
+
}
|
|
441
|
+
const jobs = await this.databasePersistence.getActiveJobs();
|
|
442
|
+
if (jobId) {
|
|
443
|
+
return jobs.filter(job => job.job_id === jobId);
|
|
444
|
+
}
|
|
445
|
+
return jobs.slice(0, limit);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get job statistics from database
|
|
449
|
+
*/
|
|
450
|
+
async getJobStatistics(jobId) {
|
|
451
|
+
if (!this.databasePersistence) {
|
|
452
|
+
throw new Error('Database persistence not configured');
|
|
453
|
+
}
|
|
454
|
+
const jobs = await this.databasePersistence.getActiveJobs();
|
|
455
|
+
if (jobId) {
|
|
456
|
+
const jobJobs = jobs.filter(job => job.job_id === jobId);
|
|
457
|
+
return this.calculateJobStatistics(jobJobs);
|
|
458
|
+
}
|
|
459
|
+
return this.calculateJobStatistics(jobs);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Calculate job statistics
|
|
463
|
+
*/
|
|
464
|
+
calculateJobStatistics(jobs) {
|
|
465
|
+
const total = jobs.length;
|
|
466
|
+
const byStatus = jobs.reduce((acc, job) => {
|
|
467
|
+
acc[job.status] = (acc[job.status] || 0) + 1;
|
|
468
|
+
return acc;
|
|
469
|
+
}, {});
|
|
470
|
+
const successRate = byStatus.completed ? (byStatus.completed / total) * 100 : 0;
|
|
471
|
+
return {
|
|
472
|
+
totalJobs: total,
|
|
473
|
+
byStatus,
|
|
474
|
+
successRate,
|
|
475
|
+
lastExecution: jobs.length > 0 ? jobs[0].started_at : null,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Check if daemon is running
|
|
480
|
+
*/
|
|
481
|
+
isDaemonRunning() {
|
|
482
|
+
if (!fs.existsSync(this.socketPath)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
// Try to access the socket to verify it's working
|
|
487
|
+
fs.accessSync(this.socketPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
catch (_error) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get connection status
|
|
496
|
+
*/
|
|
497
|
+
isConnected() {
|
|
498
|
+
return this.connected;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
export default DaemonClient;
|