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,232 @@
|
|
|
1
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
3
|
+
import { stdout } from "process";
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
import { Script, createContext } from "vm";
|
|
6
|
+
import { shell_exec } from "../lib/shell.lib.js";
|
|
7
|
+
import { parseShellCommand } from "../lib/shell-parser.js";
|
|
8
|
+
import { ShellExecutor } from "../lib/shell-executor.js";
|
|
9
|
+
export const Terminal = () => {
|
|
10
|
+
const [input, setInput] = useState("");
|
|
11
|
+
const [lines, setLines] = useState([]);
|
|
12
|
+
const [_mode, _setMode] = useState("auto"); // auto, js, shell
|
|
13
|
+
const [workingDir, setWorkingDir] = useState(process.cwd());
|
|
14
|
+
const [shellExecutor] = useState(() => new ShellExecutor());
|
|
15
|
+
const { exit } = useApp();
|
|
16
|
+
// Check if raw mode is supported synchronously
|
|
17
|
+
const isRawModeSupported = process.stdin.isTTY && process.stdin.setRawMode !== undefined;
|
|
18
|
+
// Detect if input is a shell command or JavaScript
|
|
19
|
+
const detectInputType = (input) => {
|
|
20
|
+
const trimmed = input.trim();
|
|
21
|
+
// Check for common shell commands
|
|
22
|
+
const shellCommands = ["ls", "cd", "pwd", "mkdir", "rm", "cp", "mv", "cat", "grep", "find", "ps", "kill", "chmod", "chown", "git", "npm", "node", "python", "curl", "wget"];
|
|
23
|
+
const firstWord = trimmed.split(' ')[0];
|
|
24
|
+
// Check for JavaScript patterns
|
|
25
|
+
const jsPatterns = [
|
|
26
|
+
/^(const|let|var|function|class|\{|\[|if|for|while|try|async|await)/,
|
|
27
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[=]/,
|
|
28
|
+
/\.(map|filter|reduce|forEach)/,
|
|
29
|
+
/^Math\.|^JSON\.|^console\./,
|
|
30
|
+
/^[0-9]+(\.[0-9]+)?(\s*[+\-*/])/
|
|
31
|
+
];
|
|
32
|
+
// Force modes
|
|
33
|
+
if (trimmed.startsWith("js:"))
|
|
34
|
+
return "js";
|
|
35
|
+
if (trimmed.startsWith("sh:") || trimmed.startsWith("$"))
|
|
36
|
+
return "shell";
|
|
37
|
+
// JavaScript detection
|
|
38
|
+
if (jsPatterns.some(pattern => pattern.test(trimmed)))
|
|
39
|
+
return "js";
|
|
40
|
+
// Shell command detection
|
|
41
|
+
if (shellCommands.includes(firstWord))
|
|
42
|
+
return "shell";
|
|
43
|
+
// Default to shell for most other cases
|
|
44
|
+
return "shell";
|
|
45
|
+
};
|
|
46
|
+
// Execute JavaScript code in a VM context with enhanced features
|
|
47
|
+
const executeScript = async (input) => {
|
|
48
|
+
try {
|
|
49
|
+
let code = input;
|
|
50
|
+
if (code.startsWith("js:"))
|
|
51
|
+
code = code.substring(3).trim();
|
|
52
|
+
// Create context with useful globals
|
|
53
|
+
const context = createContext({
|
|
54
|
+
console,
|
|
55
|
+
process,
|
|
56
|
+
__dirname: workingDir,
|
|
57
|
+
__filename: '',
|
|
58
|
+
global,
|
|
59
|
+
Buffer,
|
|
60
|
+
setTimeout,
|
|
61
|
+
setInterval,
|
|
62
|
+
clearTimeout,
|
|
63
|
+
clearInterval,
|
|
64
|
+
shell_exec, // Make shell_exec available in JS context
|
|
65
|
+
sh: shell_exec, // Short alias
|
|
66
|
+
});
|
|
67
|
+
const script = new Script(code);
|
|
68
|
+
const result = script.runInContext(context);
|
|
69
|
+
return `${result !== undefined ? result : '(undefined)'}`;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return `JS Error: ${error.message}`;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
// Execute shell command using POSIX parser and executor
|
|
76
|
+
const executeShell = async (input) => {
|
|
77
|
+
try {
|
|
78
|
+
let command = input.trim();
|
|
79
|
+
if (command.startsWith("sh:"))
|
|
80
|
+
command = command.substring(3).trim();
|
|
81
|
+
if (command.startsWith("$"))
|
|
82
|
+
command = command.substring(1).trim();
|
|
83
|
+
// Parse the command using POSIX grammar
|
|
84
|
+
const ast = parseShellCommand(command);
|
|
85
|
+
// Execute the parsed command
|
|
86
|
+
const result = await shellExecutor.execute(ast);
|
|
87
|
+
// Update working directory from executor context
|
|
88
|
+
const context = shellExecutor.getContext();
|
|
89
|
+
if (context.cwd !== workingDir) {
|
|
90
|
+
setWorkingDir(context.cwd);
|
|
91
|
+
}
|
|
92
|
+
if (!result.success && result.stderr) {
|
|
93
|
+
return `Shell Error: ${result.stderr}`;
|
|
94
|
+
}
|
|
95
|
+
let output = "";
|
|
96
|
+
if (result.stdout)
|
|
97
|
+
output += result.stdout;
|
|
98
|
+
if (result.stderr)
|
|
99
|
+
output += (output ? "\n" : "") + `stderr: ${result.stderr}`;
|
|
100
|
+
return output || "(no output)";
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Fallback to old shell execution for unparseable commands
|
|
104
|
+
try {
|
|
105
|
+
let command = input.trim();
|
|
106
|
+
if (command.startsWith("sh:"))
|
|
107
|
+
command = command.substring(3).trim();
|
|
108
|
+
if (command.startsWith("$"))
|
|
109
|
+
command = command.substring(1).trim();
|
|
110
|
+
const result = await shell_exec(command);
|
|
111
|
+
if (result.error) {
|
|
112
|
+
return `Shell Error: ${result.error}`;
|
|
113
|
+
}
|
|
114
|
+
let output = "";
|
|
115
|
+
if (result.stdout)
|
|
116
|
+
output += result.stdout;
|
|
117
|
+
if (result.stderr)
|
|
118
|
+
output += (output ? "\n" : "") + `stderr: ${result.stderr}`;
|
|
119
|
+
return output || "(no output)";
|
|
120
|
+
}
|
|
121
|
+
catch (_fallbackError) {
|
|
122
|
+
return `Shell Error: ${error.message}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// Handle input submission
|
|
127
|
+
const handleSubmit = async () => {
|
|
128
|
+
if (!input.trim())
|
|
129
|
+
return;
|
|
130
|
+
const inputType = detectInputType(input);
|
|
131
|
+
const prompt = `[${workingDir.split('/').pop() || '/'}] ${inputType === 'js' ? 'js>' : '$'} ${input}`;
|
|
132
|
+
setLines(prev => [...prev, prompt]);
|
|
133
|
+
try {
|
|
134
|
+
let result;
|
|
135
|
+
if (inputType === "js") {
|
|
136
|
+
result = await executeScript(input);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
result = await executeShell(input);
|
|
140
|
+
}
|
|
141
|
+
// Add result with proper formatting and spacing
|
|
142
|
+
setLines(prev => [...prev, result, ""]);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Add error and a blank line for spacing
|
|
146
|
+
setLines(prev => [...prev, `Error: ${error.message}`, ""]);
|
|
147
|
+
}
|
|
148
|
+
setInput("");
|
|
149
|
+
};
|
|
150
|
+
// Handle resizing of the terminal
|
|
151
|
+
const [size, setSize] = useState({
|
|
152
|
+
columns: stdout.columns,
|
|
153
|
+
rows: stdout.rows,
|
|
154
|
+
});
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const handleResize = () => {
|
|
157
|
+
setSize({
|
|
158
|
+
columns: stdout.columns,
|
|
159
|
+
rows: stdout.rows,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
stdout.on("resize", handleResize);
|
|
163
|
+
return () => {
|
|
164
|
+
stdout.off("resize", handleResize);
|
|
165
|
+
};
|
|
166
|
+
}, []);
|
|
167
|
+
// Only use input if raw mode is supported
|
|
168
|
+
const [_isInteractive, setIsInteractive] = useState(false);
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
// Check if raw mode is supported
|
|
171
|
+
setIsInteractive(process.stdin.isTTY && process.stdin.setRawMode !== undefined);
|
|
172
|
+
}, []);
|
|
173
|
+
useInput((input, key) => {
|
|
174
|
+
if (!isRawModeSupported)
|
|
175
|
+
return;
|
|
176
|
+
if (key.return) {
|
|
177
|
+
handleSubmit();
|
|
178
|
+
}
|
|
179
|
+
else if (key.ctrl && input === "c") {
|
|
180
|
+
exit();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Initialize with welcome message
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
setLines([
|
|
186
|
+
"🚀 LSH Interactive Shell",
|
|
187
|
+
"Mix shell commands and JavaScript seamlessly!",
|
|
188
|
+
"Examples:",
|
|
189
|
+
" ls -la (shell command)",
|
|
190
|
+
" 2 + 2 (JavaScript expression)",
|
|
191
|
+
" js: console.log('Hello') (force JS mode)",
|
|
192
|
+
" sh: echo 'Hi' (force shell mode)",
|
|
193
|
+
" await sh('ls') (call shell from JS)",
|
|
194
|
+
"---"
|
|
195
|
+
]);
|
|
196
|
+
}, []);
|
|
197
|
+
const currentDir = workingDir.split('/').pop() || '/';
|
|
198
|
+
const nextInputType = detectInputType(input);
|
|
199
|
+
const promptSymbol = nextInputType === 'js' ? 'js>' : '$';
|
|
200
|
+
if (!isRawModeSupported) {
|
|
201
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
202
|
+
React.createElement(Text, { color: "red", bold: true }, "\u26A0\uFE0F Interactive mode not supported"),
|
|
203
|
+
React.createElement(Text, { color: "yellow" }, "Raw mode is not supported in this environment."),
|
|
204
|
+
React.createElement(Text, { color: "gray" }, "To use the interactive REPL, run this command in a proper terminal:"),
|
|
205
|
+
React.createElement(Text, { color: "cyan" }, " npm start repl"),
|
|
206
|
+
React.createElement(Text, { color: "gray" }, "or"),
|
|
207
|
+
React.createElement(Text, { color: "cyan" }, " node dist/app.js repl"),
|
|
208
|
+
React.createElement(Text, { color: "gray" }, "For testing, use the shell lib functions directly in your Node.js code.")));
|
|
209
|
+
}
|
|
210
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1, width: size.columns, height: size.rows },
|
|
211
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
212
|
+
React.createElement(Text, { color: "cyan", bold: true }, "LSH - Interactive Shell with JavaScript"),
|
|
213
|
+
React.createElement(Text, { color: "gray" },
|
|
214
|
+
"Current directory: ",
|
|
215
|
+
workingDir),
|
|
216
|
+
React.createElement(Text, { color: "gray" },
|
|
217
|
+
"Mode: Auto-detect (",
|
|
218
|
+
nextInputType === 'js' ? 'JavaScript' : 'Shell',
|
|
219
|
+
")"),
|
|
220
|
+
React.createElement(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, marginY: 1, height: size.rows - 8, flexDirection: "column", width: "100%" },
|
|
221
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", width: "100%" }, lines.slice(Math.max(0, lines.length - (size.rows - 15))).map((line, index) => (React.createElement(Text, { key: index, wrap: "wrap", width: size.columns - 6 }, line)))),
|
|
222
|
+
React.createElement(Box, { flexDirection: "row", marginTop: 1, width: "100%" },
|
|
223
|
+
React.createElement(Text, { color: "green" },
|
|
224
|
+
"[",
|
|
225
|
+
currentDir,
|
|
226
|
+
"] ",
|
|
227
|
+
promptSymbol,
|
|
228
|
+
" "),
|
|
229
|
+
React.createElement(Box, { flexGrow: 1, width: size.columns - currentDir.length - 8 },
|
|
230
|
+
React.createElement(TextInput, { value: input, onChange: setInput, placeholder: "Enter shell command or JavaScript..." }))))),
|
|
231
|
+
React.createElement(Text, { color: "grey" }, "Press Ctrl+C to exit. Use js: or sh: to force execution mode.")));
|
|
232
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useInput, useStdout } from 'ink';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Divider } from './Divider.js';
|
|
4
|
+
export function UserInput({ ..._props }) {
|
|
5
|
+
const [_feature, _setFeature] = useState('');
|
|
6
|
+
const [_readData, _setReadData] = useState([]);
|
|
7
|
+
const [_writeData, _setWriteData] = useState([]);
|
|
8
|
+
const [_userInput, _setUserInput] = useState('');
|
|
9
|
+
const [_name, _setName] = useState('');
|
|
10
|
+
const _handleSubmit = (_query) => {
|
|
11
|
+
// Do something with query
|
|
12
|
+
console.log(_query);
|
|
13
|
+
_setUserInput('');
|
|
14
|
+
};
|
|
15
|
+
const { stdout: _stdout, write: _write } = useStdout();
|
|
16
|
+
const _handleChange = (_query) => {
|
|
17
|
+
};
|
|
18
|
+
useInput((input, _key) => {
|
|
19
|
+
console.log(input);
|
|
20
|
+
// console.log(key);
|
|
21
|
+
if (input === 'q') {
|
|
22
|
+
// Exit program
|
|
23
|
+
}
|
|
24
|
+
;
|
|
25
|
+
});
|
|
26
|
+
const input = (React.createElement(React.Fragment, null,
|
|
27
|
+
React.createElement(Divider, null),
|
|
28
|
+
React.createElement(Divider, null)));
|
|
29
|
+
return input;
|
|
30
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH API Server - RESTful API for daemon control and job management
|
|
3
|
+
*/
|
|
4
|
+
import cors from 'cors';
|
|
5
|
+
import jwt from 'jsonwebtoken';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { handleApiOperation } from '../lib/api-error-handler.js';
|
|
8
|
+
import { BaseAPIServer } from '../lib/base-api-server.js';
|
|
9
|
+
export class LSHApiServer extends BaseAPIServer {
|
|
10
|
+
daemon;
|
|
11
|
+
apiConfig;
|
|
12
|
+
clients = new Set(); // SSE clients
|
|
13
|
+
constructor(daemon, config = {}) {
|
|
14
|
+
const baseConfig = {
|
|
15
|
+
port: config.port || 3030,
|
|
16
|
+
corsOrigins: config.corsOrigins || ['http://localhost:*'],
|
|
17
|
+
jsonLimit: config.jsonLimit || '10mb',
|
|
18
|
+
enableHelmet: config.enableHelmet !== false,
|
|
19
|
+
enableRequestLogging: config.enableRequestLogging !== false,
|
|
20
|
+
};
|
|
21
|
+
super(baseConfig, 'LSHApiServer');
|
|
22
|
+
this.daemon = daemon;
|
|
23
|
+
this.apiConfig = {
|
|
24
|
+
...this.config,
|
|
25
|
+
apiKey: config.apiKey || process.env.LSH_API_KEY || crypto.randomBytes(32).toString('hex'),
|
|
26
|
+
jwtSecret: config.jwtSecret || process.env.LSH_JWT_SECRET || crypto.randomBytes(32).toString('hex'),
|
|
27
|
+
enableWebhooks: config.enableWebhooks || false,
|
|
28
|
+
webhookEndpoints: config.webhookEndpoints || [],
|
|
29
|
+
...config
|
|
30
|
+
};
|
|
31
|
+
this.setupEventHandlers();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Override CORS configuration to support wildcard patterns
|
|
35
|
+
*/
|
|
36
|
+
configureCORS() {
|
|
37
|
+
const origins = this.config.corsOrigins;
|
|
38
|
+
if (origins === '*' || !origins) {
|
|
39
|
+
return cors();
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(origins)) {
|
|
42
|
+
return cors({
|
|
43
|
+
origin: (origin, callback) => {
|
|
44
|
+
if (!origin || origins.some(pattern => {
|
|
45
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
46
|
+
return regex.test(origin);
|
|
47
|
+
})) {
|
|
48
|
+
callback(null, true);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
callback(new Error('Not allowed by CORS'));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return cors({ origin: origins });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Helper method to handle API operations with automatic error handling and webhooks
|
|
60
|
+
*/
|
|
61
|
+
async handleOperation(res, operation, successStatus = 200, webhookEvent) {
|
|
62
|
+
await handleApiOperation(res, operation, { successStatus, webhookEvent }, this.apiConfig.enableWebhooks ? this.triggerWebhook.bind(this) : undefined);
|
|
63
|
+
}
|
|
64
|
+
authenticateRequest(req, res, next) {
|
|
65
|
+
const apiKey = req.headers['x-api-key'];
|
|
66
|
+
const authHeader = req.headers['authorization'];
|
|
67
|
+
// Check API key
|
|
68
|
+
if (apiKey && apiKey === this.apiConfig.apiKey) {
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
// Check JWT token
|
|
72
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
73
|
+
const token = authHeader.substring(7);
|
|
74
|
+
try {
|
|
75
|
+
jwt.verify(token, this.apiConfig.jwtSecret);
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
catch (_err) {
|
|
79
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
83
|
+
}
|
|
84
|
+
setupRoutes() {
|
|
85
|
+
// Health check (no auth)
|
|
86
|
+
this.app.get('/health', (req, res) => {
|
|
87
|
+
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
88
|
+
});
|
|
89
|
+
// Generate JWT token
|
|
90
|
+
this.app.post('/api/auth', (req, res) => {
|
|
91
|
+
const { apiKey } = req.body;
|
|
92
|
+
if (apiKey === this.apiConfig.apiKey) {
|
|
93
|
+
const token = jwt.sign({ type: 'api-access', created: Date.now() }, this.apiConfig.jwtSecret, { expiresIn: '24h' });
|
|
94
|
+
res.json({ token });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
res.status(401).json({ error: 'Invalid API key' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Protected routes
|
|
101
|
+
this.app.use('/api', this.authenticateRequest.bind(this));
|
|
102
|
+
// Daemon status
|
|
103
|
+
this.app.get('/api/status', async (req, res) => {
|
|
104
|
+
await this.handleOperation(res, async () => this.daemon.getStatus());
|
|
105
|
+
});
|
|
106
|
+
// Job management
|
|
107
|
+
this.app.get('/api/jobs', async (req, res) => {
|
|
108
|
+
await this.handleOperation(res, async () => {
|
|
109
|
+
const { filter, limit } = req.query;
|
|
110
|
+
return this.daemon.listJobs(filter ? JSON.parse(filter) : undefined, limit ? parseInt(limit) : undefined);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
this.app.post('/api/jobs', async (req, res) => {
|
|
114
|
+
await this.handleOperation(res, async () => this.daemon.addJob(req.body), 201, 'job.created');
|
|
115
|
+
});
|
|
116
|
+
this.app.get('/api/jobs/:id', (req, res) => {
|
|
117
|
+
const job = this.daemon.getJob(req.params.id);
|
|
118
|
+
if (!job) {
|
|
119
|
+
return res.status(404).json({ error: 'Job not found' });
|
|
120
|
+
}
|
|
121
|
+
res.json(job);
|
|
122
|
+
});
|
|
123
|
+
this.app.post('/api/jobs/:id/start', async (req, res) => {
|
|
124
|
+
await this.handleOperation(res, async () => this.daemon.startJob(req.params.id), 200, 'job.started');
|
|
125
|
+
});
|
|
126
|
+
this.app.post('/api/jobs/:id/stop', async (req, res) => {
|
|
127
|
+
await this.handleOperation(res, async () => {
|
|
128
|
+
const { signal } = req.body;
|
|
129
|
+
return this.daemon.stopJob(req.params.id, signal);
|
|
130
|
+
}, 200, 'job.stopped');
|
|
131
|
+
});
|
|
132
|
+
this.app.post('/api/jobs/:id/trigger', async (req, res) => {
|
|
133
|
+
await this.handleOperation(res, async () => this.daemon.triggerJob(req.params.id));
|
|
134
|
+
});
|
|
135
|
+
this.app.delete('/api/jobs/:id', async (req, res) => {
|
|
136
|
+
await this.handleOperation(res, async () => {
|
|
137
|
+
const force = req.query.force === 'true';
|
|
138
|
+
const success = await this.daemon.removeJob(req.params.id, force);
|
|
139
|
+
if (!success) {
|
|
140
|
+
throw new Error('Failed to remove job');
|
|
141
|
+
}
|
|
142
|
+
return { id: req.params.id };
|
|
143
|
+
}, 204, 'job.removed');
|
|
144
|
+
});
|
|
145
|
+
// Bulk operations
|
|
146
|
+
this.app.post('/api/jobs/bulk', async (req, res) => {
|
|
147
|
+
const { jobs } = req.body;
|
|
148
|
+
if (!Array.isArray(jobs)) {
|
|
149
|
+
return res.status(400).json({ error: 'Jobs must be an array' });
|
|
150
|
+
}
|
|
151
|
+
const results = [];
|
|
152
|
+
for (const jobSpec of jobs) {
|
|
153
|
+
try {
|
|
154
|
+
const job = await this.daemon.addJob(jobSpec);
|
|
155
|
+
results.push({ success: true, job });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
results.push({ success: false, error: error.message, jobSpec });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
res.json({ results });
|
|
162
|
+
});
|
|
163
|
+
// Server-sent events for real-time updates
|
|
164
|
+
this.app.get('/api/events', (req, res) => {
|
|
165
|
+
res.writeHead(200, {
|
|
166
|
+
'Content-Type': 'text/event-stream',
|
|
167
|
+
'Cache-Control': 'no-cache',
|
|
168
|
+
'Connection': 'keep-alive',
|
|
169
|
+
'X-Accel-Buffering': 'no' // Disable nginx buffering
|
|
170
|
+
});
|
|
171
|
+
// Send initial ping
|
|
172
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`);
|
|
173
|
+
// Add client to set
|
|
174
|
+
this.clients.add(res);
|
|
175
|
+
// Setup heartbeat
|
|
176
|
+
const heartbeat = setInterval(() => {
|
|
177
|
+
res.write(`:ping\n\n`);
|
|
178
|
+
}, 30000);
|
|
179
|
+
// Cleanup on disconnect
|
|
180
|
+
req.on('close', () => {
|
|
181
|
+
clearInterval(heartbeat);
|
|
182
|
+
this.clients.delete(res);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
// Webhook management
|
|
186
|
+
this.app.get('/api/webhooks', (req, res) => {
|
|
187
|
+
res.json({
|
|
188
|
+
enabled: this.apiConfig.enableWebhooks,
|
|
189
|
+
endpoints: this.apiConfig.webhookEndpoints
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
this.app.post('/api/webhooks', (req, res) => {
|
|
193
|
+
const { endpoint } = req.body;
|
|
194
|
+
if (!endpoint) {
|
|
195
|
+
return res.status(400).json({ error: 'Endpoint URL required' });
|
|
196
|
+
}
|
|
197
|
+
if (!this.apiConfig.webhookEndpoints?.includes(endpoint)) {
|
|
198
|
+
this.apiConfig.webhookEndpoints?.push(endpoint);
|
|
199
|
+
}
|
|
200
|
+
res.json({ success: true, endpoints: this.apiConfig.webhookEndpoints });
|
|
201
|
+
});
|
|
202
|
+
// Data export endpoints for integration
|
|
203
|
+
this.app.get('/api/export/jobs', async (req, res) => {
|
|
204
|
+
const jobs = await this.daemon.listJobs();
|
|
205
|
+
const format = req.query.format || 'json';
|
|
206
|
+
if (format === 'csv') {
|
|
207
|
+
res.setHeader('Content-Type', 'text/csv');
|
|
208
|
+
res.setHeader('Content-Disposition', 'attachment; filename="jobs.csv"');
|
|
209
|
+
const csv = this.convertToCSV(jobs);
|
|
210
|
+
res.send(csv);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
res.json(jobs);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// Supabase integration endpoint
|
|
217
|
+
this.app.post('/api/supabase/sync', async (req, res) => {
|
|
218
|
+
await this.handleOperation(res, async () => {
|
|
219
|
+
// This endpoint can be called by Supabase jobs to sync data
|
|
220
|
+
const { table, operation, data } = req.body;
|
|
221
|
+
// Emit event for mcli listener
|
|
222
|
+
this.emit('supabase:sync', { table, operation, data });
|
|
223
|
+
// Broadcast to SSE clients
|
|
224
|
+
this.broadcastToClients({
|
|
225
|
+
type: 'supabase:sync',
|
|
226
|
+
table,
|
|
227
|
+
operation,
|
|
228
|
+
data,
|
|
229
|
+
timestamp: Date.now()
|
|
230
|
+
});
|
|
231
|
+
return { success: true, message: 'Data synced' };
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
setupEventHandlers() {
|
|
236
|
+
// Listen to daemon events and broadcast to SSE clients
|
|
237
|
+
const events = ['job:started', 'job:completed', 'job:failed', 'job:stopped'];
|
|
238
|
+
events.forEach(event => {
|
|
239
|
+
this.daemon.on(event, (data) => {
|
|
240
|
+
this.broadcastToClients({
|
|
241
|
+
type: event,
|
|
242
|
+
data,
|
|
243
|
+
timestamp: Date.now()
|
|
244
|
+
});
|
|
245
|
+
if (this.apiConfig.enableWebhooks) {
|
|
246
|
+
this.triggerWebhook(event, data);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
broadcastToClients(data) {
|
|
252
|
+
const message = `data: ${JSON.stringify(data)}\n\n`;
|
|
253
|
+
this.clients.forEach(client => {
|
|
254
|
+
client.write(message);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async triggerWebhook(event, data) {
|
|
258
|
+
if (!this.apiConfig.webhookEndpoints?.length)
|
|
259
|
+
return;
|
|
260
|
+
const payload = {
|
|
261
|
+
event,
|
|
262
|
+
data,
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
source: 'lsh-daemon'
|
|
265
|
+
};
|
|
266
|
+
for (const endpoint of this.apiConfig.webhookEndpoints) {
|
|
267
|
+
try {
|
|
268
|
+
await fetch(endpoint, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: {
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
'X-LSH-Event': event
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify(payload)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
this.logger.error(`Webhook failed for ${endpoint}`, error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
convertToCSV(data) {
|
|
283
|
+
if (!data.length)
|
|
284
|
+
return '';
|
|
285
|
+
const headers = Object.keys(data[0]);
|
|
286
|
+
const csv = [
|
|
287
|
+
headers.join(','),
|
|
288
|
+
...data.map(row => headers.map(header => {
|
|
289
|
+
const value = row[header];
|
|
290
|
+
return typeof value === 'string' && value.includes(',')
|
|
291
|
+
? `"${value}"`
|
|
292
|
+
: value;
|
|
293
|
+
}).join(','))
|
|
294
|
+
];
|
|
295
|
+
return csv.join('\n');
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Override onStop to cleanup SSE connections
|
|
299
|
+
*/
|
|
300
|
+
onStop() {
|
|
301
|
+
// Close all SSE connections
|
|
302
|
+
this.clients.forEach(client => client.end());
|
|
303
|
+
this.clients.clear();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Override start to log API key
|
|
307
|
+
*/
|
|
308
|
+
async start() {
|
|
309
|
+
await super.start();
|
|
310
|
+
this.logger.info(`API Key: ${this.apiConfig.apiKey}`);
|
|
311
|
+
}
|
|
312
|
+
getApiKey() {
|
|
313
|
+
return this.apiConfig.apiKey;
|
|
314
|
+
}
|
|
315
|
+
}
|