spiralcord-full 2.1.0 → 2.3.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 +621 -347
- package/package.json +4 -2
- package/src/plugins/manager.js +177 -34
- package/src/runtime.js +157 -22
- package/src/test.js +85 -63
- package/src/types/index.d.ts +126 -0
- package/src/web/server.js +69 -41
package/bin/spiral.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
6
|
+
const { execSync, execFileSync } = require('child_process');
|
|
7
7
|
|
|
8
8
|
const program = new Command();
|
|
9
|
+
const SPIRAL_ROOT = path.resolve(__dirname, '..');
|
|
9
10
|
|
|
10
11
|
program
|
|
11
12
|
.name('spiral')
|
|
12
13
|
.description('Spiralcord - Discord Bot Runtime & Maker')
|
|
13
|
-
.version('
|
|
14
|
+
.version(require('../package.json').version);
|
|
14
15
|
|
|
15
16
|
program
|
|
16
17
|
.command('install <repo>')
|
|
@@ -30,20 +31,33 @@ program
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
let repoUrl = repo;
|
|
33
|
-
if (!repo.includes('github.com') && !repo.startsWith('http')) {
|
|
34
|
+
if (!repo.includes('github.com') && !repo.startsWith('http') && !repo.startsWith('git@')) {
|
|
34
35
|
repoUrl = `https://github.com/${repo}`;
|
|
35
36
|
}
|
|
37
|
+
if (!repoUrl.endsWith('.git')) repoUrl += '.git';
|
|
36
38
|
|
|
37
39
|
console.log(`Installing from ${repoUrl}...`);
|
|
38
40
|
|
|
39
41
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
execFileSync('git', ['init'], { cwd, stdio: 'inherit' });
|
|
43
|
+
execFileSync('git', ['remote', 'add', 'origin', repoUrl], { cwd, stdio: 'inherit' });
|
|
44
|
+
|
|
45
|
+
let fetched = false;
|
|
46
|
+
for (const branch of ['main', 'master']) {
|
|
47
|
+
try {
|
|
48
|
+
execFileSync('git', ['fetch', 'origin', branch, '--depth', '1'], { cwd, stdio: 'inherit' });
|
|
49
|
+
execFileSync('git', ['checkout', '-b', branch, 'FETCH_HEAD'], { cwd, stdio: 'inherit' });
|
|
50
|
+
fetched = true;
|
|
51
|
+
break;
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fetched) {
|
|
56
|
+
throw new Error('Could not fetch main or master branch');
|
|
57
|
+
}
|
|
44
58
|
} catch (error) {
|
|
45
|
-
console.error('Failed to clone repository.
|
|
46
|
-
try {
|
|
59
|
+
console.error('Failed to clone repository:', error.message);
|
|
60
|
+
try { execFileSync('git', ['clean', '-fd'], { cwd, stdio: 'inherit' }); } catch {}
|
|
47
61
|
try { fs.rmSync(path.join(cwd, '.git'), { recursive: true }); } catch {}
|
|
48
62
|
return;
|
|
49
63
|
}
|
|
@@ -65,106 +79,531 @@ program
|
|
|
65
79
|
|
|
66
80
|
program
|
|
67
81
|
.command('update')
|
|
68
|
-
.description('Update
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
const
|
|
82
|
+
.description('Update spiralcord to latest version')
|
|
83
|
+
.action(async () => {
|
|
84
|
+
const pkgPath = path.join(SPIRAL_ROOT, 'package.json');
|
|
85
|
+
const currentVer = require(pkgPath).version;
|
|
86
|
+
console.log(`Current version: v${currentVer}`);
|
|
87
|
+
console.log('Updating spiralcord...');
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
try {
|
|
90
|
+
execSync('npm install -g spiralcord@latest', { stdio: 'inherit' });
|
|
91
|
+
delete require.cache[require.resolve(pkgPath)];
|
|
92
|
+
const newVer = require(pkgPath).version;
|
|
93
|
+
if (newVer !== currentVer) {
|
|
94
|
+
console.log(`\nUpdated: v${currentVer} → v${newVer}`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log('\nAlready up to date!');
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Failed to update:', error.message);
|
|
76
100
|
}
|
|
101
|
+
});
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
async function loadConfig() {
|
|
104
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(configPath)) {
|
|
107
|
+
console.log('spiral.json not found. Creating default config...');
|
|
108
|
+
const defaultConfig = {
|
|
109
|
+
name: 'My Spiral Bot',
|
|
110
|
+
token: '',
|
|
111
|
+
prefix: '!',
|
|
112
|
+
intents: ['Guilds', 'GuildMessages', 'MessageContent'],
|
|
113
|
+
clientId: '',
|
|
114
|
+
disabledCommands: []
|
|
115
|
+
};
|
|
116
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
117
|
+
console.log('Created spiral.json — edit it with your bot token, then run "spiral run".');
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
82
120
|
|
|
83
|
-
|
|
121
|
+
let config;
|
|
122
|
+
try {
|
|
123
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(`Error: spiral.json is invalid JSON: ${e.message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
84
128
|
|
|
85
|
-
|
|
129
|
+
if (!config || typeof config !== 'object') {
|
|
130
|
+
console.error('Error: spiral.json must be a JSON object');
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
86
133
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const configPath = path.join(pluginsDir, plugin, 'config.json');
|
|
92
|
-
if (fs.existsSync(configPath)) {
|
|
93
|
-
pluginConfigs[plugin] = fs.readFileSync(configPath, 'utf-8');
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
134
|
+
if (!config.token || config.token === '') {
|
|
135
|
+
console.error('Error: Set your bot token in spiral.json');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
97
138
|
|
|
98
|
-
|
|
139
|
+
if (!Array.isArray(config.intents)) {
|
|
140
|
+
console.warn('Warning: Missing intents in spiral.json, using defaults');
|
|
141
|
+
config.intents = ['Guilds', 'GuildMessages', 'MessageContent'];
|
|
142
|
+
}
|
|
99
143
|
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function startBot(config) {
|
|
148
|
+
const SpiralRuntime = require('../src/runtime');
|
|
149
|
+
const runtime = new SpiralRuntime(config);
|
|
150
|
+
await runtime.start();
|
|
151
|
+
return runtime;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
program
|
|
155
|
+
.command('run')
|
|
156
|
+
.description('Start the bot runtime')
|
|
157
|
+
.option('-R, --realtime', 'Start with auto-reload and REPL console')
|
|
158
|
+
.action(async (options) => {
|
|
100
159
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
160
|
+
const config = await loadConfig();
|
|
161
|
+
if (!config) return;
|
|
162
|
+
|
|
163
|
+
if (options.realtime) {
|
|
164
|
+
const chokidar = (() => {
|
|
165
|
+
try {
|
|
166
|
+
const resolved = require.resolve('chokidar', { paths: [process.cwd()] });
|
|
167
|
+
return require(resolved);
|
|
168
|
+
} catch { return null; }
|
|
169
|
+
})();
|
|
170
|
+
if (!chokidar) {
|
|
171
|
+
console.error('Error: "spiral run -R" requires chokidar. Run: npm install chokidar');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
107
174
|
|
|
108
|
-
|
|
175
|
+
let runtime = null;
|
|
176
|
+
let restarting = false;
|
|
177
|
+
let pendingRestart = false;
|
|
178
|
+
let r = null;
|
|
179
|
+
|
|
180
|
+
async function start() {
|
|
181
|
+
const cfg = await loadConfig();
|
|
182
|
+
if (!cfg) return;
|
|
183
|
+
runtime = await startBot(cfg);
|
|
184
|
+
console.log('[runtime] Ready. Press Ctrl+C to stop.');
|
|
185
|
+
startRepl();
|
|
186
|
+
}
|
|
109
187
|
|
|
110
|
-
|
|
188
|
+
function startRepl() {
|
|
189
|
+
if (r) r.close();
|
|
190
|
+
const repl = require('repl');
|
|
191
|
+
r = repl.start({
|
|
192
|
+
prompt: 'spiral> ',
|
|
193
|
+
useGlobal: false,
|
|
194
|
+
terminal: true
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
r.context.runtime = runtime;
|
|
198
|
+
r.context.client = runtime.client;
|
|
199
|
+
r.context.db = runtime.db;
|
|
200
|
+
r.context.manager = runtime.pluginManager;
|
|
201
|
+
r.context.config = runtime.config;
|
|
202
|
+
|
|
203
|
+
const spmCmds = (() => {
|
|
204
|
+
try {
|
|
205
|
+
return require('../spm/lib/commands');
|
|
206
|
+
} catch { return null; }
|
|
207
|
+
})();
|
|
208
|
+
|
|
209
|
+
const pluginCmds = runtime.pluginManager.getReplCommands();
|
|
210
|
+
const cmdCount = Object.keys(pluginCmds).length;
|
|
211
|
+
if (cmdCount > 0) {
|
|
212
|
+
console.log(`[repl] ${cmdCount} plugin command(s) registered: ${Object.keys(pluginCmds).map(c => `.${c}`).join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
r.defineCommand('status', {
|
|
216
|
+
help: 'Show uptime, servers, users',
|
|
217
|
+
action: function () {
|
|
218
|
+
this.clearBufferedCommand();
|
|
219
|
+
const uptime = process.uptime();
|
|
220
|
+
const h = Math.floor(uptime / 3600);
|
|
221
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
222
|
+
const s = Math.floor(uptime % 60);
|
|
223
|
+
const servers = runtime.client.guilds.cache.size;
|
|
224
|
+
const users = runtime.client.guilds.cache.reduce((acc, g) => acc + g.memberCount, 0);
|
|
225
|
+
console.log(`Uptime: ${h}h ${m}m ${s}s | Servers: ${servers} | Users: ${users}`);
|
|
226
|
+
this.displayPrompt();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
r.defineCommand('reload', {
|
|
231
|
+
help: 'Hot-reload all plugins',
|
|
232
|
+
action: function () {
|
|
233
|
+
this.clearBufferedCommand();
|
|
234
|
+
(async () => {
|
|
235
|
+
try {
|
|
236
|
+
await runtime.stop();
|
|
237
|
+
clearSpiralCache();
|
|
238
|
+
await start();
|
|
239
|
+
console.log('Plugins reloaded.');
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('Reload failed:', err.message);
|
|
242
|
+
}
|
|
243
|
+
r.displayPrompt();
|
|
244
|
+
})();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
r.defineCommand('stop', {
|
|
249
|
+
help: 'Stop the bot',
|
|
250
|
+
action: function () {
|
|
251
|
+
this.clearBufferedCommand();
|
|
252
|
+
(async () => {
|
|
253
|
+
console.log('Shutting down...');
|
|
254
|
+
if (runtime) await runtime.stop();
|
|
255
|
+
process.exit(0);
|
|
256
|
+
})();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
r.defineCommand('commands', {
|
|
261
|
+
help: 'List all registered commands',
|
|
262
|
+
action: function () {
|
|
263
|
+
this.clearBufferedCommand();
|
|
264
|
+
const cmds = runtime.pluginManager.getAllCommands();
|
|
265
|
+
if (cmds.size === 0) {
|
|
266
|
+
console.log('No commands registered.');
|
|
267
|
+
} else {
|
|
268
|
+
for (const [name, cmd] of cmds) {
|
|
269
|
+
console.log(` ${cmd.slash ? '/' : '.'}${name} — ${cmd.description || ''}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
r.displayPrompt();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
r.defineCommand('plugins', {
|
|
277
|
+
help: 'List installed plugins',
|
|
278
|
+
action: function () {
|
|
279
|
+
this.clearBufferedCommand();
|
|
280
|
+
(async () => {
|
|
281
|
+
const result = await spmCmds.listPlugins();
|
|
282
|
+
if (result.message) process.stdout.write(result.message);
|
|
283
|
+
r.displayPrompt();
|
|
284
|
+
})();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
r.defineCommand('install', {
|
|
289
|
+
help: 'Install a plugin from GitHub: spm.install <user/repo>',
|
|
290
|
+
action: function (source) {
|
|
291
|
+
this.clearBufferedCommand();
|
|
292
|
+
(async () => {
|
|
293
|
+
if (!source.trim()) {
|
|
294
|
+
console.log('Usage: spm.install <user/repo>');
|
|
295
|
+
} else {
|
|
296
|
+
const result = await spmCmds.installPlugin(source.trim());
|
|
297
|
+
if (result.message) process.stdout.write(result.message);
|
|
298
|
+
}
|
|
299
|
+
r.displayPrompt();
|
|
300
|
+
})();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
r.defineCommand('create', {
|
|
305
|
+
help: 'Create a new plugin: spm.create <name> [js|dsl|voice|ai]',
|
|
306
|
+
action: function (input) {
|
|
307
|
+
this.clearBufferedCommand();
|
|
308
|
+
(async () => {
|
|
309
|
+
const parts = input.trim().split(/\s+/);
|
|
310
|
+
const name = parts[0];
|
|
311
|
+
const template = parts[1] || 'js';
|
|
312
|
+
if (!name) {
|
|
313
|
+
console.log('Usage: spm.create <name> [js|dsl|voice|ai]');
|
|
314
|
+
} else {
|
|
315
|
+
const result = await spmCmds.createPlugin(name, template);
|
|
316
|
+
if (result.message) process.stdout.write(result.message);
|
|
317
|
+
}
|
|
318
|
+
r.displayPrompt();
|
|
319
|
+
})();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
r.defineCommand('enable', {
|
|
324
|
+
help: 'Enable a plugin: spm.enable <name>',
|
|
325
|
+
action: function (name) {
|
|
326
|
+
this.clearBufferedCommand();
|
|
327
|
+
(async () => {
|
|
328
|
+
if (!name.trim()) {
|
|
329
|
+
console.log('Usage: spm.enable <name>');
|
|
330
|
+
} else {
|
|
331
|
+
const result = await spmCmds.enablePlugin(name.trim());
|
|
332
|
+
if (result.message) process.stdout.write(result.message);
|
|
333
|
+
}
|
|
334
|
+
r.displayPrompt();
|
|
335
|
+
})();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
r.defineCommand('disable', {
|
|
340
|
+
help: 'Disable a plugin: spm.disable <name>',
|
|
341
|
+
action: function (name) {
|
|
342
|
+
this.clearBufferedCommand();
|
|
343
|
+
(async () => {
|
|
344
|
+
if (!name.trim()) {
|
|
345
|
+
console.log('Usage: spm.disable <name>');
|
|
346
|
+
} else {
|
|
347
|
+
const result = await spmCmds.disablePlugin(name.trim());
|
|
348
|
+
if (result.message) process.stdout.write(result.message);
|
|
349
|
+
}
|
|
350
|
+
r.displayPrompt();
|
|
351
|
+
})();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
r.defineCommand('remove', {
|
|
356
|
+
help: 'Remove a plugin: spm.remove <name>',
|
|
357
|
+
action: function (name) {
|
|
358
|
+
this.clearBufferedCommand();
|
|
359
|
+
(async () => {
|
|
360
|
+
if (!name.trim()) {
|
|
361
|
+
console.log('Usage: spm.remove <name>');
|
|
362
|
+
} else {
|
|
363
|
+
const result = await spmCmds.removePlugin(name.trim());
|
|
364
|
+
if (result.message) process.stdout.write(result.message);
|
|
365
|
+
}
|
|
366
|
+
r.displayPrompt();
|
|
367
|
+
})();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
r.defineCommand('test', {
|
|
372
|
+
help: 'Test a plugin: spm.test <name>',
|
|
373
|
+
action: function (name) {
|
|
374
|
+
this.clearBufferedCommand();
|
|
375
|
+
(async () => {
|
|
376
|
+
if (!name.trim()) {
|
|
377
|
+
console.log('Usage: spm.test <name>');
|
|
378
|
+
} else {
|
|
379
|
+
const result = await spmCmds.testPlugin(name.trim());
|
|
380
|
+
if (result.message) process.stdout.write(result.message);
|
|
381
|
+
}
|
|
382
|
+
r.displayPrompt();
|
|
383
|
+
})();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
r.defineCommand('info', {
|
|
388
|
+
help: 'Show plugin info: spm.info <name>',
|
|
389
|
+
action: function (name) {
|
|
390
|
+
this.clearBufferedCommand();
|
|
391
|
+
(async () => {
|
|
392
|
+
if (!name.trim()) {
|
|
393
|
+
console.log('Usage: spm.info <name>');
|
|
394
|
+
} else {
|
|
395
|
+
const result = await spmCmds.infoPlugin(name.trim());
|
|
396
|
+
if (result.message) process.stdout.write(result.message);
|
|
397
|
+
}
|
|
398
|
+
r.displayPrompt();
|
|
399
|
+
})();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
r.defineCommand('update', {
|
|
404
|
+
help: 'Update all plugins',
|
|
405
|
+
action: function () {
|
|
406
|
+
this.clearBufferedCommand();
|
|
407
|
+
(async () => {
|
|
408
|
+
const result = await spmCmds.updatePlugins();
|
|
409
|
+
if (result.message) process.stdout.write(result.message);
|
|
410
|
+
r.displayPrompt();
|
|
411
|
+
})();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
for (const [cmdName, cmd] of Object.entries(pluginCmds)) {
|
|
416
|
+
r.defineCommand(cmdName, {
|
|
417
|
+
help: cmd.description || `Plugin command (${cmd.plugin})`,
|
|
418
|
+
action: function (args) {
|
|
419
|
+
this.clearBufferedCommand();
|
|
420
|
+
(async () => {
|
|
421
|
+
try {
|
|
422
|
+
const ctx = {
|
|
423
|
+
runtime,
|
|
424
|
+
client: runtime.client,
|
|
425
|
+
db: runtime.db,
|
|
426
|
+
manager: runtime.pluginManager,
|
|
427
|
+
config: runtime.config,
|
|
428
|
+
user: runtime.client.user
|
|
429
|
+
};
|
|
430
|
+
const result = await cmd.execute(args.split(/\s+/), ctx);
|
|
431
|
+
if (result) console.log(result);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error(`[${cmdName}] Error:`, err.message);
|
|
434
|
+
}
|
|
435
|
+
r.displayPrompt();
|
|
436
|
+
})();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
r.on('exit', () => {
|
|
442
|
+
console.log('Bot continues running. Press Ctrl+C to stop.');
|
|
443
|
+
});
|
|
444
|
+
}
|
|
111
445
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
446
|
+
function clearSpiralCache() {
|
|
447
|
+
const spiralRoot = path.resolve(__dirname, '..');
|
|
448
|
+
Object.keys(require.cache).forEach(key => {
|
|
449
|
+
if (key.startsWith(spiralRoot + path.sep) && !key.includes('node_modules')) {
|
|
450
|
+
delete require.cache[key];
|
|
451
|
+
}
|
|
452
|
+
});
|
|
117
453
|
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
454
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
455
|
+
async function restart() {
|
|
456
|
+
if (restarting) {
|
|
457
|
+
pendingRestart = true;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
restarting = true;
|
|
461
|
+
console.log('\n[dev] File changed, restarting...');
|
|
462
|
+
try {
|
|
463
|
+
if (runtime) await runtime.stop();
|
|
464
|
+
} catch {}
|
|
465
|
+
clearSpiralCache();
|
|
466
|
+
try {
|
|
467
|
+
await start();
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('[dev] Restart failed:', error.message);
|
|
470
|
+
}
|
|
471
|
+
restarting = false;
|
|
472
|
+
if (pendingRestart) {
|
|
473
|
+
pendingRestart = false;
|
|
474
|
+
restart();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const watchDirs = [
|
|
479
|
+
path.join(process.cwd(), 'plugins'),
|
|
480
|
+
path.join(process.cwd(), 'src')
|
|
481
|
+
].filter(d => fs.existsSync(d));
|
|
482
|
+
|
|
483
|
+
const watcher = chokidar.watch(watchDirs, {
|
|
484
|
+
ignored: /node_modules|\.git|data\.json|spiral\.json/,
|
|
485
|
+
persistent: true,
|
|
486
|
+
ignoreInitial: true,
|
|
487
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
watcher.on('change', (filePath) => {
|
|
491
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.dsl') || filePath.endsWith('.json')) {
|
|
492
|
+
console.log(`[dev] Changed: ${path.relative(process.cwd(), filePath)}`);
|
|
493
|
+
restart().catch(() => {});
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await start();
|
|
498
|
+
|
|
499
|
+
process.on('SIGINT', async () => {
|
|
500
|
+
console.log('\nShutting down...');
|
|
501
|
+
watcher.close();
|
|
502
|
+
if (runtime) await runtime.stop();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
});
|
|
505
|
+
} else {
|
|
506
|
+
const runtime = await startBot(config);
|
|
507
|
+
console.log('Bot is running! Press Ctrl+C to stop.');
|
|
508
|
+
|
|
509
|
+
process.on('SIGINT', async () => {
|
|
510
|
+
console.log('\nShutting down...');
|
|
511
|
+
await runtime.stop();
|
|
512
|
+
process.exit(0);
|
|
513
|
+
});
|
|
127
514
|
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error('Failed to start bot:', error.message);
|
|
517
|
+
process.exit(1);
|
|
128
518
|
}
|
|
129
|
-
|
|
130
|
-
console.log('\nBot updated!');
|
|
131
|
-
console.log('Run: spiral run');
|
|
132
519
|
});
|
|
133
520
|
|
|
134
521
|
program
|
|
135
|
-
.command('
|
|
136
|
-
.description('Start
|
|
522
|
+
.command('dev')
|
|
523
|
+
.description('Start bot with auto-reload on file changes')
|
|
137
524
|
.action(async () => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
525
|
+
const chokidar = (() => {
|
|
526
|
+
try {
|
|
527
|
+
const resolved = require.resolve('chokidar', { paths: [process.cwd()] });
|
|
528
|
+
return require(resolved);
|
|
529
|
+
} catch { return null; }
|
|
530
|
+
})();
|
|
531
|
+
if (!chokidar) {
|
|
532
|
+
console.error('Error: "spiral dev" requires chokidar. Run: npm install chokidar');
|
|
142
533
|
process.exit(1);
|
|
143
534
|
}
|
|
144
535
|
|
|
145
|
-
|
|
536
|
+
let runtime = null;
|
|
537
|
+
let restarting = false;
|
|
538
|
+
let pendingRestart = false;
|
|
146
539
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
540
|
+
async function start() {
|
|
541
|
+
const config = await loadConfig();
|
|
542
|
+
if (!config) return;
|
|
543
|
+
runtime = await startBot(config);
|
|
544
|
+
console.log('Dev mode active — watching for changes. Press Ctrl+C to stop.');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function restart() {
|
|
548
|
+
if (restarting) {
|
|
549
|
+
pendingRestart = true;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
restarting = true;
|
|
553
|
+
console.log('\n[dev] File changed, restarting...');
|
|
554
|
+
try {
|
|
555
|
+
if (runtime) await runtime.stop();
|
|
556
|
+
} catch {}
|
|
557
|
+
const spiralRoot = path.resolve(__dirname, '..');
|
|
558
|
+
Object.keys(require.cache).forEach(key => {
|
|
559
|
+
if (key.startsWith(spiralRoot + path.sep) && !key.includes('node_modules')) {
|
|
560
|
+
delete require.cache[key];
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
try {
|
|
564
|
+
await start();
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.error('[dev] Restart failed:', error.message);
|
|
567
|
+
}
|
|
568
|
+
restarting = false;
|
|
569
|
+
if (pendingRestart) {
|
|
570
|
+
pendingRestart = false;
|
|
571
|
+
restart();
|
|
572
|
+
}
|
|
150
573
|
}
|
|
151
574
|
|
|
152
|
-
const
|
|
153
|
-
|
|
575
|
+
const watchDirs = [
|
|
576
|
+
path.join(process.cwd(), 'plugins'),
|
|
577
|
+
path.join(process.cwd(), 'src')
|
|
578
|
+
].filter(d => fs.existsSync(d));
|
|
154
579
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
580
|
+
const watcher = chokidar.watch(watchDirs, {
|
|
581
|
+
ignored: /node_modules|\.git|data\.json|spiral\.json/,
|
|
582
|
+
persistent: true,
|
|
583
|
+
ignoreInitial: true,
|
|
584
|
+
awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
|
|
585
|
+
});
|
|
158
586
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
587
|
+
watcher.on('change', (filePath) => {
|
|
588
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.dsl') || filePath.endsWith('.json')) {
|
|
589
|
+
console.log(`[dev] Changed: ${path.relative(process.cwd(), filePath)}`);
|
|
590
|
+
restart().catch(() => {});
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
await start();
|
|
164
596
|
} catch (error) {
|
|
165
597
|
console.error('Failed to start bot:', error.message);
|
|
166
598
|
process.exit(1);
|
|
167
599
|
}
|
|
600
|
+
|
|
601
|
+
process.on('SIGINT', async () => {
|
|
602
|
+
console.log('\nShutting down...');
|
|
603
|
+
watcher.close();
|
|
604
|
+
if (runtime) await runtime.stop();
|
|
605
|
+
process.exit(0);
|
|
606
|
+
});
|
|
168
607
|
});
|
|
169
608
|
|
|
170
609
|
program
|
|
@@ -172,16 +611,24 @@ program
|
|
|
172
611
|
.description('Start the web plugin manager')
|
|
173
612
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
174
613
|
.action(async (options) => {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
614
|
+
const port = parseInt(options.port, 10);
|
|
615
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
616
|
+
console.error('Error: Invalid port number');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
180
619
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
620
|
+
try {
|
|
621
|
+
const SpiralWeb = require('../src/web/server');
|
|
622
|
+
const web = new SpiralWeb(null);
|
|
623
|
+
process.env.PORT = String(port);
|
|
624
|
+
web.port = port;
|
|
625
|
+
web.start();
|
|
626
|
+
console.log(`\nSpiralcord Web Manager started!`);
|
|
627
|
+
console.log(`Open http://localhost:${port} in your browser`);
|
|
628
|
+
console.log(`Press Ctrl+C to stop`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error('Failed to start web manager:', error.message);
|
|
631
|
+
}
|
|
185
632
|
});
|
|
186
633
|
|
|
187
634
|
program
|
|
@@ -197,300 +644,127 @@ program
|
|
|
197
644
|
|
|
198
645
|
console.log('Testing plugins...\n');
|
|
199
646
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
647
|
+
try {
|
|
648
|
+
const SpiralTester = require('../src/test');
|
|
649
|
+
const tester = new SpiralTester(pluginsDir);
|
|
650
|
+
const result = await tester.test();
|
|
203
651
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
652
|
+
for (const r of result.results) {
|
|
653
|
+
const icon = r.status === 'ok' ? '\x1b[32m✓\x1b[0m' :
|
|
654
|
+
r.status === 'warning' ? '\x1b[33m⚠\x1b[0m' :
|
|
655
|
+
'\x1b[31m✗\x1b[0m';
|
|
208
656
|
|
|
209
|
-
|
|
210
|
-
|
|
657
|
+
const extra = r.commands > 0 ? ` (${r.commands} commands)` : '';
|
|
658
|
+
console.log(`${icon} ${r.name}${extra}`);
|
|
211
659
|
|
|
212
|
-
|
|
213
|
-
|
|
660
|
+
for (const issue of r.issues) {
|
|
661
|
+
console.log(` → ${issue}`);
|
|
662
|
+
}
|
|
214
663
|
}
|
|
215
|
-
}
|
|
216
664
|
|
|
217
|
-
|
|
665
|
+
console.log('\n' + '─'.repeat(40));
|
|
218
666
|
|
|
219
|
-
|
|
220
|
-
|
|
667
|
+
const passedColor = result.errors === 0 ? '\x1b[32m' : '\x1b[31m';
|
|
668
|
+
console.log(`${passedColor}Results: ${result.passed} passed, ${result.warnings} warnings, ${result.errors} errors\x1b[0m`);
|
|
221
669
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
670
|
+
if (result.details.errors.length > 0) {
|
|
671
|
+
console.log('\n\x1b[31mErrors:\x1b[0m');
|
|
672
|
+
for (const err of result.details.errors) {
|
|
673
|
+
console.log(` ✗ ${err}`);
|
|
674
|
+
}
|
|
226
675
|
}
|
|
227
|
-
}
|
|
228
676
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
677
|
+
if (result.details.warnings.length > 0) {
|
|
678
|
+
console.log('\n\x1b[33mWarnings:\x1b[0m');
|
|
679
|
+
for (const warn of result.details.warnings) {
|
|
680
|
+
console.log(` ⚠ ${warn}`);
|
|
681
|
+
}
|
|
233
682
|
}
|
|
234
|
-
}
|
|
235
683
|
|
|
236
|
-
|
|
237
|
-
|
|
684
|
+
if (result.errors > 0) {
|
|
685
|
+
console.log('\n\x1b[31mFix errors before running the bot.\x1b[0m');
|
|
686
|
+
process.exit(1);
|
|
687
|
+
} else {
|
|
688
|
+
console.log('\n\x1b[32mAll tests passed! Run "spiral run" to start.\x1b[0m');
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.error('Test failed:', error.message);
|
|
238
692
|
process.exit(1);
|
|
239
|
-
} else {
|
|
240
|
-
console.log('\n\x1b[32mAll tests passed! Run "spiral run" to start.\x1b[0m');
|
|
241
693
|
}
|
|
242
694
|
});
|
|
243
695
|
|
|
244
|
-
|
|
696
|
+
program
|
|
245
697
|
.command('plugin')
|
|
246
|
-
.description('Manage plugins')
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.description('List installed plugins')
|
|
251
|
-
.action(() => {
|
|
252
|
-
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
253
|
-
|
|
254
|
-
if (!fs.existsSync(pluginsDir)) {
|
|
255
|
-
console.log('No plugins directory found. Use "spiral install" to clone a bot.');
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const enabled = [];
|
|
260
|
-
const disabled = [];
|
|
261
|
-
|
|
262
|
-
const folders = fs.readdirSync(pluginsDir);
|
|
263
|
-
for (const f of folders) {
|
|
264
|
-
const isDisabled = f.startsWith('.') && (f.endsWith('.disabled') || !fs.existsSync(path.join(pluginsDir, f, 'plugin.json')));
|
|
265
|
-
const pluginDirName = f.endsWith('.disabled') ? f.slice(1, -9) : f;
|
|
266
|
-
const pluginPath = path.join(pluginsDir, pluginDirName, 'plugin.json');
|
|
267
|
-
const manifestPath = path.join(pluginsDir, f, 'plugin.json');
|
|
268
|
-
|
|
269
|
-
const actualManifestPath = fs.existsSync(manifestPath) ? manifestPath : pluginPath;
|
|
270
|
-
if (fs.existsSync(actualManifestPath)) {
|
|
271
|
-
const manifest = JSON.parse(fs.readFileSync(actualManifestPath, 'utf-8'));
|
|
272
|
-
if (isDisabled) {
|
|
273
|
-
disabled.push(manifest);
|
|
274
|
-
} else {
|
|
275
|
-
enabled.push(manifest);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (enabled.length === 0 && disabled.length === 0) {
|
|
281
|
-
console.log('No plugins installed.');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
console.log('Installed plugins:');
|
|
286
|
-
for (const m of enabled) {
|
|
287
|
-
console.log(` [enabled] ${m.name} v${m.version}: ${m.description}`);
|
|
288
|
-
}
|
|
289
|
-
for (const m of disabled) {
|
|
290
|
-
console.log(` [disabled] ${m.name} v${m.version}: ${m.description}`);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
pluginCmd
|
|
295
|
-
.command('install <source>')
|
|
296
|
-
.description('Install a plugin from GitHub or npm')
|
|
297
|
-
.action(async (source) => {
|
|
298
|
-
const cwd = process.cwd();
|
|
299
|
-
const pluginsDir = path.join(cwd, 'plugins');
|
|
300
|
-
|
|
301
|
-
if (!fs.existsSync(pluginsDir)) {
|
|
302
|
-
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
let repoUrl = source;
|
|
306
|
-
let pluginName = '';
|
|
307
|
-
|
|
308
|
-
if (source.includes('github.com')) {
|
|
309
|
-
repoUrl = source;
|
|
310
|
-
const match = source.match(/github\.com\/[^\/]+\/([^\/\.]+)/);
|
|
311
|
-
pluginName = match ? match[1] : '';
|
|
312
|
-
} else if (source.startsWith('http')) {
|
|
313
|
-
repoUrl = source;
|
|
314
|
-
const match = source.match(/\/([^\/\.]+?)(?:\.git)?$/);
|
|
315
|
-
pluginName = match ? match[1] : '';
|
|
316
|
-
} else {
|
|
317
|
-
pluginName = source;
|
|
318
|
-
repoUrl = `https://github.com/${source}`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!pluginName) {
|
|
322
|
-
console.error('Could not determine plugin name from source.');
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const pluginDir = path.join(pluginsDir, pluginName);
|
|
327
|
-
|
|
328
|
-
if (fs.existsSync(pluginDir)) {
|
|
329
|
-
console.error(`Plugin "${pluginName}" already exists. Remove it first.`);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
console.log(`Installing plugin "${pluginName}"...`);
|
|
334
|
-
|
|
698
|
+
.description('Manage plugins (delegates to spm)')
|
|
699
|
+
.argument('[args...]', 'spm arguments')
|
|
700
|
+
.action((args) => {
|
|
701
|
+
const spmPath = path.join(__dirname, '..', 'spm', 'bin', 'spm.js');
|
|
335
702
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const gitDir = path.join(pluginDir, '.git');
|
|
339
|
-
if (fs.existsSync(gitDir)) {
|
|
340
|
-
fs.rmSync(gitDir, { recursive: true });
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
344
|
-
if (!fs.existsSync(manifestPath)) {
|
|
345
|
-
console.error('Error: plugin.json not found in the cloned repository.');
|
|
346
|
-
fs.rmSync(pluginDir, { recursive: true });
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
351
|
-
console.log(`Installed ${manifest.name} v${manifest.version}`);
|
|
703
|
+
execFileSync('node', [spmPath, ...args], { stdio: 'inherit' });
|
|
352
704
|
} catch (error) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
fs.rmSync(pluginDir, { recursive: true });
|
|
705
|
+
if (error.status !== 0) {
|
|
706
|
+
console.error('spm exited with error');
|
|
356
707
|
}
|
|
357
708
|
}
|
|
358
709
|
});
|
|
359
710
|
|
|
360
|
-
|
|
361
|
-
.command('
|
|
362
|
-
.description('
|
|
363
|
-
.action((
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const disabledDir = path.join(pluginsDir, `.${name}.disabled`);
|
|
367
|
-
|
|
368
|
-
if (fs.existsSync(pluginDir)) {
|
|
369
|
-
console.log(`Plugin "${name}" is already enabled.`);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (fs.existsSync(disabledDir)) {
|
|
374
|
-
fs.renameSync(disabledDir, pluginDir);
|
|
375
|
-
console.log(`Plugin "${name}" enabled.`);
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
console.error(`Plugin "${name}" not found.`);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
pluginCmd
|
|
383
|
-
.command('disable <name>')
|
|
384
|
-
.description('Disable a plugin by renaming its folder')
|
|
385
|
-
.action((name) => {
|
|
386
|
-
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
387
|
-
const pluginDir = path.join(pluginsDir, name);
|
|
388
|
-
const disabledDir = path.join(pluginsDir, `.${name}.disabled`);
|
|
389
|
-
|
|
390
|
-
if (!fs.existsSync(pluginDir)) {
|
|
391
|
-
console.error(`Plugin "${name}" not found.`);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
fs.renameSync(pluginDir, disabledDir);
|
|
396
|
-
console.log(`Plugin "${name}" disabled.`);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
pluginCmd
|
|
400
|
-
.command('create <name>')
|
|
401
|
-
.description('Create a new plugin')
|
|
402
|
-
.action((name) => {
|
|
403
|
-
const cwd = process.cwd();
|
|
404
|
-
const pluginDir = path.join(cwd, 'plugins', name);
|
|
711
|
+
program
|
|
712
|
+
.command('doctor')
|
|
713
|
+
.description('Check bot health and configuration')
|
|
714
|
+
.action(async () => {
|
|
715
|
+
console.log('Spiralcord Doctor\n');
|
|
716
|
+
let issues = 0;
|
|
405
717
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
718
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
719
|
+
if (nodeMajor >= 16) {
|
|
720
|
+
console.log(`\x1b[32m✓\x1b[0m Node.js ${process.version}`);
|
|
721
|
+
} else {
|
|
722
|
+
console.log(`\x1b[31m✗\x1b[0m Node.js ${process.version} (need >=16)`);
|
|
723
|
+
issues++;
|
|
409
724
|
}
|
|
410
725
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
enabled: { type: 'boolean', default: true },
|
|
431
|
-
message: { type: 'string', default: `Hello from ${name}!` }
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
const code = `module.exports = {
|
|
436
|
-
hooks: {
|
|
437
|
-
message_received: async (payload, runtime) => {
|
|
438
|
-
const { message } = payload;
|
|
439
|
-
if (message.content.startsWith('!${name}')) {
|
|
440
|
-
const config = runtime.getPluginConfig('${name}');
|
|
441
|
-
await message.reply(config.message);
|
|
726
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
727
|
+
if (fs.existsSync(configPath)) {
|
|
728
|
+
console.log(`\x1b[32m✓\x1b[0m spiral.json found`);
|
|
729
|
+
try {
|
|
730
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
731
|
+
if (config.token && config.token !== '' && config.token !== 'YOUR_DISCORD_TOKEN_HERE') {
|
|
732
|
+
console.log(`\x1b[32m✓\x1b[0m Bot token configured`);
|
|
733
|
+
} else {
|
|
734
|
+
console.log(`\x1b[31m✗\x1b[0m Bot token not set`);
|
|
735
|
+
issues++;
|
|
736
|
+
}
|
|
737
|
+
if (config.intents && config.intents.length > 0) {
|
|
738
|
+
console.log(`\x1b[32m✓\x1b[0m Intents: ${config.intents.join(', ')}`);
|
|
739
|
+
} else {
|
|
740
|
+
console.log(`\x1b[33m⚠\x1b[0m No intents configured`);
|
|
741
|
+
}
|
|
742
|
+
} catch (e) {
|
|
743
|
+
console.log(`\x1b[31m✗\x1b[0m spiral.json invalid: ${e.message}`);
|
|
744
|
+
issues++;
|
|
442
745
|
}
|
|
746
|
+
} else {
|
|
747
|
+
console.log(`\x1b[31m✗\x1b[0m spiral.json not found`);
|
|
748
|
+
issues++;
|
|
443
749
|
}
|
|
444
|
-
}
|
|
445
|
-
};`;
|
|
446
|
-
|
|
447
|
-
fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
|
|
448
|
-
fs.writeFileSync(path.join(pluginDir, 'config.json'), JSON.stringify(config, null, 2));
|
|
449
|
-
fs.writeFileSync(path.join(pluginDir, 'config.schema.json'), JSON.stringify(schema, null, 2));
|
|
450
|
-
fs.writeFileSync(path.join(pluginDir, 'index.js'), code);
|
|
451
|
-
|
|
452
|
-
console.log(`Plugin "${name}" created at plugins/${name}/`);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
pluginCmd
|
|
456
|
-
.command('remove <name>')
|
|
457
|
-
.description('Remove a plugin')
|
|
458
|
-
.action((name) => {
|
|
459
|
-
const pluginsDir = path.join(process.cwd(), 'plugins', name);
|
|
460
|
-
const disabledDir = path.join(process.cwd(), 'plugins', `.${name}.disabled`);
|
|
461
750
|
|
|
751
|
+
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
462
752
|
if (fs.existsSync(pluginsDir)) {
|
|
463
|
-
fs.
|
|
464
|
-
console.log(
|
|
465
|
-
} else if (fs.existsSync(disabledDir)) {
|
|
466
|
-
fs.rmSync(disabledDir, { recursive: true });
|
|
467
|
-
console.log(`Plugin "${name}" removed.`);
|
|
753
|
+
const folders = fs.readdirSync(pluginsDir).filter(f => !f.startsWith('.'));
|
|
754
|
+
console.log(`\x1b[32m✓\x1b[0m ${folders.length} plugin(s) found`);
|
|
468
755
|
} else {
|
|
469
|
-
console.log(
|
|
756
|
+
console.log(`\x1b[33m⚠\x1b[0m No plugins directory`);
|
|
470
757
|
}
|
|
471
|
-
});
|
|
472
758
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
.description('Show plugin file location for editing')
|
|
476
|
-
.action((name) => {
|
|
477
|
-
const pluginsDir = path.join(process.cwd(), 'plugins', name);
|
|
759
|
+
const spiralVer = require(path.join(__dirname, '..', 'package.json')).version;
|
|
760
|
+
console.log(`\x1b[32m✓\x1b[0m Spiralcord v${spiralVer}`);
|
|
478
761
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
762
|
+
console.log('\n' + '─'.repeat(40));
|
|
763
|
+
if (issues === 0) {
|
|
764
|
+
console.log('\x1b[32mAll checks passed!\x1b[0m');
|
|
765
|
+
} else {
|
|
766
|
+
console.log(`\x1b[31m${issues} issue(s) found. Fix them before running the bot.\x1b[0m`);
|
|
482
767
|
}
|
|
483
|
-
|
|
484
|
-
console.log(`Plugin "${name}" files:`);
|
|
485
|
-
const files = fs.readdirSync(pluginsDir);
|
|
486
|
-
files.forEach(f => {
|
|
487
|
-
const fullPath = path.join(pluginsDir, f);
|
|
488
|
-
const stat = fs.statSync(fullPath);
|
|
489
|
-
if (stat.isFile()) {
|
|
490
|
-
console.log(` ${f}`);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
console.log(`\nDirectory: ${pluginsDir}`);
|
|
494
768
|
});
|
|
495
769
|
|
|
496
|
-
program.parse();
|
|
770
|
+
program.parse();
|