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
package/src/test.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const fsSync = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class SpiralTester {
|
|
6
|
+
constructor(pluginsDir) {
|
|
7
|
+
this.pluginsDir = pluginsDir;
|
|
8
|
+
this.errors = [];
|
|
9
|
+
this.warnings = [];
|
|
10
|
+
this.results = [];
|
|
11
|
+
this.allCommands = new Map();
|
|
12
|
+
this.allHooks = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async test() {
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(this.pluginsDir);
|
|
18
|
+
} catch {
|
|
19
|
+
return {
|
|
20
|
+
passed: 0,
|
|
21
|
+
warnings: 0,
|
|
22
|
+
errors: 0,
|
|
23
|
+
results: [],
|
|
24
|
+
details: { warnings: [], errors: ['Plugins directory not found'] }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries = await fs.readdir(this.pluginsDir, { withFileTypes: true });
|
|
29
|
+
const pluginFolders = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
|
|
30
|
+
|
|
31
|
+
for (const folder of pluginFolders) {
|
|
32
|
+
const pluginPath = path.join(this.pluginsDir, folder.name);
|
|
33
|
+
const result = await this.testPlugin(folder.name, pluginPath);
|
|
34
|
+
this.results.push(result);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.checkCommandConflicts();
|
|
38
|
+
this.checkHookConflicts();
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
passed: this.results.filter(r => r.status === 'ok').length,
|
|
42
|
+
warnings: this.warnings.length,
|
|
43
|
+
errors: this.errors.length,
|
|
44
|
+
results: this.results,
|
|
45
|
+
details: { warnings: this.warnings, errors: this.errors }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async testPlugin(name, pluginPath) {
|
|
50
|
+
const result = { name, status: 'ok', commands: 0, hooks: 0, issues: [] };
|
|
51
|
+
|
|
52
|
+
const manifestPath = path.join(pluginPath, 'plugin.json');
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(manifestPath);
|
|
55
|
+
} catch {
|
|
56
|
+
result.status = 'error';
|
|
57
|
+
result.issues.push('Missing plugin.json');
|
|
58
|
+
this.errors.push(`${name}: Missing plugin.json`);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let manifest;
|
|
63
|
+
try {
|
|
64
|
+
const data = await fs.readFile(manifestPath, 'utf-8');
|
|
65
|
+
manifest = JSON.parse(data);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
result.status = 'error';
|
|
68
|
+
result.issues.push(`Invalid plugin.json: ${e.message}`);
|
|
69
|
+
this.errors.push(`${name}: Invalid plugin.json - ${e.message}`);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!manifest.name) {
|
|
74
|
+
result.issues.push('Missing "name" in plugin.json');
|
|
75
|
+
this.warnings.push(`${name}: Missing "name" in manifest`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!manifest.version) {
|
|
79
|
+
result.issues.push('Missing "version" in plugin.json');
|
|
80
|
+
this.warnings.push(`${name}: Missing "version" in manifest`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const indexPath = path.join(pluginPath, 'index.js');
|
|
84
|
+
const dslPath = path.join(pluginPath, 'plugin.dsl');
|
|
85
|
+
|
|
86
|
+
const hasIndex = await this.fileExists(indexPath);
|
|
87
|
+
const hasDsl = await this.fileExists(dslPath);
|
|
88
|
+
|
|
89
|
+
if (!hasIndex && !hasDsl) {
|
|
90
|
+
result.status = 'error';
|
|
91
|
+
result.issues.push('No index.js or plugin.dsl found');
|
|
92
|
+
this.errors.push(`${name}: No index.js or plugin.dsl found`);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasIndex) {
|
|
97
|
+
const jsResult = await this.testJSPlugin(name, indexPath);
|
|
98
|
+
result.commands += jsResult.commands;
|
|
99
|
+
result.hooks += jsResult.hooks;
|
|
100
|
+
if (jsResult.issues.length > 0) {
|
|
101
|
+
result.issues.push(...jsResult.issues);
|
|
102
|
+
if (jsResult.status === 'error') result.status = 'error';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (hasDsl) {
|
|
107
|
+
const dslResult = await this.testDSLPlugin(name, dslPath);
|
|
108
|
+
result.commands += dslResult.commands;
|
|
109
|
+
if (dslResult.issues.length > 0) {
|
|
110
|
+
result.issues.push(...dslResult.issues);
|
|
111
|
+
if (dslResult.status === 'error') result.status = 'error';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const configResult = await this.testConfig(name, pluginPath, manifest);
|
|
116
|
+
if (configResult.issues.length > 0) {
|
|
117
|
+
result.issues.push(...configResult.issues);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (result.issues.length > 0 && result.status === 'ok') {
|
|
121
|
+
result.status = 'warning';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async testJSPlugin(name, indexPath) {
|
|
128
|
+
const result = { commands: 0, hooks: 0, issues: [], status: 'ok' };
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const code = await fs.readFile(indexPath, 'utf-8');
|
|
132
|
+
|
|
133
|
+
const syntaxCheck = this.checkJSSyntax(code);
|
|
134
|
+
if (!syntaxCheck.valid) {
|
|
135
|
+
result.status = 'error';
|
|
136
|
+
result.issues.push(`JS syntax error: ${syntaxCheck.error}`);
|
|
137
|
+
this.errors.push(`${name}: JS syntax error - ${syntaxCheck.error}`);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const exportsMatch = code.match(/module\.exports\s*=\s*\{/);
|
|
142
|
+
if (!exportsMatch) {
|
|
143
|
+
result.issues.push('No module.exports found');
|
|
144
|
+
this.warnings.push(`${name}: No module.exports found`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const commandsMatch = code.match(/commands\s*:\s*\{/);
|
|
148
|
+
const hooksMatch = code.match(/hooks\s*:\s*\{/);
|
|
149
|
+
const apiMatch = code.match(/api\s*:\s*\{/);
|
|
150
|
+
|
|
151
|
+
if (!commandsMatch && !hooksMatch && !apiMatch) {
|
|
152
|
+
result.issues.push('No commands, hooks, or api exported');
|
|
153
|
+
this.warnings.push(`${name}: No commands, hooks, or api exported`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (commandsMatch) {
|
|
157
|
+
const cmdRegex = /(\w+)\s*:\s*\{/g;
|
|
158
|
+
let match;
|
|
159
|
+
while ((match = cmdRegex.exec(code)) !== null) {
|
|
160
|
+
if (match[1] !== 'commands' && match[1] !== 'hooks' && match[1] !== 'api' && match[1] !== 'init') {
|
|
161
|
+
result.commands++;
|
|
162
|
+
this.allCommands.set(match[1], name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hooksMatch) {
|
|
168
|
+
const hookRegex = /(\w+)\s*:\s*async\s*\(/g;
|
|
169
|
+
let match;
|
|
170
|
+
while ((match = hookRegex.exec(code)) !== null) {
|
|
171
|
+
if (['message_received', 'bot_ready', 'interaction_received', 'presence_update', 'guild_joined', 'guild_left', 'voice_state_update', 'reaction_add', 'reaction_remove', 'bot_shutdown'].includes(match[1])) {
|
|
172
|
+
result.hooks++;
|
|
173
|
+
if (!this.allHooks.has(match[1])) this.allHooks.set(match[1], []);
|
|
174
|
+
this.allHooks.get(match[1]).push(name);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const requireMatches = code.match(/require\(['"]([^'"]+)['"]\)/g);
|
|
180
|
+
if (requireMatches) {
|
|
181
|
+
for (const req of requireMatches) {
|
|
182
|
+
const module = req.match(/require\(['"]([^'"]+)['"]\)/)[1];
|
|
183
|
+
if (module.startsWith('.') || module.startsWith('/')) continue;
|
|
184
|
+
try {
|
|
185
|
+
require.resolve(module);
|
|
186
|
+
} catch {
|
|
187
|
+
result.issues.push(`Missing dependency: ${module}`);
|
|
188
|
+
this.warnings.push(`${name}: Missing dependency - ${module}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
} catch (e) {
|
|
194
|
+
result.status = 'error';
|
|
195
|
+
result.issues.push(`Failed to read: ${e.message}`);
|
|
196
|
+
this.errors.push(`${name}: Failed to read index.js - ${e.message}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async testDSLPlugin(name, dslPath) {
|
|
203
|
+
const result = { commands: 0, issues: [], status: 'ok' };
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const dsl = await fs.readFile(dslPath, 'utf-8');
|
|
207
|
+
const lines = dsl.split('\n');
|
|
208
|
+
|
|
209
|
+
let inCommand = false;
|
|
210
|
+
let inResponsePool = false;
|
|
211
|
+
let commandName = '';
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i].trim();
|
|
215
|
+
|
|
216
|
+
if (line === '' || line.startsWith('#')) continue;
|
|
217
|
+
|
|
218
|
+
if (line.startsWith('COMMAND ')) {
|
|
219
|
+
const match = line.match(/COMMAND\s+(\S+)\s+"([^"]+)"/);
|
|
220
|
+
if (!match) {
|
|
221
|
+
result.status = 'error';
|
|
222
|
+
result.issues.push(`Line ${i + 1}: Invalid COMMAND syntax`);
|
|
223
|
+
this.errors.push(`${name}: Line ${i + 1} - Invalid COMMAND syntax`);
|
|
224
|
+
} else {
|
|
225
|
+
inCommand = true;
|
|
226
|
+
commandName = match[1];
|
|
227
|
+
result.commands++;
|
|
228
|
+
this.allCommands.set(commandName, name);
|
|
229
|
+
}
|
|
230
|
+
} else if (line === 'END') {
|
|
231
|
+
if (inResponsePool) {
|
|
232
|
+
inResponsePool = false;
|
|
233
|
+
} else {
|
|
234
|
+
inCommand = false;
|
|
235
|
+
commandName = '';
|
|
236
|
+
}
|
|
237
|
+
} else if (inCommand) {
|
|
238
|
+
if (line.startsWith('RESPONSE_POOL')) {
|
|
239
|
+
inResponsePool = true;
|
|
240
|
+
} else if (!inResponsePool) {
|
|
241
|
+
if (!line.startsWith('ALIASES') && !line.startsWith('COOLDOWN') &&
|
|
242
|
+
!line.startsWith('REQUIRES') && !line.startsWith('RESPONSE ') &&
|
|
243
|
+
!line.startsWith('EMBED') && !line.startsWith('TITLE') &&
|
|
244
|
+
!line.startsWith('DESCRIPTION')) {
|
|
245
|
+
result.issues.push(`Line ${i + 1}: Unknown DSL keyword: ${line.split(' ')[0]}`);
|
|
246
|
+
this.warnings.push(`${name}: Line ${i + 1} - Unknown DSL keyword`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (inCommand) {
|
|
253
|
+
result.status = 'error';
|
|
254
|
+
result.issues.push('Unclosed COMMAND block (missing END)');
|
|
255
|
+
this.errors.push(`${name}: Unclosed COMMAND block`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
} catch (e) {
|
|
259
|
+
result.status = 'error';
|
|
260
|
+
result.issues.push(`Failed to read: ${e.message}`);
|
|
261
|
+
this.errors.push(`${name}: Failed to read plugin.dsl - ${e.message}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async testConfig(name, pluginPath, manifest) {
|
|
268
|
+
const result = { issues: [] };
|
|
269
|
+
|
|
270
|
+
const configSchemaPath = path.join(pluginPath, 'config.schema.json');
|
|
271
|
+
const configPath = path.join(pluginPath, 'config.json');
|
|
272
|
+
|
|
273
|
+
const hasSchema = await this.fileExists(configSchemaPath);
|
|
274
|
+
const hasConfig = await this.fileExists(configPath);
|
|
275
|
+
|
|
276
|
+
if (hasSchema) {
|
|
277
|
+
try {
|
|
278
|
+
const schemaData = await fs.readFile(configSchemaPath, 'utf-8');
|
|
279
|
+
JSON.parse(schemaData);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
result.issues.push(`Invalid config.schema.json: ${e.message}`);
|
|
282
|
+
this.warnings.push(`${name}: Invalid config.schema.json`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (hasConfig) {
|
|
287
|
+
try {
|
|
288
|
+
const configData = await fs.readFile(configPath, 'utf-8');
|
|
289
|
+
const config = JSON.parse(configData);
|
|
290
|
+
|
|
291
|
+
if (hasSchema) {
|
|
292
|
+
try {
|
|
293
|
+
const schemaData = await fs.readFile(configSchemaPath, 'utf-8');
|
|
294
|
+
const schema = JSON.parse(schemaData);
|
|
295
|
+
|
|
296
|
+
if (schema.properties) {
|
|
297
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
298
|
+
if (prop.type === 'string' && config[key] !== undefined && typeof config[key] !== 'string') {
|
|
299
|
+
result.issues.push(`Config "${key}" should be string, got ${typeof config[key]}`);
|
|
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
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
result.issues.push(`Invalid config.json: ${e.message}`);
|
|
316
|
+
this.warnings.push(`${name}: Invalid config.json`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
checkCommandConflicts() {
|
|
324
|
+
const seen = new Map();
|
|
325
|
+
for (const [cmd, plugin] of this.allCommands) {
|
|
326
|
+
if (seen.has(cmd)) {
|
|
327
|
+
this.warnings.push(`Command conflict: "${cmd}" in ${seen.get(cmd)} and ${plugin}`);
|
|
328
|
+
} else {
|
|
329
|
+
seen.set(cmd, plugin);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
checkHookConflicts() {
|
|
335
|
+
for (const [hook, plugins] of this.allHooks) {
|
|
336
|
+
if (plugins.length > 1) {
|
|
337
|
+
this.warnings.push(`Hook "${hook}" used by ${plugins.length} plugins: ${plugins.join(', ')}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
checkJSSyntax(code) {
|
|
343
|
+
try {
|
|
344
|
+
new Function(code);
|
|
345
|
+
return { valid: true };
|
|
346
|
+
} catch (e) {
|
|
347
|
+
return { valid: false, error: e.message };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async fileExists(path) {
|
|
352
|
+
try {
|
|
353
|
+
await fs.access(path);
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = SpiralTester;
|