spiralcord-full 2.1.0
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/bin/spiral.js +496 -0
- package/package.json +55 -0
- package/src/index.js +7 -0
- package/src/plugins/manager.js +413 -0
- package/src/runtime.js +180 -0
- package/src/test.js +361 -0
- package/src/web/public/index.html +358 -0
- package/src/web/server.js +184 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Spiralcord - Plugin Manager</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; min-height: 100vh; }
|
|
10
|
+
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; display: flex; justify-content: space-between; align-items: center; }
|
|
11
|
+
.header h1 { font-size: 22px; }
|
|
12
|
+
.status { display: flex; align-items: center; gap: 8px; font-size: 14px; }
|
|
13
|
+
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #ff4444; }
|
|
14
|
+
.status-dot.online { background: #00ff00; }
|
|
15
|
+
.container { display: flex; height: calc(100vh - 70px); }
|
|
16
|
+
.sidebar { width: 280px; background: #161b22; padding: 15px; overflow-y: auto; border-right: 1px solid #30363d; }
|
|
17
|
+
.main { flex: 1; padding: 20px; overflow-y: auto; }
|
|
18
|
+
.section-title { color: #667eea; font-size: 14px; margin: 15px 0 8px; text-transform: uppercase; letter-spacing: 1px; }
|
|
19
|
+
.plugin-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s; }
|
|
20
|
+
.plugin-card:hover { border-color: #667eea; }
|
|
21
|
+
.plugin-card.active { border-color: #667eea; background: #1c2333; }
|
|
22
|
+
.plugin-card .name { font-weight: 600; color: #c9d1d9; }
|
|
23
|
+
.plugin-card .desc { font-size: 12px; color: #8b949e; margin-top: 3px; }
|
|
24
|
+
.plugin-card .meta { display: flex; gap: 10px; margin-top: 5px; font-size: 11px; }
|
|
25
|
+
.plugin-card .badge { padding: 2px 6px; border-radius: 3px; font-size: 10px; }
|
|
26
|
+
.badge-builtin { background: #1f6feb33; color: #58a6ff; }
|
|
27
|
+
.badge-user { background: #23883333; color: #3fb950; }
|
|
28
|
+
.badge-enabled { background: #23883333; color: #3fb950; }
|
|
29
|
+
.badge-disabled { background: #da363433; color: #f85149; }
|
|
30
|
+
.detail-panel { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; }
|
|
31
|
+
.detail-panel h2 { color: #667eea; margin-bottom: 15px; }
|
|
32
|
+
.detail-panel h3 { color: #c9d1d9; font-size: 16px; margin: 15px 0 8px; }
|
|
33
|
+
.form-group { margin-bottom: 12px; }
|
|
34
|
+
.form-group label { display: block; margin-bottom: 4px; color: #8b949e; font-size: 13px; }
|
|
35
|
+
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 5px; color: #c9d1d9; font-size: 14px; }
|
|
36
|
+
.form-group textarea { min-height: 80px; font-family: monospace; }
|
|
37
|
+
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; transition: opacity 0.2s; }
|
|
38
|
+
.btn:hover { opacity: 0.8; }
|
|
39
|
+
.btn-primary { background: #667eea; color: #fff; }
|
|
40
|
+
.btn-success { background: #238636; color: #fff; }
|
|
41
|
+
.btn-danger { background: #da3634; color: #fff; }
|
|
42
|
+
.btn-group { display: flex; gap: 8px; margin-top: 15px; }
|
|
43
|
+
.cmd-list { margin-top: 10px; }
|
|
44
|
+
.cmd-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; background: #0d1117; border-radius: 5px; margin-top: 5px; }
|
|
45
|
+
.cmd-item .cmd-name { font-family: monospace; color: #667eea; }
|
|
46
|
+
.cmd-item .cmd-desc { color: #8b949e; font-size: 12px; }
|
|
47
|
+
.config-form { margin-top: 15px; }
|
|
48
|
+
.config-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: #0d1117; border-radius: 5px; margin-top: 5px; }
|
|
49
|
+
.config-item .config-key { font-family: monospace; color: #58a6ff; }
|
|
50
|
+
.config-item .config-value { display: flex; gap: 10px; align-items: center; }
|
|
51
|
+
.config-item input { width: 150px; padding: 4px 8px; background: #161b22; border: 1px solid #30363d; border-radius: 3px; color: #c9d1d9; }
|
|
52
|
+
.empty-state { text-align: center; padding: 40px; color: #8b949e; }
|
|
53
|
+
.empty-state h3 { margin-bottom: 10px; }
|
|
54
|
+
.tabs { display: flex; gap: 5px; margin-bottom: 15px; }
|
|
55
|
+
.tab { padding: 6px 12px; background: #161b22; border: 1px solid #30363d; border-radius: 5px; cursor: pointer; font-size: 13px; }
|
|
56
|
+
.tab.active { background: #667eea; border-color: #667eea; }
|
|
57
|
+
.tab:hover { border-color: #667eea; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<div class="header">
|
|
62
|
+
<h1>🌀 Spiralcord - Plugin Manager</h1>
|
|
63
|
+
<div class="status">
|
|
64
|
+
<div class="status-dot" id="statusDot"></div>
|
|
65
|
+
<span id="statusText">Disconnected</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="container">
|
|
70
|
+
<div class="sidebar">
|
|
71
|
+
<div class="section-title">Builtin Plugins</div>
|
|
72
|
+
<div id="builtinPlugins"></div>
|
|
73
|
+
<div class="section-title">User Plugins</div>
|
|
74
|
+
<div id="userPlugins"></div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="main">
|
|
78
|
+
<div id="emptyState" class="empty-state">
|
|
79
|
+
<h3>Select a plugin to view details</h3>
|
|
80
|
+
<p>Choose a plugin from the sidebar to configure it</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div id="pluginDetail" style="display: none;">
|
|
84
|
+
<div class="detail-panel">
|
|
85
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
86
|
+
<div>
|
|
87
|
+
<h2 id="pluginName"></h2>
|
|
88
|
+
<p id="pluginDesc" style="color: #8b949e;"></p>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="btn-group" style="margin: 0;">
|
|
91
|
+
<button class="btn btn-success" id="enableBtn" onclick="togglePlugin()">Enable</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="tabs">
|
|
96
|
+
<div class="tab active" onclick="showTab('commands')">Commands</div>
|
|
97
|
+
<div class="tab" onclick="showTab('config')">Configuration</div>
|
|
98
|
+
<div class="tab" onclick="showTab('info')">Info</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div id="commandsTab">
|
|
102
|
+
<h3>Commands</h3>
|
|
103
|
+
<div id="commandsList" class="cmd-list"></div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div id="configTab" style="display: none;">
|
|
107
|
+
<h3>Configuration</h3>
|
|
108
|
+
<div id="configForm" class="config-form"></div>
|
|
109
|
+
<button class="btn btn-primary" onclick="savePluginConfig()" style="margin-top: 10px;">Save Config</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div id="infoTab" style="display: none;">
|
|
113
|
+
<h3>Plugin Info</h3>
|
|
114
|
+
<div id="pluginInfo"></div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
let plugins = [];
|
|
123
|
+
let selectedPlugin = null;
|
|
124
|
+
let pluginConfig = {};
|
|
125
|
+
let configSchema = null;
|
|
126
|
+
|
|
127
|
+
async function init() {
|
|
128
|
+
await loadPlugins();
|
|
129
|
+
updateStatus();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function loadPlugins() {
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch('/api/plugins');
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
if (data.success) {
|
|
137
|
+
plugins = data.plugins;
|
|
138
|
+
renderPluginLists();
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Failed to load plugins:', error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderPluginLists() {
|
|
146
|
+
const builtin = document.getElementById('builtinPlugins');
|
|
147
|
+
const user = document.getElementById('userPlugins');
|
|
148
|
+
const config = window.spiralConfig || {};
|
|
149
|
+
|
|
150
|
+
builtin.innerHTML = '';
|
|
151
|
+
user.innerHTML = '';
|
|
152
|
+
|
|
153
|
+
plugins.forEach(p => {
|
|
154
|
+
const isEnabled = !config.enabledPlugins || config.enabledPlugins.includes(p.name);
|
|
155
|
+
const card = document.createElement('div');
|
|
156
|
+
card.className = 'plugin-card';
|
|
157
|
+
card.onclick = () => selectPlugin(p);
|
|
158
|
+
card.innerHTML = `
|
|
159
|
+
<div class="name">${p.name}</div>
|
|
160
|
+
<div class="desc">${p.description}</div>
|
|
161
|
+
<div class="meta">
|
|
162
|
+
<span class="badge badge-${p.builtin ? 'builtin' : 'user'}">${p.builtin ? 'builtin' : 'user'}</span>
|
|
163
|
+
<span class="badge badge-${isEnabled ? 'enabled' : 'disabled'}">${isEnabled ? 'enabled' : 'disabled'}</span>
|
|
164
|
+
<span>v${p.version}</span>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
if (p.builtin) {
|
|
169
|
+
builtin.appendChild(card);
|
|
170
|
+
} else {
|
|
171
|
+
user.appendChild(card);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function selectPlugin(plugin) {
|
|
177
|
+
selectedPlugin = plugin;
|
|
178
|
+
document.getElementById('emptyState').style.display = 'none';
|
|
179
|
+
document.getElementById('pluginDetail').style.display = 'block';
|
|
180
|
+
|
|
181
|
+
document.querySelectorAll('.plugin-card').forEach(c => c.classList.remove('active'));
|
|
182
|
+
event.currentTarget.classList.add('active');
|
|
183
|
+
|
|
184
|
+
document.getElementById('pluginName').textContent = plugin.name;
|
|
185
|
+
document.getElementById('pluginDesc').textContent = plugin.description;
|
|
186
|
+
|
|
187
|
+
const config = window.spiralConfig || {};
|
|
188
|
+
const isEnabled = !config.enabledPlugins || config.enabledPlugins.includes(plugin.name);
|
|
189
|
+
const btn = document.getElementById('enableBtn');
|
|
190
|
+
btn.textContent = isEnabled ? 'Disable' : 'Enable';
|
|
191
|
+
btn.className = `btn ${isEnabled ? 'btn-danger' : 'btn-success'}`;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(`/api/plugins/${plugin.name}`);
|
|
195
|
+
const data = await response.json();
|
|
196
|
+
if (data.success) {
|
|
197
|
+
configSchema = data.configSchema;
|
|
198
|
+
pluginConfig = data.config || {};
|
|
199
|
+
renderCommands(data.manifest.commands || {});
|
|
200
|
+
renderConfig();
|
|
201
|
+
renderInfo(data.manifest);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Failed to load plugin details:', error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
showTab('commands');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderCommands(commands) {
|
|
211
|
+
const list = document.getElementById('commandsList');
|
|
212
|
+
list.innerHTML = '';
|
|
213
|
+
|
|
214
|
+
Object.entries(commands).forEach(([name, cmd]) => {
|
|
215
|
+
const item = document.createElement('div');
|
|
216
|
+
item.className = 'cmd-item';
|
|
217
|
+
item.innerHTML = `
|
|
218
|
+
<span class="cmd-name">!${name}</span>
|
|
219
|
+
<span class="cmd-desc">${cmd.description}</span>
|
|
220
|
+
`;
|
|
221
|
+
list.appendChild(item);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderConfig() {
|
|
226
|
+
const form = document.getElementById('configForm');
|
|
227
|
+
form.innerHTML = '';
|
|
228
|
+
|
|
229
|
+
if (!configSchema || !configSchema.properties) {
|
|
230
|
+
form.innerHTML = '<p style="color: #8b949e;">No configuration options available</p>';
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Object.entries(configSchema.properties).forEach(([key, prop]) => {
|
|
235
|
+
const item = document.createElement('div');
|
|
236
|
+
item.className = 'config-item';
|
|
237
|
+
const value = pluginConfig[key] !== undefined ? pluginConfig[key] : prop.default;
|
|
238
|
+
item.innerHTML = `
|
|
239
|
+
<div>
|
|
240
|
+
<div class="config-key">${key}</div>
|
|
241
|
+
<div style="font-size: 11px; color: #8b949e;">${prop.description || ''}</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="config-value">
|
|
244
|
+
<input type="${prop.type === 'number' ? 'number' : 'text'}"
|
|
245
|
+
id="config_${key}"
|
|
246
|
+
value="${value !== undefined ? value : ''}"
|
|
247
|
+
data-key="${key}">
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
form.appendChild(item);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderInfo(manifest) {
|
|
255
|
+
const info = document.getElementById('pluginInfo');
|
|
256
|
+
info.innerHTML = `
|
|
257
|
+
<div class="config-item"><span class="config-key">Name</span><span>${manifest.name}</span></div>
|
|
258
|
+
<div class="config-item"><span class="config-key">Version</span><span>${manifest.version}</span></div>
|
|
259
|
+
<div class="config-item"><span class="config-key">Description</span><span>${manifest.description}</span></div>
|
|
260
|
+
<div class="config-item"><span class="config-key">Commands</span><span>${Object.keys(manifest.commands || {}).length}</span></div>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function showTab(tab) {
|
|
265
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
266
|
+
event.currentTarget?.classList?.add('active');
|
|
267
|
+
|
|
268
|
+
document.getElementById('commandsTab').style.display = tab === 'commands' ? 'block' : 'none';
|
|
269
|
+
document.getElementById('configTab').style.display = tab === 'config' ? 'block' : 'none';
|
|
270
|
+
document.getElementById('infoTab').style.display = tab === 'info' ? 'block' : 'none';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function savePluginConfig() {
|
|
274
|
+
if (!selectedPlugin) return;
|
|
275
|
+
|
|
276
|
+
const inputs = document.querySelectorAll('#configForm input');
|
|
277
|
+
const config = {};
|
|
278
|
+
inputs.forEach(input => {
|
|
279
|
+
const key = input.dataset.key;
|
|
280
|
+
const value = input.type === 'number' ? Number(input.value) : input.value;
|
|
281
|
+
config[key] = value;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await fetch(`/api/plugins/${selectedPlugin.name}/config`, {
|
|
286
|
+
method: 'PUT',
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
body: JSON.stringify({ config })
|
|
289
|
+
});
|
|
290
|
+
alert('Config saved!');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
alert('Failed to save config');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function togglePlugin() {
|
|
297
|
+
if (!selectedPlugin) return;
|
|
298
|
+
|
|
299
|
+
const configPath = '/api/config';
|
|
300
|
+
const response = await fetch(configPath);
|
|
301
|
+
const data = await response.json();
|
|
302
|
+
const config = data.config || {};
|
|
303
|
+
|
|
304
|
+
if (!config.enabledPlugins) config.enabledPlugins = plugins.map(p => p.name);
|
|
305
|
+
|
|
306
|
+
if (config.enabledPlugins.includes(selectedPlugin.name)) {
|
|
307
|
+
config.enabledPlugins = config.enabledPlugins.filter(p => p !== selectedPlugin.name);
|
|
308
|
+
} else {
|
|
309
|
+
config.enabledPlugins.push(selectedPlugin.name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
window.spiralConfig = config;
|
|
313
|
+
|
|
314
|
+
await fetch(configPath, {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: { 'Content-Type': 'application/json' },
|
|
317
|
+
body: JSON.stringify({ config })
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
renderPluginLists();
|
|
321
|
+
selectPlugin(selectedPlugin);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function updateStatus() {
|
|
325
|
+
try {
|
|
326
|
+
const response = await fetch('/api/status');
|
|
327
|
+
const data = await response.json();
|
|
328
|
+
const dot = document.getElementById('statusDot');
|
|
329
|
+
const text = document.getElementById('statusText');
|
|
330
|
+
if (data.running) {
|
|
331
|
+
dot.classList.add('online');
|
|
332
|
+
text.textContent = 'Running';
|
|
333
|
+
} else {
|
|
334
|
+
dot.classList.remove('online');
|
|
335
|
+
text.textContent = 'Stopped';
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Failed to update status:', error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function loadConfig() {
|
|
343
|
+
try {
|
|
344
|
+
const response = await fetch('/api/config');
|
|
345
|
+
const data = await response.json();
|
|
346
|
+
window.spiralConfig = data.config || {};
|
|
347
|
+
renderPluginLists();
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Failed to load config:', error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
loadConfig();
|
|
354
|
+
init();
|
|
355
|
+
setInterval(updateStatus, 5000);
|
|
356
|
+
</script>
|
|
357
|
+
</body>
|
|
358
|
+
</html>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
|
|
6
|
+
class SpiralWeb {
|
|
7
|
+
constructor(runtime) {
|
|
8
|
+
this.runtime = runtime;
|
|
9
|
+
this.app = express();
|
|
10
|
+
this.server = http.createServer(this.app);
|
|
11
|
+
this.port = process.env.PORT || 3000;
|
|
12
|
+
|
|
13
|
+
this.app.use(express.json());
|
|
14
|
+
this.app.use(express.static(path.join(__dirname, 'public')));
|
|
15
|
+
|
|
16
|
+
this.setupRoutes();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setupRoutes() {
|
|
20
|
+
this.app.get('/api/config', (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
23
|
+
if (fs.existsSync(configPath)) {
|
|
24
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
25
|
+
res.json({ success: true, config });
|
|
26
|
+
} else {
|
|
27
|
+
res.json({ success: true, config: {} });
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
res.status(500).json({ success: false, error: error.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.app.post('/api/config', (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const { config } = req.body;
|
|
37
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
38
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
39
|
+
res.json({ success: true });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
res.status(500).json({ success: false, error: error.message });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.app.get('/api/plugins', (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
48
|
+
const builtinDir = path.join(__dirname, '..', 'plugins', 'builtin');
|
|
49
|
+
|
|
50
|
+
const plugins = [];
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(builtinDir)) {
|
|
53
|
+
const folders = fs.readdirSync(builtinDir);
|
|
54
|
+
for (const folder of folders) {
|
|
55
|
+
const manifestPath = path.join(builtinDir, folder, 'plugin.json');
|
|
56
|
+
if (fs.existsSync(manifestPath)) {
|
|
57
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
58
|
+
plugins.push({ ...manifest, builtin: true, source: 'builtin' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (fs.existsSync(pluginsDir)) {
|
|
64
|
+
const folders = fs.readdirSync(pluginsDir);
|
|
65
|
+
for (const folder of folders) {
|
|
66
|
+
const manifestPath = path.join(pluginsDir, folder, 'plugin.json');
|
|
67
|
+
if (fs.existsSync(manifestPath)) {
|
|
68
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
69
|
+
plugins.push({ ...manifest, builtin: false, source: 'user' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.json({ success: true, plugins });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
res.status(500).json({ success: false, error: error.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.app.get('/api/plugins/:name', (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const name = req.params.name;
|
|
83
|
+
let pluginPath = path.join(__dirname, '..', 'plugins', 'builtin', name);
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(pluginPath)) {
|
|
86
|
+
pluginPath = path.join(process.cwd(), 'plugins', name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(pluginPath)) {
|
|
90
|
+
return res.status(404).json({ success: false, error: 'Plugin not found' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pluginPath, 'plugin.json'), 'utf-8'));
|
|
94
|
+
|
|
95
|
+
let configSchema = null;
|
|
96
|
+
const schemaPath = path.join(pluginPath, 'config.schema.json');
|
|
97
|
+
if (fs.existsSync(schemaPath)) {
|
|
98
|
+
configSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let config = null;
|
|
102
|
+
const configPath = path.join(pluginPath, 'config.json');
|
|
103
|
+
if (fs.existsSync(configPath)) {
|
|
104
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
res.json({ success: true, manifest, configSchema, config });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
res.status(500).json({ success: false, error: error.message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.app.put('/api/plugins/:name/config', (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const name = req.params.name;
|
|
116
|
+
const { config } = req.body;
|
|
117
|
+
|
|
118
|
+
let pluginPath = path.join(__dirname, '..', 'plugins', 'builtin', name);
|
|
119
|
+
if (!fs.existsSync(pluginPath)) {
|
|
120
|
+
pluginPath = path.join(process.cwd(), 'plugins', name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(pluginPath)) {
|
|
124
|
+
return res.status(404).json({ success: false, error: 'Plugin not found' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const configPath = path.join(pluginPath, 'config.json');
|
|
128
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
129
|
+
res.json({ success: true });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
res.status(500).json({ success: false, error: error.message });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.app.get('/api/status', (req, res) => {
|
|
136
|
+
res.json({
|
|
137
|
+
success: true,
|
|
138
|
+
running: this.runtime?.client?.isReady() || false,
|
|
139
|
+
uptime: this.runtime?.client?.uptime || 0
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.app.get('/api/plugins/:name/api', (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const name = req.params.name;
|
|
146
|
+
const api = this.runtime?.pluginManager?.getPluginAPI(name);
|
|
147
|
+
if (api) {
|
|
148
|
+
res.json({ success: true, api: Object.keys(api) });
|
|
149
|
+
} else {
|
|
150
|
+
res.json({ success: true, api: [] });
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
res.status(500).json({ success: false, error: error.message });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.app.get('/api/hooks', (req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const hooks = {};
|
|
160
|
+
if (this.runtime?.pluginManager) {
|
|
161
|
+
const eventNames = this.runtime.pluginManager.eventNames();
|
|
162
|
+
for (const name of eventNames) {
|
|
163
|
+
hooks[name] = this.runtime.pluginManager.listenerCount(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
res.json({ success: true, hooks });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
res.status(500).json({ success: false, error: error.message });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
start() {
|
|
174
|
+
this.server.listen(this.port, () => {
|
|
175
|
+
console.log(`Spiralcord Web Manager running at http://localhost:${this.port}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stop() {
|
|
180
|
+
this.server.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = SpiralWeb;
|