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 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('2.0.0');
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
- execSync('git init', { cwd, stdio: 'inherit' });
41
- execSync(`git remote add origin ${repoUrl}`, { cwd, stdio: 'inherit' });
42
- execSync('git fetch origin main --depth 1', { cwd, stdio: 'inherit' });
43
- execSync('git checkout -b main FETCH_HEAD', { cwd, stdio: 'inherit' });
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 { execSync('git clean -fd', { cwd, stdio: 'inherit' }); } catch {}
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 bot from GitHub (preserves token and configs)')
69
- .option('--no-install', 'Skip npm install')
70
- .action(async (options) => {
71
- const cwd = process.cwd();
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
- if (!fs.existsSync(path.join(cwd, 'spiral.json'))) {
74
- console.error('Error: spiral.json not found. Run "spiral install" first.');
75
- return;
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
- if (!fs.existsSync(path.join(cwd, '.git'))) {
79
- console.error('Error: Not a git repo. Run "spiral install" first.');
80
- return;
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
- console.log('Saving configs...');
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
- const spiralConfig = fs.readFileSync(path.join(cwd, 'spiral.json'), 'utf-8');
129
+ if (!config || typeof config !== 'object') {
130
+ console.error('Error: spiral.json must be a JSON object');
131
+ return null;
132
+ }
86
133
 
87
- const pluginConfigs = {};
88
- const pluginsDir = path.join(cwd, 'plugins');
89
- if (fs.existsSync(pluginsDir)) {
90
- for (const plugin of fs.readdirSync(pluginsDir)) {
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
- console.log('Pulling updates...');
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
- execSync('git fetch origin main', { cwd, stdio: 'inherit' });
102
- execSync('git reset --hard origin/main', { cwd, stdio: 'inherit' });
103
- } catch (error) {
104
- console.error('Failed to pull updates.');
105
- return;
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
- console.log('Restoring configs...');
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
- fs.writeFileSync(path.join(cwd, 'spiral.json'), spiralConfig);
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
- if (fs.existsSync(pluginsDir)) {
113
- for (const [plugin, config] of Object.entries(pluginConfigs)) {
114
- const configPath = path.join(pluginsDir, plugin, 'config.json');
115
- if (fs.existsSync(path.join(pluginsDir, plugin))) {
116
- fs.writeFileSync(configPath, config);
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
- if (options.install !== false) {
122
- console.log('\nInstalling dependencies...');
123
- try {
124
- execSync('npm install', { cwd, stdio: 'inherit' });
125
- } catch (error) {
126
- console.error('Failed to install dependencies. Run "npm install" manually.');
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('run')
136
- .description('Start the bot runtime')
522
+ .command('dev')
523
+ .description('Start bot with auto-reload on file changes')
137
524
  .action(async () => {
138
- const configPath = path.join(process.cwd(), 'spiral.json');
139
-
140
- if (!fs.existsSync(configPath)) {
141
- console.error('Error: spiral.json not found. Use "spiral install" to clone a bot.');
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
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
536
+ let runtime = null;
537
+ let restarting = false;
538
+ let pendingRestart = false;
146
539
 
147
- if (!config.token || config.token === 'YOUR_DISCORD_TOKEN_HERE') {
148
- console.error('Error: Please set your bot token in spiral.json');
149
- process.exit(1);
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 SpiralRuntime = require('../src/runtime');
153
- const runtime = new SpiralRuntime(config);
575
+ const watchDirs = [
576
+ path.join(process.cwd(), 'plugins'),
577
+ path.join(process.cwd(), 'src')
578
+ ].filter(d => fs.existsSync(d));
154
579
 
155
- try {
156
- await runtime.start();
157
- console.log('Bot is running! Press Ctrl+C to stop.');
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
- process.on('SIGINT', async () => {
160
- console.log('\nShutting down...');
161
- await runtime.stop();
162
- process.exit(0);
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 SpiralWeb = require('../src/web/server');
176
- const web = new SpiralWeb(null);
177
-
178
- process.env.PORT = options.port;
179
- web.port = parseInt(options.port);
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
- web.start();
182
- console.log(`\nSpiralcord Web Manager started!`);
183
- console.log(`Open http://localhost:${options.port} in your browser`);
184
- console.log(`Press Ctrl+C to stop`);
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
- const SpiralTester = require('../src/test');
201
- const tester = new SpiralTester(pluginsDir);
202
- const result = await tester.test();
647
+ try {
648
+ const SpiralTester = require('../src/test');
649
+ const tester = new SpiralTester(pluginsDir);
650
+ const result = await tester.test();
203
651
 
204
- for (const r of result.results) {
205
- const icon = r.status === 'ok' ? '\x1b[32m✓\x1b[0m' :
206
- r.status === 'warning' ? '\x1b[33m⚠\x1b[0m' :
207
- '\x1b[31m✗\x1b[0m';
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
- const extra = r.commands > 0 ? ` (${r.commands} commands)` : '';
210
- console.log(`${icon} ${r.name}${extra}`);
657
+ const extra = r.commands > 0 ? ` (${r.commands} commands)` : '';
658
+ console.log(`${icon} ${r.name}${extra}`);
211
659
 
212
- for (const issue of r.issues) {
213
- console.log(` → ${issue}`);
660
+ for (const issue of r.issues) {
661
+ console.log(` → ${issue}`);
662
+ }
214
663
  }
215
- }
216
664
 
217
- console.log('\n' + '─'.repeat(40));
665
+ console.log('\n' + '─'.repeat(40));
218
666
 
219
- const passedColor = result.errors === 0 ? '\x1b[32m' : '\x1b[31m';
220
- console.log(`${passedColor}Results: ${result.passed} passed, ${result.warnings} warnings, ${result.errors} errors\x1b[0m`);
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
- if (result.details.errors.length > 0) {
223
- console.log('\n\x1b[31mErrors:\x1b[0m');
224
- for (const err of result.details.errors) {
225
- console.log(` ✗ ${err}`);
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
- if (result.details.warnings.length > 0) {
230
- console.log('\n\x1b[33mWarnings:\x1b[0m');
231
- for (const warn of result.details.warnings) {
232
- console.log(` ⚠ ${warn}`);
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
- if (result.errors > 0) {
237
- console.log('\n\x1b[31mFix errors before running the bot.\x1b[0m');
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
- const pluginCmd = program
696
+ program
245
697
  .command('plugin')
246
- .description('Manage plugins');
247
-
248
- pluginCmd
249
- .command('list')
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
- execSync(`git clone --depth 1 ${repoUrl} "${pluginDir}"`, { stdio: 'inherit' });
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
- console.error('Failed to install plugin:', error.message);
354
- if (fs.existsSync(pluginDir)) {
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
- pluginCmd
361
- .command('enable <name>')
362
- .description('Enable a plugin')
363
- .action((name) => {
364
- const pluginsDir = path.join(process.cwd(), 'plugins');
365
- const pluginDir = path.join(pluginsDir, name);
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
- if (fs.existsSync(pluginDir)) {
407
- console.error(`Plugin "${name}" already exists.`);
408
- return;
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
- fs.mkdirSync(pluginDir, { recursive: true });
412
-
413
- const manifest = {
414
- name: name,
415
- version: '1.0.0',
416
- description: `${name} plugin`,
417
- hooks: ['message_received']
418
- };
419
-
420
- const config = {
421
- enabled: true,
422
- message: `Hello from ${name}!`
423
- };
424
-
425
- const schema = {
426
- $schema: 'http://json-schema.org/draft-07/schema#',
427
- title: `${name} Plugin Config`,
428
- type: 'object',
429
- properties: {
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.rmSync(pluginsDir, { recursive: true });
464
- console.log(`Plugin "${name}" removed.`);
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(`Plugin "${name}" not found.`);
756
+ console.log(`\x1b[33m⚠\x1b[0m No plugins directory`);
470
757
  }
471
- });
472
758
 
473
- pluginCmd
474
- .command('edit <name>')
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
- if (!fs.existsSync(pluginsDir)) {
480
- console.error(`Plugin "${name}" not found.`);
481
- return;
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();