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,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Command Registrar
|
|
3
|
+
* Registers all daemon-related CLI commands using BaseCommandRegistrar
|
|
4
|
+
*/
|
|
5
|
+
import { BaseCommandRegistrar } from '../../lib/base-command-registrar.js';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export class DaemonCommandRegistrar extends BaseCommandRegistrar {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('Daemon');
|
|
13
|
+
}
|
|
14
|
+
async register(program) {
|
|
15
|
+
const daemonCmd = this.createCommand(program, 'daemon', 'LSH daemon management commands');
|
|
16
|
+
this.registerDaemonControlCommands(daemonCmd);
|
|
17
|
+
this.registerJobManagementCommands(daemonCmd);
|
|
18
|
+
this.registerDatabaseCommands(daemonCmd);
|
|
19
|
+
}
|
|
20
|
+
registerDaemonControlCommands(daemonCmd) {
|
|
21
|
+
// Status command
|
|
22
|
+
this.addSubcommand(daemonCmd, {
|
|
23
|
+
name: 'status',
|
|
24
|
+
description: 'Get daemon status',
|
|
25
|
+
action: async () => {
|
|
26
|
+
const status = await this.withDaemonAction(async (client) => {
|
|
27
|
+
return await client.getStatus();
|
|
28
|
+
});
|
|
29
|
+
this.logInfo('Daemon Status:');
|
|
30
|
+
this.logInfo(` PID: ${status.pid}`);
|
|
31
|
+
this.logInfo(` Uptime: ${Math.floor(status.uptime / 60)} minutes`);
|
|
32
|
+
this.logInfo(` Memory: ${Math.round(status.memoryUsage.heapUsed / 1024 / 1024)} MB`);
|
|
33
|
+
this.logInfo(` Jobs: ${status.jobs.total} total, ${status.jobs.running} running`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// Start command
|
|
37
|
+
this.addSubcommand(daemonCmd, {
|
|
38
|
+
name: 'start',
|
|
39
|
+
description: 'Start the daemon',
|
|
40
|
+
action: async () => {
|
|
41
|
+
const { spawn } = await import('child_process');
|
|
42
|
+
const socketPath = `/tmp/lsh-job-daemon-${process.env.USER || 'user'}.sock`;
|
|
43
|
+
const daemonProcess = spawn('node', ['dist/daemon/lshd.js', 'start', socketPath], {
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: 'ignore'
|
|
46
|
+
});
|
|
47
|
+
daemonProcess.unref();
|
|
48
|
+
this.logSuccess('Daemon started');
|
|
49
|
+
this.logInfo('Check status with: lsh daemon status');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Stop command
|
|
53
|
+
this.addSubcommand(daemonCmd, {
|
|
54
|
+
name: 'stop',
|
|
55
|
+
description: 'Stop the daemon',
|
|
56
|
+
action: async () => {
|
|
57
|
+
if (!this.isDaemonRunning()) {
|
|
58
|
+
this.logWarning('Daemon is not running');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await this.withDaemonAction(async (client) => {
|
|
62
|
+
await client.stopDaemon();
|
|
63
|
+
});
|
|
64
|
+
this.logSuccess('Daemon stopped');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Restart command
|
|
68
|
+
this.addSubcommand(daemonCmd, {
|
|
69
|
+
name: 'restart',
|
|
70
|
+
description: 'Restart the daemon',
|
|
71
|
+
action: async () => {
|
|
72
|
+
if (this.isDaemonRunning()) {
|
|
73
|
+
await this.withDaemonAction(async (client) => {
|
|
74
|
+
await client.restartDaemon();
|
|
75
|
+
});
|
|
76
|
+
this.logSuccess('Daemon restarted');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.logWarning('Daemon is not running, starting...');
|
|
80
|
+
const { spawn } = await import('child_process');
|
|
81
|
+
const daemonProcess = spawn('node', ['dist/daemon/lshd.js', 'start'], {
|
|
82
|
+
detached: true,
|
|
83
|
+
stdio: 'ignore'
|
|
84
|
+
});
|
|
85
|
+
daemonProcess.unref();
|
|
86
|
+
this.logSuccess('Daemon started');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Cleanup command
|
|
91
|
+
this.addSubcommand(daemonCmd, {
|
|
92
|
+
name: 'cleanup',
|
|
93
|
+
description: 'Clean up daemon processes and files',
|
|
94
|
+
options: [
|
|
95
|
+
{ flags: '-f, --force', description: 'Force cleanup without prompts', defaultValue: false }
|
|
96
|
+
],
|
|
97
|
+
action: async (options) => {
|
|
98
|
+
await this.cleanupDaemon(options.force);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
registerJobManagementCommands(daemonCmd) {
|
|
103
|
+
const jobCmd = daemonCmd
|
|
104
|
+
.command('job')
|
|
105
|
+
.description('Job management commands');
|
|
106
|
+
// Create job
|
|
107
|
+
this.addSubcommand(jobCmd, {
|
|
108
|
+
name: 'create',
|
|
109
|
+
description: 'Create a new cron job',
|
|
110
|
+
options: [
|
|
111
|
+
{ flags: '-n, --name <name>', description: 'Job name' },
|
|
112
|
+
{ flags: '-c, --command <command>', description: 'Command to execute' },
|
|
113
|
+
{ flags: '-s, --schedule <schedule>', description: 'Cron schedule (e.g., "0 2 * * *")' },
|
|
114
|
+
{ flags: '-i, --interval <interval>', description: 'Interval in milliseconds' },
|
|
115
|
+
{ flags: '-d, --description <description>', description: 'Job description' },
|
|
116
|
+
{ flags: '-w, --working-dir <dir>', description: 'Working directory' },
|
|
117
|
+
{ flags: '-e, --env <env>', description: 'Environment variables (JSON)' },
|
|
118
|
+
{ flags: '-t, --tags <tags>', description: 'Comma-separated tags' },
|
|
119
|
+
{ flags: '-p, --priority <priority>', description: 'Priority (0-10)', defaultValue: '5' },
|
|
120
|
+
{ flags: '--max-retries <retries>', description: 'Maximum retries', defaultValue: '3' },
|
|
121
|
+
{ flags: '--timeout <timeout>', description: 'Timeout in milliseconds', defaultValue: '0' },
|
|
122
|
+
{ flags: '--no-database-sync', description: 'Disable database synchronization' }
|
|
123
|
+
],
|
|
124
|
+
action: async (options) => {
|
|
125
|
+
if (!options.name || !options.command || (!options.schedule && !options.interval)) {
|
|
126
|
+
throw new Error('Missing required options: --name, --command, and (--schedule or --interval)');
|
|
127
|
+
}
|
|
128
|
+
const jobSpec = this.createJobSpec(options);
|
|
129
|
+
await this.withDaemonAction(async (client) => {
|
|
130
|
+
await client.createDatabaseCronJob(jobSpec);
|
|
131
|
+
});
|
|
132
|
+
this.logSuccess('Job created successfully:');
|
|
133
|
+
this.logInfo(` ID: ${jobSpec.id}`);
|
|
134
|
+
this.logInfo(` Name: ${jobSpec.name}`);
|
|
135
|
+
this.logInfo(` Command: ${jobSpec.command}`);
|
|
136
|
+
this.logInfo(` Schedule: ${this.formatSchedule(jobSpec.schedule)}`);
|
|
137
|
+
this.logInfo(` Database Sync: ${jobSpec.databaseSync ? 'Enabled' : 'Disabled'}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// List jobs
|
|
141
|
+
this.addSubcommand(jobCmd, {
|
|
142
|
+
name: 'list',
|
|
143
|
+
description: 'List all jobs',
|
|
144
|
+
options: [
|
|
145
|
+
{ flags: '-f, --filter <filter>', description: 'Filter jobs by status' }
|
|
146
|
+
],
|
|
147
|
+
action: async (options) => {
|
|
148
|
+
const jobs = await this.withDaemonAction(async (client) => {
|
|
149
|
+
return await client.listJobs(options.filter ? { status: options.filter } : undefined);
|
|
150
|
+
});
|
|
151
|
+
this.displayJobs(jobs);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Start job
|
|
155
|
+
this.addSubcommand(jobCmd, {
|
|
156
|
+
name: 'start',
|
|
157
|
+
description: 'Start a job',
|
|
158
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
159
|
+
action: async (jobId) => {
|
|
160
|
+
await this.withDaemonAction(async (client) => {
|
|
161
|
+
await client.startJob(jobId);
|
|
162
|
+
});
|
|
163
|
+
this.logSuccess(`Job ${jobId} started`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// Trigger job
|
|
167
|
+
this.addSubcommand(jobCmd, {
|
|
168
|
+
name: 'trigger',
|
|
169
|
+
description: 'Trigger a job to run immediately (bypass schedule)',
|
|
170
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
171
|
+
action: async (jobId) => {
|
|
172
|
+
const result = await this.withDaemonAction(async (client) => await client.triggerJob(jobId), { forUser: true });
|
|
173
|
+
this.logSuccess(`Job ${jobId} triggered successfully`);
|
|
174
|
+
if (result.output) {
|
|
175
|
+
this.logInfo('Output:');
|
|
176
|
+
this.logInfo(result.output);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// Trigger all jobs
|
|
181
|
+
this.addSubcommand(jobCmd, {
|
|
182
|
+
name: 'trigger-all',
|
|
183
|
+
description: 'Trigger all active jobs to run immediately',
|
|
184
|
+
options: [
|
|
185
|
+
{ flags: '-f, --filter <status>', description: 'Filter by job status', defaultValue: 'created' }
|
|
186
|
+
],
|
|
187
|
+
action: async (options) => {
|
|
188
|
+
await this.withDaemonAction(async (client) => {
|
|
189
|
+
const jobs = await client.listJobs({ status: options.filter });
|
|
190
|
+
this.logInfo(`Triggering ${jobs.length} jobs...`);
|
|
191
|
+
for (const job of jobs) {
|
|
192
|
+
try {
|
|
193
|
+
this.logInfo(` Triggering ${job.name} (${job.id})...`);
|
|
194
|
+
const result = await client.triggerJob(job.id);
|
|
195
|
+
this.logSuccess(` ${job.name} completed`);
|
|
196
|
+
if (result.output) {
|
|
197
|
+
const preview = result.output.substring(0, 100);
|
|
198
|
+
this.logInfo(` Output: ${preview}${result.output.length > 100 ? '...' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
this.logError(` ${job.name} failed: ${error.message || error}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, { forUser: true });
|
|
206
|
+
this.logSuccess('All jobs triggered');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Stop job
|
|
210
|
+
this.addSubcommand(jobCmd, {
|
|
211
|
+
name: 'stop',
|
|
212
|
+
description: 'Stop a job',
|
|
213
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
214
|
+
options: [
|
|
215
|
+
{ flags: '-s, --signal <signal>', description: 'Signal to send', defaultValue: 'SIGTERM' }
|
|
216
|
+
],
|
|
217
|
+
action: async (jobId, options) => {
|
|
218
|
+
await this.withDaemonAction(async (client) => {
|
|
219
|
+
await client.stopJob(jobId, options.signal);
|
|
220
|
+
});
|
|
221
|
+
this.logSuccess(`Job ${jobId} stopped with signal ${options.signal}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// Remove job
|
|
225
|
+
this.addSubcommand(jobCmd, {
|
|
226
|
+
name: 'remove',
|
|
227
|
+
description: 'Remove a job',
|
|
228
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
229
|
+
options: [
|
|
230
|
+
{ flags: '-f, --force', description: 'Force removal', defaultValue: false }
|
|
231
|
+
],
|
|
232
|
+
action: async (jobId, options) => {
|
|
233
|
+
await this.withDaemonAction(async (client) => {
|
|
234
|
+
await client.removeJob(jobId, options.force);
|
|
235
|
+
});
|
|
236
|
+
this.logSuccess(`Job ${jobId} removed`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// Job info
|
|
240
|
+
this.addSubcommand(jobCmd, {
|
|
241
|
+
name: 'info',
|
|
242
|
+
description: 'Get job information',
|
|
243
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
244
|
+
action: async (jobId) => {
|
|
245
|
+
const job = await this.withDaemonAction(async (client) => {
|
|
246
|
+
return await client.getJob(jobId);
|
|
247
|
+
});
|
|
248
|
+
if (!job) {
|
|
249
|
+
throw new Error(`Job ${jobId} not found`);
|
|
250
|
+
}
|
|
251
|
+
this.logInfo(`Job Information: ${jobId}`);
|
|
252
|
+
this.logInfo(` Name: ${job.name}`);
|
|
253
|
+
this.logInfo(` Command: ${job.command}`);
|
|
254
|
+
this.logInfo(` Status: ${job.status}`);
|
|
255
|
+
this.logInfo(` Priority: ${job.priority}`);
|
|
256
|
+
this.logInfo(` Working Directory: ${job.cwd}`);
|
|
257
|
+
this.logInfo(` User: ${job.user}`);
|
|
258
|
+
this.logInfo(` Tags: ${job.tags?.join(', ') || 'None'}`);
|
|
259
|
+
if (job.schedule) {
|
|
260
|
+
this.logInfo(` Schedule: ${this.formatSchedule(job.schedule)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
registerDatabaseCommands(daemonCmd) {
|
|
266
|
+
const dbCmd = daemonCmd
|
|
267
|
+
.command('db')
|
|
268
|
+
.description('Database integration commands');
|
|
269
|
+
// Job history
|
|
270
|
+
this.addSubcommand(dbCmd, {
|
|
271
|
+
name: 'history',
|
|
272
|
+
description: 'Get job execution history from database',
|
|
273
|
+
options: [
|
|
274
|
+
{ flags: '-j, --job-id <jobId>', description: 'Filter by job ID' },
|
|
275
|
+
{ flags: '-l, --limit <limit>', description: 'Limit number of results', defaultValue: '50' }
|
|
276
|
+
],
|
|
277
|
+
action: async (options) => {
|
|
278
|
+
const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(options.jobId, parseInt(options.limit)), { forUser: true, requireRunning: false });
|
|
279
|
+
this.logInfo(`Job History (${jobs.length} records):`);
|
|
280
|
+
jobs.forEach(job => {
|
|
281
|
+
const started = new Date(job.started_at).toLocaleString();
|
|
282
|
+
const completed = job.completed_at ? new Date(job.completed_at).toLocaleString() : 'Running';
|
|
283
|
+
this.logInfo(` ${job.job_id}: ${job.command}`);
|
|
284
|
+
this.logInfo(` Started: ${started}`);
|
|
285
|
+
this.logInfo(` Completed: ${completed}`);
|
|
286
|
+
this.logInfo(` Status: ${job.status}`);
|
|
287
|
+
this.logInfo(` Exit Code: ${job.exit_code || 'N/A'}`);
|
|
288
|
+
this.logInfo('');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// Job statistics
|
|
293
|
+
this.addSubcommand(dbCmd, {
|
|
294
|
+
name: 'stats',
|
|
295
|
+
description: 'Get job statistics from database',
|
|
296
|
+
options: [
|
|
297
|
+
{ flags: '-j, --job-id <jobId>', description: 'Filter by job ID' }
|
|
298
|
+
],
|
|
299
|
+
action: async (options) => {
|
|
300
|
+
const stats = await this.withDaemonAction(async (client) => await client.getJobStatistics(options.jobId), { forUser: true, requireRunning: false });
|
|
301
|
+
this.logInfo('Job Statistics:');
|
|
302
|
+
this.logInfo(` Total Jobs: ${stats.totalJobs}`);
|
|
303
|
+
this.logInfo(` Success Rate: ${stats.successRate.toFixed(1)}%`);
|
|
304
|
+
this.logInfo(` Last Execution: ${stats.lastExecution || 'Never'}`);
|
|
305
|
+
this.logInfo('\n Status Breakdown:');
|
|
306
|
+
Object.entries(stats.byStatus).forEach(([status, count]) => {
|
|
307
|
+
this.logInfo(` ${status}: ${count}`);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// Recent executions
|
|
312
|
+
this.addSubcommand(dbCmd, {
|
|
313
|
+
name: 'recent',
|
|
314
|
+
description: 'Show most recent job executions with output',
|
|
315
|
+
options: [
|
|
316
|
+
{ flags: '-l, --limit <limit>', description: 'Number of recent executions to show', defaultValue: '5' }
|
|
317
|
+
],
|
|
318
|
+
action: async (options) => {
|
|
319
|
+
const jobs = await this.withDaemonAction(async (client) => await client.getJobHistory(undefined, parseInt(options.limit)), { forUser: true });
|
|
320
|
+
this.logInfo(`Recent Job Executions (${jobs.length} records):`);
|
|
321
|
+
jobs.forEach((job, index) => {
|
|
322
|
+
const started = new Date(job.started_at).toLocaleString();
|
|
323
|
+
const status = job.status === 'completed' ? '✅' :
|
|
324
|
+
job.status === 'failed' ? '❌' :
|
|
325
|
+
job.status === 'running' ? '⏳' : '⏸️';
|
|
326
|
+
this.logInfo(`\n${index + 1}. ${status} ${job.job_id}`);
|
|
327
|
+
this.logInfo(` Executed: ${started}`);
|
|
328
|
+
this.logInfo(` Status: ${job.status}`);
|
|
329
|
+
if (job.output) {
|
|
330
|
+
const preview = job.output.substring(0, 200);
|
|
331
|
+
this.logInfo(` Output: ${preview}${job.output.length > 200 ? '...' : ''}`);
|
|
332
|
+
}
|
|
333
|
+
if (job.error) {
|
|
334
|
+
this.logInfo(` Error: ${job.error}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
if (jobs.length === 0) {
|
|
338
|
+
this.logInfo('\nNo recent executions found.');
|
|
339
|
+
this.logInfo(' Trigger jobs to see execution history:');
|
|
340
|
+
this.logInfo(' lsh daemon job trigger <jobId>');
|
|
341
|
+
this.logInfo(' lsh daemon job trigger-all');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
// Job status with executions
|
|
346
|
+
this.addSubcommand(dbCmd, {
|
|
347
|
+
name: 'status',
|
|
348
|
+
description: 'Show detailed status and recent executions of a specific job',
|
|
349
|
+
arguments: [{ name: 'jobId', required: true }],
|
|
350
|
+
action: async (jobId) => {
|
|
351
|
+
await this.withDaemonAction(async (client) => {
|
|
352
|
+
const jobs = await client.listJobs();
|
|
353
|
+
const job = jobs.find(j => j.id === jobId);
|
|
354
|
+
if (!job) {
|
|
355
|
+
throw new Error(`Job ${jobId} not found in daemon registry`);
|
|
356
|
+
}
|
|
357
|
+
this.logInfo(`Job Status: ${job.name} (${jobId})`);
|
|
358
|
+
const cmdPreview = job.command.substring(0, 100);
|
|
359
|
+
this.logInfo(` Command: ${cmdPreview}${job.command.length > 100 ? '...' : ''}`);
|
|
360
|
+
this.logInfo(` Status: ${job.status}`);
|
|
361
|
+
this.logInfo(` Schedule: ${this.formatSchedule(job.schedule)}`);
|
|
362
|
+
this.logInfo(` Priority: ${job.priority}`);
|
|
363
|
+
const executions = await client.getJobHistory(jobId, 5);
|
|
364
|
+
if (executions.length > 0) {
|
|
365
|
+
this.logInfo(`\nRecent Executions (${executions.length} records):`);
|
|
366
|
+
executions.forEach((exec, index) => {
|
|
367
|
+
const started = new Date(exec.started_at).toLocaleString();
|
|
368
|
+
const status = exec.status === 'completed' ? '✅' :
|
|
369
|
+
exec.status === 'failed' ? '❌' :
|
|
370
|
+
exec.status === 'running' ? '⏳' : '⏸️';
|
|
371
|
+
this.logInfo(`\n${index + 1}. ${status} ${started}`);
|
|
372
|
+
this.logInfo(` Status: ${exec.status} (Exit Code: ${exec.exit_code || 'N/A'})`);
|
|
373
|
+
if (exec.output) {
|
|
374
|
+
const preview = exec.output.substring(0, 150);
|
|
375
|
+
this.logInfo(` Output: ${preview}${exec.output.length > 150 ? '...' : ''}`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
this.logInfo('\nNo execution history found for this job.');
|
|
381
|
+
this.logInfo(` Run: lsh daemon job trigger ${jobId}`);
|
|
382
|
+
}
|
|
383
|
+
}, { forUser: true });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
// Sync jobs to database
|
|
387
|
+
this.addSubcommand(dbCmd, {
|
|
388
|
+
name: 'sync',
|
|
389
|
+
description: 'Sync current in-memory jobs to database',
|
|
390
|
+
action: async () => {
|
|
391
|
+
const synced = await this.withDaemonAction(async (client) => {
|
|
392
|
+
const jobs = await client.listJobs();
|
|
393
|
+
this.logInfo(`Syncing ${jobs.length} jobs to database...`);
|
|
394
|
+
let syncCount = 0;
|
|
395
|
+
for (const job of jobs) {
|
|
396
|
+
try {
|
|
397
|
+
const dbStatus = job.status === 'created' ? 'stopped' :
|
|
398
|
+
job.status === 'running' ? 'running' :
|
|
399
|
+
job.status === 'completed' ? 'completed' : 'failed';
|
|
400
|
+
await client.syncJobToDatabase({
|
|
401
|
+
id: job.id,
|
|
402
|
+
name: job.name,
|
|
403
|
+
command: job.command,
|
|
404
|
+
schedule: job.schedule,
|
|
405
|
+
enabled: true,
|
|
406
|
+
databaseSync: true
|
|
407
|
+
}, dbStatus);
|
|
408
|
+
this.logSuccess(` Synced ${job.name} (${job.id}) - status: ${dbStatus}`);
|
|
409
|
+
syncCount++;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
this.logError(` Failed to sync ${job.name}: ${error.message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { syncCount, totalJobs: jobs.length };
|
|
416
|
+
}, { forUser: true });
|
|
417
|
+
this.logSuccess(`\nSuccessfully synced ${synced.syncCount}/${synced.totalJobs} jobs to database`);
|
|
418
|
+
this.logInfo('\nCheck results with:');
|
|
419
|
+
this.logInfo(' lsh daemon db stats');
|
|
420
|
+
this.logInfo(' lsh daemon db history');
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async cleanupDaemon(force = false) {
|
|
425
|
+
this.logInfo('LSH Daemon Cleanup');
|
|
426
|
+
this.logInfo('====================');
|
|
427
|
+
this.logInfo('');
|
|
428
|
+
try {
|
|
429
|
+
// 1. Find daemon processes
|
|
430
|
+
this.logInfo('Finding LSH daemon processes...');
|
|
431
|
+
let daemonPids = [];
|
|
432
|
+
try {
|
|
433
|
+
const { stdout } = await execAsync('pgrep -f "lshd.js"');
|
|
434
|
+
daemonPids = stdout.trim().split('\n').filter(pid => pid.length > 0);
|
|
435
|
+
}
|
|
436
|
+
catch (_error) {
|
|
437
|
+
// No processes found
|
|
438
|
+
}
|
|
439
|
+
if (daemonPids.length > 0) {
|
|
440
|
+
this.logInfo('Found daemon processes:');
|
|
441
|
+
try {
|
|
442
|
+
const { stdout } = await execAsync('ps aux | grep lshd | grep -v grep');
|
|
443
|
+
stdout.split('\n').filter(line => line.trim()).forEach(line => this.logInfo(` ${line}`));
|
|
444
|
+
}
|
|
445
|
+
catch (_error) {
|
|
446
|
+
this.logInfo(` PIDs: ${daemonPids.join(', ')}`);
|
|
447
|
+
}
|
|
448
|
+
this.logInfo('');
|
|
449
|
+
if (force) {
|
|
450
|
+
this.logInfo('Force mode: Killing daemon processes...');
|
|
451
|
+
try {
|
|
452
|
+
await execAsync('pkill -f "lshd.js"');
|
|
453
|
+
this.logSuccess('Daemon processes killed');
|
|
454
|
+
}
|
|
455
|
+
catch (_error) {
|
|
456
|
+
this.logWarning('Some processes may require manual cleanup');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this.logWarning('Found running daemon processes. Use --force to kill them automatically');
|
|
461
|
+
this.logInfo(' Or manually run: pkill -f "lshd.js"');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
this.logInfo('No daemon processes found');
|
|
466
|
+
}
|
|
467
|
+
// 2. Clean up socket files
|
|
468
|
+
this.logInfo('Cleaning up socket files...');
|
|
469
|
+
try {
|
|
470
|
+
const { stdout } = await execAsync(`find /tmp -name "lsh-*daemon*.sock" 2>/dev/null || echo ""`);
|
|
471
|
+
const socketFiles = stdout.trim().split('\n').filter(file => file.length > 0);
|
|
472
|
+
if (socketFiles.length > 0) {
|
|
473
|
+
this.logInfo('Found socket files:');
|
|
474
|
+
socketFiles.forEach(file => this.logInfo(` ${file}`));
|
|
475
|
+
for (const socket of socketFiles) {
|
|
476
|
+
try {
|
|
477
|
+
if (fs.existsSync(socket)) {
|
|
478
|
+
fs.unlinkSync(socket);
|
|
479
|
+
this.logSuccess(`Removed: ${socket}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
this.logWarning(`Could not remove: ${socket} (${error.message})`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.logInfo('No socket files found');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (_error) {
|
|
492
|
+
this.logInfo('No socket files found');
|
|
493
|
+
}
|
|
494
|
+
// 3. Clean up PID files
|
|
495
|
+
this.logInfo('Cleaning up PID files...');
|
|
496
|
+
try {
|
|
497
|
+
const { stdout } = await execAsync(`find /tmp -name "lsh-*daemon*.pid" 2>/dev/null || echo ""`);
|
|
498
|
+
const pidFiles = stdout.trim().split('\n').filter(file => file.length > 0);
|
|
499
|
+
if (pidFiles.length > 0) {
|
|
500
|
+
this.logInfo('Found PID files:');
|
|
501
|
+
pidFiles.forEach(file => this.logInfo(` ${file}`));
|
|
502
|
+
for (const pidFile of pidFiles) {
|
|
503
|
+
try {
|
|
504
|
+
if (fs.existsSync(pidFile)) {
|
|
505
|
+
fs.unlinkSync(pidFile);
|
|
506
|
+
this.logSuccess(`Removed: ${pidFile}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
this.logWarning(`Could not remove: ${pidFile} (${error.message})`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
this.logInfo('No PID files found');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch (_error) {
|
|
519
|
+
this.logInfo('No PID files found');
|
|
520
|
+
}
|
|
521
|
+
// 4. Verify cleanup
|
|
522
|
+
this.logInfo('');
|
|
523
|
+
this.logInfo('Verifying cleanup...');
|
|
524
|
+
let remainingProcesses = [];
|
|
525
|
+
try {
|
|
526
|
+
const { stdout } = await execAsync('pgrep -f "lshd.js"');
|
|
527
|
+
remainingProcesses = stdout.trim().split('\n').filter(pid => pid.length > 0);
|
|
528
|
+
}
|
|
529
|
+
catch (_error) {
|
|
530
|
+
// No processes found (good)
|
|
531
|
+
}
|
|
532
|
+
let remainingSockets = [];
|
|
533
|
+
try {
|
|
534
|
+
const { stdout } = await execAsync(`find /tmp -name "lsh-*daemon*.sock" 2>/dev/null || echo ""`);
|
|
535
|
+
remainingSockets = stdout.trim().split('\n').filter(file => file.length > 0);
|
|
536
|
+
}
|
|
537
|
+
catch (_error) {
|
|
538
|
+
// No sockets found (good)
|
|
539
|
+
}
|
|
540
|
+
if (remainingProcesses.length === 0 && remainingSockets.length === 0) {
|
|
541
|
+
this.logSuccess('Cleanup completed successfully!');
|
|
542
|
+
this.logInfo('');
|
|
543
|
+
this.logInfo('Next steps:');
|
|
544
|
+
this.logInfo(' 1. Start fresh daemon: lsh daemon start');
|
|
545
|
+
this.logInfo(' 2. Check status: lsh daemon status');
|
|
546
|
+
this.logInfo(' 3. View dashboard: ./scripts/monitor-dashboard.sh');
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
this.logWarning('Some items may still remain:');
|
|
550
|
+
if (remainingProcesses.length > 0) {
|
|
551
|
+
this.logInfo(` Processes: ${remainingProcesses.join(', ')}`);
|
|
552
|
+
}
|
|
553
|
+
if (remainingSockets.length > 0) {
|
|
554
|
+
this.logInfo(` Sockets: ${remainingSockets.join(', ')}`);
|
|
555
|
+
}
|
|
556
|
+
this.logInfo('');
|
|
557
|
+
this.logInfo('Try running with --force for complete cleanup:');
|
|
558
|
+
this.logInfo(' lsh daemon cleanup --force');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
throw new Error(`Cleanup failed: ${error.message}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Service - CLI command registration
|
|
3
|
+
* Uses DaemonCommandRegistrar for clean, maintainable command setup
|
|
4
|
+
*/
|
|
5
|
+
import { DaemonCommandRegistrar } from './daemon-registrar.js';
|
|
6
|
+
export async function init_daemon(program) {
|
|
7
|
+
const registrar = new DaemonCommandRegistrar();
|
|
8
|
+
await registrar.register(program);
|
|
9
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getFiles } from "../../util/lib.util.js";
|
|
2
|
+
async function parseCommands(files) {
|
|
3
|
+
const commands = {};
|
|
4
|
+
for (const file of files) {
|
|
5
|
+
if (file !== "lib.ts") {
|
|
6
|
+
const cmd_exports = await import(`./${file.split(".")[0]}.js`);
|
|
7
|
+
for (const [key, value] of Object.entries(cmd_exports)) {
|
|
8
|
+
if (key.indexOf("cmd") !== -1) {
|
|
9
|
+
commands[key.split("cmd_")[1]] = value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return commands;
|
|
15
|
+
}
|
|
16
|
+
export async function loadCommands() {
|
|
17
|
+
const files = await getFiles();
|
|
18
|
+
const cmdMap = await parseCommands(files);
|
|
19
|
+
return cmdMap;
|
|
20
|
+
}
|
|
21
|
+
async function _makeCommand(commander) {
|
|
22
|
+
const _commands = await loadCommands();
|
|
23
|
+
commander.command("jug").action(() => {
|
|
24
|
+
console.log("heat jug");
|
|
25
|
+
});
|
|
26
|
+
commander.command("pot").action(() => {
|
|
27
|
+
console.log("heat pot");
|
|
28
|
+
});
|
|
29
|
+
return commander;
|
|
30
|
+
}
|
|
31
|
+
// export async function init_lib_cmd(program: Command) {
|
|
32
|
+
// const brew = program.command("lib");
|
|
33
|
+
// // const commands = await loadCommands();
|
|
34
|
+
// // await set(lsh.commands, commands);
|
|
35
|
+
// brew.command("tea").action(() => {
|
|
36
|
+
// console.log("brew tea");
|
|
37
|
+
// });
|
|
38
|
+
// brew.command("coffee").action(() => {
|
|
39
|
+
// console.log("brew coffee");
|
|
40
|
+
// });
|
|
41
|
+
// await makeCommand(brew);
|
|
42
|
+
// // for (let c in commands) {
|
|
43
|
+
// // brew.command(c).action(() => console.log(c));
|
|
44
|
+
// // }
|
|
45
|
+
// // .command("lib").description("lsh lib commands");
|
|
46
|
+
// // lib
|
|
47
|
+
// // .showHelpAfterError(true)
|
|
48
|
+
// // .showSuggestionAfterError(true);
|
|
49
|
+
// // const commands = await loadCommands();
|
|
50
|
+
// // set(lsh.commands, commands);
|
|
51
|
+
// // for (const [key, value] of Object.entries(get(lsh.commands))) {
|
|
52
|
+
// // // console.log(`${key} : ${value}`);
|
|
53
|
+
// // lib.command(key).action(() => {console.log(value)});
|
|
54
|
+
// // };
|
|
55
|
+
// // .action(async (type: String, action: String, spec: Spec) => {
|
|
56
|
+
// // const commands = await loadCommands();
|
|
57
|
+
// // set(lsh.commands, commands);
|
|
58
|
+
// // switch (type) {
|
|
59
|
+
// // case "ls":
|
|
60
|
+
// // // console.log("lsh called");
|
|
61
|
+
// // // console.log(get(lsh.commands)['rand']());
|
|
62
|
+
// // break;
|
|
63
|
+
// // default:
|
|
64
|
+
// // console.log("default");
|
|
65
|
+
// // }
|
|
66
|
+
// // });
|
|
67
|
+
// }
|
|
68
|
+
export async function init_lib(program) {
|
|
69
|
+
const lib = program.command("lib");
|
|
70
|
+
// Load and register dynamic commands
|
|
71
|
+
const commands = await loadCommands();
|
|
72
|
+
for (const commandName of Object.keys(commands)) {
|
|
73
|
+
lib
|
|
74
|
+
.command(commandName)
|
|
75
|
+
.action(async () => {
|
|
76
|
+
console.log(commands[commandName]());
|
|
77
|
+
})
|
|
78
|
+
.description("commandName")
|
|
79
|
+
.usage(`${commandName} used as follows:`);
|
|
80
|
+
}
|
|
81
|
+
// Optional: Enhance the 'lib' command group with additional descriptions and error handling
|
|
82
|
+
lib
|
|
83
|
+
.showHelpAfterError("Command not recognized, here's some help.")
|
|
84
|
+
.showSuggestionAfterError(true);
|
|
85
|
+
return lib;
|
|
86
|
+
}
|