spiralcord-full 2.2.7 → 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 CHANGED
@@ -154,18 +154,364 @@ async function startBot(config) {
154
154
  program
155
155
  .command('run')
156
156
  .description('Start the bot runtime')
157
- .action(async () => {
157
+ .option('-R, --realtime', 'Start with auto-reload and REPL console')
158
+ .action(async (options) => {
158
159
  try {
159
160
  const config = await loadConfig();
160
161
  if (!config) return;
161
- const runtime = await startBot(config);
162
- console.log('Bot is running! Press Ctrl+C to stop.');
163
162
 
164
- process.on('SIGINT', async () => {
165
- console.log('\nShutting down...');
166
- await runtime.stop();
167
- process.exit(0);
168
- });
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
+ }
174
+
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
+ }
187
+
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
+ }
445
+
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
+ });
453
+ }
454
+
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
+ });
514
+ }
169
515
  } catch (error) {
170
516
  console.error('Failed to start bot:', error.message);
171
517
  process.exit(1);
@@ -176,7 +522,12 @@ program
176
522
  .command('dev')
177
523
  .description('Start bot with auto-reload on file changes')
178
524
  .action(async () => {
179
- const chokidar = (() => { try { return require('chokidar'); } catch { return null; } })();
525
+ const chokidar = (() => {
526
+ try {
527
+ const resolved = require.resolve('chokidar', { paths: [process.cwd()] });
528
+ return require(resolved);
529
+ } catch { return null; }
530
+ })();
180
531
  if (!chokidar) {
181
532
  console.error('Error: "spiral dev" requires chokidar. Run: npm install chokidar');
182
533
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spiralcord-full",
3
- "version": "2.2.7",
3
+ "version": "2.3.0",
4
4
  "description": "Spiralcord Full - Discord Bot Runtime with Voice, Music, AI, Database & Image Support",
5
5
  "main": "src/index.js",
6
6
  "types": "src/types/index.d.ts",
@@ -255,6 +255,21 @@ class SpiralPluginManager extends EventEmitter {
255
255
  return result;
256
256
  }
257
257
 
258
+ getReplCommands() {
259
+ const result = {};
260
+ for (const [pluginName, { module: mod }] of this.plugins) {
261
+ if (mod.repl) {
262
+ for (const [cmdName, cmd] of Object.entries(mod.repl)) {
263
+ result[cmdName] = {
264
+ ...cmd,
265
+ plugin: pluginName
266
+ };
267
+ }
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+
258
273
  async executeCommand(name, message, args) {
259
274
  const cmd = this.getCommand(name);
260
275
  if (!cmd) return false;
@@ -44,11 +44,26 @@ export interface SpiralHooks {
44
44
  [key: string]: ((payload: any, runtime: SpiralRuntime) => Promise<void>) | undefined;
45
45
  }
46
46
 
47
+ export interface ReplContext {
48
+ runtime: SpiralRuntime;
49
+ client: any;
50
+ db: SpiralDB;
51
+ manager: SpiralPluginManager;
52
+ config: SpiralConfig;
53
+ user: any;
54
+ }
55
+
56
+ export interface PluginReplCommand {
57
+ description: string;
58
+ execute: (args: string[], context: ReplContext) => Promise<string | void> | string | void;
59
+ }
60
+
47
61
  export interface SpiralPlugin {
48
62
  init?: (config: SpiralPluginConfig, runtime: SpiralRuntime) => Promise<void>;
49
63
  commands?: Record<string, SpiralCommand>;
50
64
  keywords?: Record<string, (message: any, args: string[], runtime: SpiralRuntime) => Promise<void>>;
51
65
  hooks?: SpiralHooks;
66
+ repl?: Record<string, PluginReplCommand>;
52
67
  api?: Record<string, (...args: any[]) => any>;
53
68
  }
54
69
 
@@ -66,6 +81,7 @@ export interface SpiralRuntime {
66
81
  config: SpiralConfig;
67
82
  client: any;
68
83
  db: SpiralDB;
84
+ pluginManager: SpiralPluginManager;
69
85
  startTime: number;
70
86
  start(): Promise<void>;
71
87
  restart(): Promise<void>;
@@ -80,6 +96,16 @@ export interface SpiralRuntime {
80
96
  emitHookWithResult(eventName: string, payload: any): Promise<boolean>;
81
97
  }
82
98
 
99
+ export interface SpiralPluginManager {
100
+ loadPlugins(pluginsDir: string): Promise<void>;
101
+ reloadPlugins(): Promise<void>;
102
+ getCommand(name: string): any;
103
+ getAllCommands(): Map<string, any>;
104
+ getReplCommands(): Record<string, PluginReplCommand & { plugin: string }>;
105
+ getSlashCommands(): Array<{ name: string; description: string }>;
106
+ executeCommand(name: string, message: any, args: string[]): Promise<void>;
107
+ }
108
+
83
109
  export interface SpiralTester {
84
110
  test(): Promise<{
85
111
  passed: number;