spiralcord-full 2.1.0 → 2.2.7
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 +273 -350
- package/package.json +4 -2
- package/src/plugins/manager.js +162 -34
- package/src/runtime.js +157 -22
- package/src/test.js +85 -63
- package/src/types/index.d.ts +100 -0
- package/src/web/server.js +69 -41
package/src/test.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const fs = require('fs').promises;
|
|
2
|
-
const fsSync = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
|
|
5
4
|
class SpiralTester {
|
|
@@ -147,31 +146,59 @@ class SpiralTester {
|
|
|
147
146
|
const commandsMatch = code.match(/commands\s*:\s*\{/);
|
|
148
147
|
const hooksMatch = code.match(/hooks\s*:\s*\{/);
|
|
149
148
|
const apiMatch = code.match(/api\s*:\s*\{/);
|
|
149
|
+
const keywordsMatch = code.match(/keywords\s*:\s*\{/);
|
|
150
|
+
const initMatch = code.match(/init\s*:/);
|
|
150
151
|
|
|
151
|
-
if (!commandsMatch && !hooksMatch && !apiMatch) {
|
|
152
|
-
result.issues.push('No commands, hooks, or
|
|
153
|
-
this.warnings.push(`${name}: No commands, hooks, or
|
|
152
|
+
if (!commandsMatch && !hooksMatch && !apiMatch && !keywordsMatch && !initMatch) {
|
|
153
|
+
result.issues.push('No commands, hooks, api, keywords, or init exported');
|
|
154
|
+
this.warnings.push(`${name}: No commands, hooks, api, keywords, or init exported`);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
if (commandsMatch) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
const cmdBlockStart = code.indexOf(commandsMatch[0]);
|
|
159
|
+
if (cmdBlockStart !== -1) {
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let blockContent = '';
|
|
162
|
+
let inBlock = false;
|
|
163
|
+
for (let i = cmdBlockStart; i < code.length; i++) {
|
|
164
|
+
if (code[i] === '{') { depth++; inBlock = true; }
|
|
165
|
+
if (inBlock) blockContent += code[i];
|
|
166
|
+
if (code[i] === '}') { depth--; if (depth === 0) break; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cmdRegex = /(\w+)\s*:\s*\{/g;
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = cmdRegex.exec(blockContent)) !== null) {
|
|
172
|
+
if (match[1] !== 'commands' && match[1] !== 'hooks' && match[1] !== 'api' && match[1] !== 'init') {
|
|
173
|
+
result.commands++;
|
|
174
|
+
if (!this.allCommands.has(match[1])) this.allCommands.set(match[1], []);
|
|
175
|
+
this.allCommands.get(match[1]).push(name);
|
|
176
|
+
}
|
|
163
177
|
}
|
|
164
178
|
}
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
if (hooksMatch) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
const hookBlockStart = code.indexOf(hooksMatch[0]);
|
|
183
|
+
if (hookBlockStart !== -1) {
|
|
184
|
+
let depth = 0;
|
|
185
|
+
let blockContent = '';
|
|
186
|
+
let inBlock = false;
|
|
187
|
+
for (let i = hookBlockStart; i < code.length; i++) {
|
|
188
|
+
if (code[i] === '{') { depth++; inBlock = true; }
|
|
189
|
+
if (inBlock) blockContent += code[i];
|
|
190
|
+
if (code[i] === '}') { depth--; if (depth === 0) break; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const knownHooks = ['message_received', 'bot_ready', 'interaction_received', 'presence_update', 'guild_joined', 'guild_left', 'voice_state_update', 'reaction_add', 'reaction_remove', 'bot_shutdown'];
|
|
194
|
+
const hookRegex = /(\w+)\s*:\s*async\s*(?:\(|function\s*\()/g;
|
|
195
|
+
let match;
|
|
196
|
+
while ((match = hookRegex.exec(blockContent)) !== null) {
|
|
197
|
+
if (knownHooks.includes(match[1])) {
|
|
198
|
+
result.hooks++;
|
|
199
|
+
if (!this.allHooks.has(match[1])) this.allHooks.set(match[1], []);
|
|
200
|
+
this.allHooks.get(match[1]).push(name);
|
|
201
|
+
}
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
204
|
}
|
|
@@ -179,13 +206,13 @@ class SpiralTester {
|
|
|
179
206
|
const requireMatches = code.match(/require\(['"]([^'"]+)['"]\)/g);
|
|
180
207
|
if (requireMatches) {
|
|
181
208
|
for (const req of requireMatches) {
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
209
|
+
const mod = req.match(/require\(['"]([^'"]+)['"]\)/)[1];
|
|
210
|
+
if (mod.startsWith('.') || mod.startsWith('/')) continue;
|
|
184
211
|
try {
|
|
185
|
-
require.resolve(
|
|
212
|
+
require.resolve(mod, { paths: [path.dirname(indexPath)] });
|
|
186
213
|
} catch {
|
|
187
|
-
result.issues.push(`Missing dependency: ${
|
|
188
|
-
this.warnings.push(`${name}: Missing dependency - ${
|
|
214
|
+
result.issues.push(`Missing dependency: ${mod}`);
|
|
215
|
+
this.warnings.push(`${name}: Missing dependency - ${mod}`);
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
}
|
|
@@ -215,17 +242,22 @@ class SpiralTester {
|
|
|
215
242
|
|
|
216
243
|
if (line === '' || line.startsWith('#')) continue;
|
|
217
244
|
|
|
218
|
-
if (line.startsWith('COMMAND ')) {
|
|
219
|
-
const
|
|
245
|
+
if (line.startsWith('COMMAND ') || line.startsWith('SLASH_COMMAND ')) {
|
|
246
|
+
const pattern = line.startsWith('SLASH_COMMAND ')
|
|
247
|
+
? /SLASH_COMMAND\s+(\S+)\s+"([^"]+)"/
|
|
248
|
+
: /COMMAND\s+(\S+)\s+"([^"]+)"/;
|
|
249
|
+
const match = line.match(pattern);
|
|
220
250
|
if (!match) {
|
|
221
251
|
result.status = 'error';
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
const keyword = line.startsWith('SLASH_COMMAND') ? 'SLASH_COMMAND' : 'COMMAND';
|
|
253
|
+
result.issues.push(`Line ${i + 1}: Invalid ${keyword} syntax. Use: ${keyword} name "description"`);
|
|
254
|
+
this.errors.push(`${name}: Line ${i + 1} - Invalid ${keyword} syntax`);
|
|
224
255
|
} else {
|
|
225
256
|
inCommand = true;
|
|
226
257
|
commandName = match[1];
|
|
227
258
|
result.commands++;
|
|
228
|
-
this.allCommands.set(commandName,
|
|
259
|
+
if (!this.allCommands.has(commandName)) this.allCommands.set(commandName, []);
|
|
260
|
+
this.allCommands.get(commandName).push(name);
|
|
229
261
|
}
|
|
230
262
|
} else if (line === 'END') {
|
|
231
263
|
if (inResponsePool) {
|
|
@@ -238,11 +270,10 @@ class SpiralTester {
|
|
|
238
270
|
if (line.startsWith('RESPONSE_POOL')) {
|
|
239
271
|
inResponsePool = true;
|
|
240
272
|
} else if (!inResponsePool) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
result.issues.push(`Line ${i + 1}: Unknown DSL keyword: ${line.split(' ')[0]}`);
|
|
273
|
+
const validKeywords = ['ALIASES', 'COOLDOWN', 'REQUIRES_ARGS', 'REQUIRES_PERMISSION', 'RESPONSE', 'EMBED', 'TITLE', 'DESCRIPTION', 'BUTTON', 'SELECT', 'DB_SET', 'DB_GET', 'DB_ADD', 'DB_DELETE', 'DB_RESPONSE'];
|
|
274
|
+
const firstWord = line.split(' ')[0];
|
|
275
|
+
if (!validKeywords.includes(firstWord)) {
|
|
276
|
+
result.issues.push(`Line ${i + 1}: Unknown DSL keyword: ${firstWord}`);
|
|
246
277
|
this.warnings.push(`${name}: Line ${i + 1} - Unknown DSL keyword`);
|
|
247
278
|
}
|
|
248
279
|
}
|
|
@@ -273,10 +304,11 @@ class SpiralTester {
|
|
|
273
304
|
const hasSchema = await this.fileExists(configSchemaPath);
|
|
274
305
|
const hasConfig = await this.fileExists(configPath);
|
|
275
306
|
|
|
307
|
+
let schema = null;
|
|
276
308
|
if (hasSchema) {
|
|
277
309
|
try {
|
|
278
310
|
const schemaData = await fs.readFile(configSchemaPath, 'utf-8');
|
|
279
|
-
JSON.parse(schemaData);
|
|
311
|
+
schema = JSON.parse(schemaData);
|
|
280
312
|
} catch (e) {
|
|
281
313
|
result.issues.push(`Invalid config.schema.json: ${e.message}`);
|
|
282
314
|
this.warnings.push(`${name}: Invalid config.schema.json`);
|
|
@@ -288,28 +320,21 @@ class SpiralTester {
|
|
|
288
320
|
const configData = await fs.readFile(configPath, 'utf-8');
|
|
289
321
|
const config = JSON.parse(configData);
|
|
290
322
|
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
301
|
-
}
|
|
302
|
-
if (prop.type === 'number' && config[key] !== undefined && typeof config[key] !== 'number') {
|
|
303
|
-
result.issues.push(`Config "${key}" should be number, got ${typeof config[key]}`);
|
|
304
|
-
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
305
|
-
}
|
|
306
|
-
if (prop.type === 'boolean' && config[key] !== undefined && typeof config[key] !== 'boolean') {
|
|
307
|
-
result.issues.push(`Config "${key}" should be boolean, got ${typeof config[key]}`);
|
|
308
|
-
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
323
|
+
if (schema && schema.properties) {
|
|
324
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
325
|
+
if (prop.type === 'string' && config[key] !== undefined && typeof config[key] !== 'string') {
|
|
326
|
+
result.issues.push(`Config "${key}" should be string, got ${typeof config[key]}`);
|
|
327
|
+
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
328
|
+
}
|
|
329
|
+
if (prop.type === 'number' && config[key] !== undefined && typeof config[key] !== 'number') {
|
|
330
|
+
result.issues.push(`Config "${key}" should be number, got ${typeof config[key]}`);
|
|
331
|
+
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
311
332
|
}
|
|
312
|
-
|
|
333
|
+
if (prop.type === 'boolean' && config[key] !== undefined && typeof config[key] !== 'boolean') {
|
|
334
|
+
result.issues.push(`Config "${key}" should be boolean, got ${typeof config[key]}`);
|
|
335
|
+
this.warnings.push(`${name}: Config "${key}" type mismatch`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
313
338
|
}
|
|
314
339
|
} catch (e) {
|
|
315
340
|
result.issues.push(`Invalid config.json: ${e.message}`);
|
|
@@ -321,12 +346,9 @@ class SpiralTester {
|
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
checkCommandConflicts() {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.warnings.push(`Command conflict: "${cmd}" in ${seen.get(cmd)} and ${plugin}`);
|
|
328
|
-
} else {
|
|
329
|
-
seen.set(cmd, plugin);
|
|
349
|
+
for (const [cmd, plugins] of this.allCommands) {
|
|
350
|
+
if (plugins.length > 1) {
|
|
351
|
+
this.warnings.push(`Command conflict: "${cmd}" in ${plugins.join(', ')}`);
|
|
330
352
|
}
|
|
331
353
|
}
|
|
332
354
|
}
|
|
@@ -348,9 +370,9 @@ class SpiralTester {
|
|
|
348
370
|
}
|
|
349
371
|
}
|
|
350
372
|
|
|
351
|
-
async fileExists(
|
|
373
|
+
async fileExists(filePath) {
|
|
352
374
|
try {
|
|
353
|
-
await fs.access(
|
|
375
|
+
await fs.access(filePath);
|
|
354
376
|
return true;
|
|
355
377
|
} catch {
|
|
356
378
|
return false;
|
|
@@ -358,4 +380,4 @@ class SpiralTester {
|
|
|
358
380
|
}
|
|
359
381
|
}
|
|
360
382
|
|
|
361
|
-
module.exports = SpiralTester;
|
|
383
|
+
module.exports = SpiralTester;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface SpiralConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
token: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
intents: string[];
|
|
6
|
+
clientId: string;
|
|
7
|
+
disabledCommands: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SpiralPluginManifest {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description: string;
|
|
14
|
+
author?: string;
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
collision_policy?: 'error' | 'first-wins' | 'last-wins';
|
|
17
|
+
builtin?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpiralPluginConfig {
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SpiralCommand {
|
|
25
|
+
description: string;
|
|
26
|
+
usage?: string;
|
|
27
|
+
aliases?: string[];
|
|
28
|
+
slash?: boolean;
|
|
29
|
+
priority?: number;
|
|
30
|
+
execute: (message: any, args: string[], runtime: SpiralRuntime) => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SpiralHooks {
|
|
34
|
+
bot_ready?: (payload: { client: any; user: any; config: SpiralConfig }, runtime: SpiralRuntime) => Promise<void>;
|
|
35
|
+
message_received?: (payload: { message: any; user: any; guild: any; channel: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
36
|
+
interaction_received?: (payload: { interaction: any; user: any; guild: any; channel: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
37
|
+
presence_update?: (payload: { oldPresence: any; newPresence: any; user: any; guild: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
38
|
+
voice_state_update?: (payload: { oldState: any; newState: any; user: any; guild: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
39
|
+
reaction_add?: (payload: { reaction: any; user: any; message: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
40
|
+
reaction_remove?: (payload: { reaction: any; user: any; message: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
41
|
+
guild_joined?: (payload: { guild: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
42
|
+
guild_left?: (payload: { guild: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
43
|
+
bot_shutdown?: (payload: { client: any }, runtime: SpiralRuntime) => Promise<void>;
|
|
44
|
+
[key: string]: ((payload: any, runtime: SpiralRuntime) => Promise<void>) | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SpiralPlugin {
|
|
48
|
+
init?: (config: SpiralPluginConfig, runtime: SpiralRuntime) => Promise<void>;
|
|
49
|
+
commands?: Record<string, SpiralCommand>;
|
|
50
|
+
keywords?: Record<string, (message: any, args: string[], runtime: SpiralRuntime) => Promise<void>>;
|
|
51
|
+
hooks?: SpiralHooks;
|
|
52
|
+
api?: Record<string, (...args: any[]) => any>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SpiralDB {
|
|
56
|
+
get(key: string, defaultValue?: any): any;
|
|
57
|
+
set(key: string, value: any): any;
|
|
58
|
+
add(key: string, amount: number): number;
|
|
59
|
+
delete(key: string): boolean;
|
|
60
|
+
has(key: string): boolean;
|
|
61
|
+
all(): Record<string, any>;
|
|
62
|
+
clear(): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SpiralRuntime {
|
|
66
|
+
config: SpiralConfig;
|
|
67
|
+
client: any;
|
|
68
|
+
db: SpiralDB;
|
|
69
|
+
startTime: number;
|
|
70
|
+
start(): Promise<void>;
|
|
71
|
+
restart(): Promise<void>;
|
|
72
|
+
stop(): Promise<void>;
|
|
73
|
+
getUptime(): number;
|
|
74
|
+
getCommand(name: string): any;
|
|
75
|
+
getAllCommands(): Map<string, any>;
|
|
76
|
+
getPluginConfig(pluginName: string): SpiralPluginConfig;
|
|
77
|
+
getPluginAPI(pluginName: string): Record<string, (...args: any[]) => any> | null;
|
|
78
|
+
emitHook(eventName: string, payload: any): Promise<void>;
|
|
79
|
+
emitHookSerial(eventName: string, payload: any): Promise<void>;
|
|
80
|
+
emitHookWithResult(eventName: string, payload: any): Promise<boolean>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SpiralTester {
|
|
84
|
+
test(): Promise<{
|
|
85
|
+
passed: number;
|
|
86
|
+
warnings: number;
|
|
87
|
+
errors: number;
|
|
88
|
+
results: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
status: 'ok' | 'warning' | 'error';
|
|
91
|
+
commands: number;
|
|
92
|
+
hooks: number;
|
|
93
|
+
issues: string[];
|
|
94
|
+
}>;
|
|
95
|
+
details: {
|
|
96
|
+
warnings: string[];
|
|
97
|
+
errors: string[];
|
|
98
|
+
};
|
|
99
|
+
}>;
|
|
100
|
+
}
|
package/src/web/server.js
CHANGED
|
@@ -10,12 +10,33 @@ class SpiralWeb {
|
|
|
10
10
|
this.server = http.createServer(this.app);
|
|
11
11
|
this.port = process.env.PORT || 3000;
|
|
12
12
|
|
|
13
|
-
this.app.use(express.json());
|
|
13
|
+
this.app.use(express.json({ limit: '1mb' }));
|
|
14
|
+
|
|
15
|
+
this.app.use((req, res, next) => {
|
|
16
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
17
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
18
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
19
|
+
next();
|
|
20
|
+
});
|
|
21
|
+
|
|
14
22
|
this.app.use(express.static(path.join(__dirname, 'public')));
|
|
15
23
|
|
|
16
24
|
this.setupRoutes();
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
_sanitizeName(name) {
|
|
28
|
+
if (!name || typeof name !== 'string') return null;
|
|
29
|
+
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
30
|
+
if (sanitized !== name || sanitized.length === 0 || sanitized.length > 100) return null;
|
|
31
|
+
if (sanitized.includes('..') || sanitized.startsWith('.')) return null;
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_sanitizeError(error) {
|
|
36
|
+
const msg = error.message || String(error);
|
|
37
|
+
return msg.replace(/\/[^\s:]+/g, '[path]');
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
setupRoutes() {
|
|
20
41
|
this.app.get('/api/config', (req, res) => {
|
|
21
42
|
try {
|
|
@@ -27,65 +48,57 @@ class SpiralWeb {
|
|
|
27
48
|
res.json({ success: true, config: {} });
|
|
28
49
|
}
|
|
29
50
|
} catch (error) {
|
|
30
|
-
res.status(500).json({ success: false, error: error
|
|
51
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
31
52
|
}
|
|
32
53
|
});
|
|
33
54
|
|
|
34
55
|
this.app.post('/api/config', (req, res) => {
|
|
35
56
|
try {
|
|
36
57
|
const { config } = req.body;
|
|
58
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
59
|
+
return res.status(400).json({ success: false, error: 'Config must be a JSON object' });
|
|
60
|
+
}
|
|
37
61
|
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
38
62
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
39
63
|
res.json({ success: true });
|
|
40
64
|
} catch (error) {
|
|
41
|
-
res.status(500).json({ success: false, error: error
|
|
65
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
42
66
|
}
|
|
43
67
|
});
|
|
44
68
|
|
|
45
69
|
this.app.get('/api/plugins', (req, res) => {
|
|
46
70
|
try {
|
|
47
71
|
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
48
|
-
const builtinDir = path.join(__dirname, '..', 'plugins', 'builtin');
|
|
49
|
-
|
|
50
72
|
const plugins = [];
|
|
51
73
|
|
|
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
74
|
if (fs.existsSync(pluginsDir)) {
|
|
64
|
-
const
|
|
75
|
+
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
76
|
+
const folders = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
|
|
65
77
|
for (const folder of folders) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
try {
|
|
79
|
+
const manifestPath = path.join(pluginsDir, folder.name, 'plugin.json');
|
|
80
|
+
if (fs.existsSync(manifestPath)) {
|
|
81
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
82
|
+
plugins.push({ ...manifest, builtin: false, source: 'user' });
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
res.json({ success: true, plugins });
|
|
75
89
|
} catch (error) {
|
|
76
|
-
res.status(500).json({ success: false, error: error
|
|
90
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
77
91
|
}
|
|
78
92
|
});
|
|
79
93
|
|
|
80
94
|
this.app.get('/api/plugins/:name', (req, res) => {
|
|
81
95
|
try {
|
|
82
|
-
const name = req.params.name;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!fs.existsSync(pluginPath)) {
|
|
86
|
-
pluginPath = path.join(process.cwd(), 'plugins', name);
|
|
96
|
+
const name = this._sanitizeName(req.params.name);
|
|
97
|
+
if (!name) {
|
|
98
|
+
return res.status(400).json({ success: false, error: 'Invalid plugin name' });
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
const pluginPath = path.join(process.cwd(), 'plugins', name);
|
|
89
102
|
if (!fs.existsSync(pluginPath)) {
|
|
90
103
|
return res.status(404).json({ success: false, error: 'Plugin not found' });
|
|
91
104
|
}
|
|
@@ -106,20 +119,23 @@ class SpiralWeb {
|
|
|
106
119
|
|
|
107
120
|
res.json({ success: true, manifest, configSchema, config });
|
|
108
121
|
} catch (error) {
|
|
109
|
-
res.status(500).json({ success: false, error: error
|
|
122
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
110
123
|
}
|
|
111
124
|
});
|
|
112
125
|
|
|
113
126
|
this.app.put('/api/plugins/:name/config', (req, res) => {
|
|
114
127
|
try {
|
|
115
|
-
const name = req.params.name;
|
|
116
|
-
|
|
128
|
+
const name = this._sanitizeName(req.params.name);
|
|
129
|
+
if (!name) {
|
|
130
|
+
return res.status(400).json({ success: false, error: 'Invalid plugin name' });
|
|
131
|
+
}
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
if (!
|
|
120
|
-
|
|
133
|
+
const { config } = req.body;
|
|
134
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
135
|
+
return res.status(400).json({ success: false, error: 'Config must be a JSON object' });
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
const pluginPath = path.join(process.cwd(), 'plugins', name);
|
|
123
139
|
if (!fs.existsSync(pluginPath)) {
|
|
124
140
|
return res.status(404).json({ success: false, error: 'Plugin not found' });
|
|
125
141
|
}
|
|
@@ -128,7 +144,7 @@ class SpiralWeb {
|
|
|
128
144
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
129
145
|
res.json({ success: true });
|
|
130
146
|
} catch (error) {
|
|
131
|
-
res.status(500).json({ success: false, error: error
|
|
147
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
132
148
|
}
|
|
133
149
|
});
|
|
134
150
|
|
|
@@ -142,7 +158,11 @@ class SpiralWeb {
|
|
|
142
158
|
|
|
143
159
|
this.app.get('/api/plugins/:name/api', (req, res) => {
|
|
144
160
|
try {
|
|
145
|
-
const name = req.params.name;
|
|
161
|
+
const name = this._sanitizeName(req.params.name);
|
|
162
|
+
if (!name) {
|
|
163
|
+
return res.status(400).json({ success: false, error: 'Invalid plugin name' });
|
|
164
|
+
}
|
|
165
|
+
|
|
146
166
|
const api = this.runtime?.pluginManager?.getPluginAPI(name);
|
|
147
167
|
if (api) {
|
|
148
168
|
res.json({ success: true, api: Object.keys(api) });
|
|
@@ -150,7 +170,7 @@ class SpiralWeb {
|
|
|
150
170
|
res.json({ success: true, api: [] });
|
|
151
171
|
}
|
|
152
172
|
} catch (error) {
|
|
153
|
-
res.status(500).json({ success: false, error: error
|
|
173
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
154
174
|
}
|
|
155
175
|
});
|
|
156
176
|
|
|
@@ -165,20 +185,28 @@ class SpiralWeb {
|
|
|
165
185
|
}
|
|
166
186
|
res.json({ success: true, hooks });
|
|
167
187
|
} catch (error) {
|
|
168
|
-
res.status(500).json({ success: false, error: error
|
|
188
|
+
res.status(500).json({ success: false, error: this._sanitizeError(error) });
|
|
169
189
|
}
|
|
170
190
|
});
|
|
171
191
|
}
|
|
172
192
|
|
|
173
193
|
start() {
|
|
194
|
+
this.server.on('error', (error) => {
|
|
195
|
+
if (error.code === 'EADDRINUSE') {
|
|
196
|
+
console.error(`Port ${this.port} is already in use`);
|
|
197
|
+
} else {
|
|
198
|
+
console.error('Server error:', error.message);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
174
202
|
this.server.listen(this.port, () => {
|
|
175
203
|
console.log(`Spiralcord Web Manager running at http://localhost:${this.port}`);
|
|
176
204
|
});
|
|
177
205
|
}
|
|
178
206
|
|
|
179
|
-
stop() {
|
|
180
|
-
this.server.close();
|
|
207
|
+
stop(callback) {
|
|
208
|
+
this.server.close(callback);
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
211
|
|
|
184
|
-
module.exports = SpiralWeb;
|
|
212
|
+
module.exports = SpiralWeb;
|