meocord 3.1.0 → 3.2.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/README.md +138 -15
- package/dist/cjs/decorator/index.cjs +1 -1
- package/dist/esm/bin/app-template/README.md.template +61 -0
- package/dist/esm/bin/app-template/_env.example.template +2 -0
- package/dist/esm/bin/app-template/_gitignore.template +22 -0
- package/dist/esm/bin/app-template/_prettierrc.mjs.template +10 -0
- package/dist/esm/bin/app-template/eslint.config.ts.template +25 -0
- package/dist/esm/bin/app-template/meocord.config.ts.template +25 -0
- package/dist/esm/bin/app-template/package.json.template +38 -0
- package/dist/esm/bin/app-template/src/app.ts.template +44 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +28 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +29 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.ts.template +11 -0
- package/dist/esm/bin/app-template/src/controllers/slash/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.spec.ts.template +13 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.ts.template +52 -0
- package/dist/esm/bin/app-template/src/main.ts.template +14 -0
- package/dist/esm/bin/app-template/src/services/sample.service.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/services/sample.service.ts.template +9 -0
- package/dist/esm/bin/app-template/tsconfig.eslint.json.template +5 -0
- package/dist/esm/bin/app-template/tsconfig.json.template +30 -0
- package/dist/esm/bin/app-template/tsconfig.test.json.template +8 -0
- package/dist/esm/bin/app-template/vitest.config.ts.template +35 -0
- package/dist/esm/bin/builder-template/controller/button.controller.template +1 -1
- package/dist/esm/bin/helper/app-generator.helper.js +78 -0
- package/dist/esm/bin/meocord.js +156 -75
- package/dist/esm/decorator/guard.decorator.js +1 -1
- package/dist/esm/util/common.util.js +11 -3
- package/dist/esm/util/generator-cli.util.js +21 -4
- package/dist/esm/util/package-manager.util.js +9 -2
- package/dist/esm/util/package-version.util.js +32 -0
- package/dist/esm/util/runtime.util.js +72 -0
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/decorator/index.d.ts +1 -1
- package/dist/types/interface/index.d.ts +5 -3
- package/package.json +11 -12
package/dist/esm/bin/meocord.js
CHANGED
|
@@ -7,8 +7,9 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
import { capitalize } from 'lodash-es';
|
|
8
8
|
import wait from '../util/wait.util.js';
|
|
9
9
|
import { GeneratorCLI } from './generator.js';
|
|
10
|
+
import { runtimePrefixFor, AppGeneratorHelper } from './helper/app-generator.helper.js';
|
|
10
11
|
import * as fs from 'node:fs';
|
|
11
|
-
import { setEnvironment, compileAndValidateConfig } from '../util/common.util.js';
|
|
12
|
+
import { setEnvironment, compileAndValidateConfig, validateDiscordToken } from '../util/common.util.js';
|
|
12
13
|
import { prepareModifiedTsConfig } from '../util/tsconfig.util.js';
|
|
13
14
|
import { Command } from 'commander';
|
|
14
15
|
import { simpleGit } from 'simple-git';
|
|
@@ -16,6 +17,8 @@ import { execSync } from 'child_process';
|
|
|
16
17
|
import * as p from '@clack/prompts';
|
|
17
18
|
import { detectInstalledPMs, getInstallCommand } from '../util/package-manager.util.js';
|
|
18
19
|
import { ensureReady, configureCommandHelp } from '../util/meocord-cli.util.js';
|
|
20
|
+
import { resolveOwnVersion } from '../util/package-version.util.js';
|
|
21
|
+
import { buildAppCommand, resolveRuntime } from '../util/runtime.util.js';
|
|
19
22
|
import packageJson from '../package.json.js';
|
|
20
23
|
import { fileURLToPath } from 'url';
|
|
21
24
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
|
@@ -25,7 +28,35 @@ const __filename$1 = fileURLToPath(import.meta.url);
|
|
|
25
28
|
const __dirname$1 = path.dirname(__filename$1);
|
|
26
29
|
/**
|
|
27
30
|
* A Command Line Interface (CLI) for managing the MeoCord application.
|
|
28
|
-
*/
|
|
31
|
+
*/ /**
|
|
32
|
+
* Oldest Node the framework supports, mirroring `engines.node`.
|
|
33
|
+
*
|
|
34
|
+
* A test holds the two together, since a floor that drifted from the manifest would warn
|
|
35
|
+
* about versions npm is happy to install on.
|
|
36
|
+
*/ const MINIMUM_NODE_MAJOR = 22;
|
|
37
|
+
/**
|
|
38
|
+
* Warns when the running Node is older than the framework supports.
|
|
39
|
+
*
|
|
40
|
+
* Advisory only. Comparing against the newest LTS release meant warning about being a
|
|
41
|
+
* patch behind, and reaching the network to find out meant a failed request could stop
|
|
42
|
+
* the command outright — for a check whose only outcome is a warning.
|
|
43
|
+
*/ function warnIfNodeIsBelowSupported() {
|
|
44
|
+
const major = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
45
|
+
if (Number.isFinite(major) && major < MINIMUM_NODE_MAJOR) {
|
|
46
|
+
p.log.warn(`Node ${process.versions.node} is older than the supported minimum (v${MINIMUM_NODE_MAJOR}). ` + `The app will be created, but may not run.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The package manager's own complaint, which `execSync` buries on the thrown object.
|
|
51
|
+
*
|
|
52
|
+
* Its `message` says only that a command exited non-zero, which is never the part worth
|
|
53
|
+
* reading.
|
|
54
|
+
*/ function installFailure(error) {
|
|
55
|
+
const { stderr, message } = error ?? {};
|
|
56
|
+
const reported = stderr?.toString().trim();
|
|
57
|
+
return new Error(reported || message || String(error));
|
|
58
|
+
}
|
|
59
|
+
class MeoCordCLI {
|
|
29
60
|
/**
|
|
30
61
|
* Configures and runs the MeoCord CLI.
|
|
31
62
|
*/ async run() {
|
|
@@ -81,6 +112,9 @@ copies or substantial portions of the Software.
|
|
|
81
112
|
if (options.build || options.dev) {
|
|
82
113
|
await compileAndValidateConfig();
|
|
83
114
|
}
|
|
115
|
+
// Checked for every start, including one that skips the build: this is the point
|
|
116
|
+
// where the application actually needs to log in.
|
|
117
|
+
await validateDiscordToken();
|
|
84
118
|
if (options.build) {
|
|
85
119
|
await this.build(mode);
|
|
86
120
|
await this.compileConfig();
|
|
@@ -94,7 +128,6 @@ copies or substantial portions of the Software.
|
|
|
94
128
|
async createApp(appName, options) {
|
|
95
129
|
const kebabCaseAppName = appName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
96
130
|
const appPath = path.resolve(process.cwd(), kebabCaseAppName);
|
|
97
|
-
const gitRepo = 'https://github.com/l7aromeo/meocord-template.git';
|
|
98
131
|
p.intro(`meocord v${this.version}`);
|
|
99
132
|
// Validate directory
|
|
100
133
|
if (fs.existsSync(appPath)) {
|
|
@@ -102,24 +135,7 @@ copies or substantial portions of the Software.
|
|
|
102
135
|
await wait(100);
|
|
103
136
|
process.exit(1);
|
|
104
137
|
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const res = await fetch('https://nodejs.org/dist/index.json');
|
|
108
|
-
const releases = await res.json();
|
|
109
|
-
const ltsRelease = releases.find((r)=>r.lts !== false);
|
|
110
|
-
if (ltsRelease) latestLTS = ltsRelease.version.slice(1);
|
|
111
|
-
} catch {
|
|
112
|
-
p.cancel('No internet connection. Creating a MeoCord app requires network access.');
|
|
113
|
-
await wait(100);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
if (latestLTS) {
|
|
117
|
-
const [major, minor, patch] = process.version.slice(1).split('.').map(Number);
|
|
118
|
-
const [minMajor, minMinor, minPatch] = latestLTS.split('.').map(Number);
|
|
119
|
-
if (major < minMajor || major === minMajor && minor < minMinor || major === minMajor && minor === minMinor && patch < minPatch) {
|
|
120
|
-
p.log.warn(`Your Node.js (${process.version}) is behind the latest LTS (v${latestLTS}). Consider upgrading for best compatibility.`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
138
|
+
warnIfNodeIsBelowSupported();
|
|
123
139
|
// Determine package manager
|
|
124
140
|
const installedPMs = detectInstalledPMs();
|
|
125
141
|
let pm;
|
|
@@ -155,47 +171,42 @@ copies or substantial portions of the Software.
|
|
|
155
171
|
pm = selected;
|
|
156
172
|
}
|
|
157
173
|
const s = p.spinner();
|
|
158
|
-
// Clone template
|
|
159
174
|
s.start(`Creating a new MeoCord app: ${kebabCaseAppName}`);
|
|
160
175
|
try {
|
|
161
|
-
|
|
176
|
+
this.appGeneratorHelper.generateApp(appPath, {
|
|
177
|
+
appName: kebabCaseAppName,
|
|
178
|
+
displayName: appName,
|
|
179
|
+
version: this.version,
|
|
180
|
+
packageManager: pm,
|
|
181
|
+
runtimePrefix: runtimePrefixFor(pm)
|
|
182
|
+
});
|
|
162
183
|
} catch (error) {
|
|
163
|
-
s.stop('Failed to
|
|
164
|
-
|
|
165
|
-
await wait(100);
|
|
166
|
-
process.exit(1);
|
|
184
|
+
s.stop('Failed to create the app.');
|
|
185
|
+
await this.abortCreate(appPath, error);
|
|
167
186
|
}
|
|
168
187
|
s.stop(`App created at: ${appPath}`);
|
|
169
|
-
// Initialize git
|
|
170
188
|
s.start('Initializing Git repository...');
|
|
171
189
|
try {
|
|
172
|
-
fs.rmSync(path.join(appPath, '.git'), {
|
|
173
|
-
recursive: true,
|
|
174
|
-
force: true
|
|
175
|
-
});
|
|
176
190
|
const git = simpleGit(appPath);
|
|
177
191
|
await git.init();
|
|
178
192
|
await git.add('./*');
|
|
179
193
|
await git.commit('Initial commit');
|
|
180
194
|
} catch (error) {
|
|
181
195
|
s.stop('Failed to initialize Git.');
|
|
182
|
-
|
|
183
|
-
await wait(100);
|
|
184
|
-
process.exit(1);
|
|
196
|
+
await this.abortCreate(appPath, error);
|
|
185
197
|
}
|
|
186
198
|
s.stop('Git repository initialized.');
|
|
187
|
-
// Install dependencies
|
|
188
199
|
s.start(`Installing dependencies with ${pm}...`);
|
|
189
200
|
try {
|
|
201
|
+
// Output is captured rather than discarded: a failed install is only actionable
|
|
202
|
+
// if the resolver's complaint survives to the error message.
|
|
190
203
|
execSync(getInstallCommand(pm), {
|
|
191
204
|
cwd: appPath,
|
|
192
|
-
stdio: '
|
|
205
|
+
stdio: 'pipe'
|
|
193
206
|
});
|
|
194
207
|
} catch (error) {
|
|
195
208
|
s.stop('Failed to install dependencies.');
|
|
196
|
-
|
|
197
|
-
await wait(100);
|
|
198
|
-
process.exit(1);
|
|
209
|
+
await this.abortCreate(appPath, installFailure(error));
|
|
199
210
|
}
|
|
200
211
|
s.stop('Dependencies installed.');
|
|
201
212
|
p.outro(`MeoCord app "${kebabCaseAppName}" is ready!`);
|
|
@@ -341,6 +352,40 @@ copies or substantial portions of the Software.
|
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
/**
|
|
355
|
+
* Replaces the running application with one built from the current sources.
|
|
356
|
+
*
|
|
357
|
+
* The replacement is spawned only once the previous process has exited. Both would
|
|
358
|
+
* hold the same gateway session, and claiming it before the first lets go produces a
|
|
359
|
+
* login conflict rather than a reload.
|
|
360
|
+
*/ restartApp() {
|
|
361
|
+
const previous = this.appProcess;
|
|
362
|
+
this.appProcess = null;
|
|
363
|
+
if (!previous || previous.killed) {
|
|
364
|
+
this.appProcess = this.spawnApp();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
previous.removeAllListeners('exit');
|
|
368
|
+
previous.once('exit', ()=>{
|
|
369
|
+
this.appProcess = this.spawnApp();
|
|
370
|
+
});
|
|
371
|
+
previous.kill();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Runs the built application.
|
|
375
|
+
*
|
|
376
|
+
* Shared by both start modes so watching and production launch the bundle the same
|
|
377
|
+
* way; a watcher that spawned it differently could pass in development and fail in
|
|
378
|
+
* production on the difference alone.
|
|
379
|
+
*
|
|
380
|
+
* @returns The application process.
|
|
381
|
+
*/ spawnApp() {
|
|
382
|
+
const { command, args } = buildAppCommand(this.runtime, this.mainJSPath);
|
|
383
|
+
return spawn(command, args, {
|
|
384
|
+
cwd: this.projectRoot,
|
|
385
|
+
stdio: 'inherit'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
344
389
|
* Starts the MeoCord application in development mode with live updates.
|
|
345
390
|
*/ async startDev() {
|
|
346
391
|
try {
|
|
@@ -357,7 +402,6 @@ copies or substantial portions of the Software.
|
|
|
357
402
|
await wait(100);
|
|
358
403
|
process.exit(1);
|
|
359
404
|
}
|
|
360
|
-
let nodemonProcess = null;
|
|
361
405
|
let isRunning = false;
|
|
362
406
|
const watch = ()=>compiler.watch({}, (err, stats)=>{
|
|
363
407
|
if (err) {
|
|
@@ -367,18 +411,7 @@ copies or substantial portions of the Software.
|
|
|
367
411
|
if (stats?.hasErrors()) {
|
|
368
412
|
this.logger.error('Build failed due to errors in the compilation process:', stats.compilation.errors);
|
|
369
413
|
} else {
|
|
370
|
-
|
|
371
|
-
nodemonProcess.kill();
|
|
372
|
-
nodemonProcess = null;
|
|
373
|
-
}
|
|
374
|
-
nodemonProcess = spawn('npx -y nodemon', [
|
|
375
|
-
'-q',
|
|
376
|
-
this.mainJSPath
|
|
377
|
-
], {
|
|
378
|
-
shell: true,
|
|
379
|
-
cwd: this.projectRoot,
|
|
380
|
-
stdio: 'inherit'
|
|
381
|
-
});
|
|
414
|
+
this.restartApp();
|
|
382
415
|
isRunning = true;
|
|
383
416
|
}
|
|
384
417
|
});
|
|
@@ -387,12 +420,12 @@ copies or substantial portions of the Software.
|
|
|
387
420
|
const fsWatcher = fs.watch(path.resolve(process.cwd(), 'meocord.config.ts'), ()=>{
|
|
388
421
|
clearTimeout(debounceWatcher);
|
|
389
422
|
debounceWatcher = setTimeout(async ()=>{
|
|
390
|
-
if (isRunning &&
|
|
423
|
+
if (isRunning && this.appProcess) {
|
|
391
424
|
isRunning = false;
|
|
392
425
|
this.logger.log('MeoCord config change detected, reloading config...');
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
if (this.appProcess && !this.appProcess.killed) {
|
|
427
|
+
this.appProcess.kill();
|
|
428
|
+
this.appProcess = null;
|
|
396
429
|
}
|
|
397
430
|
await new Promise((resolve)=>compiler.close(resolve));
|
|
398
431
|
watch();
|
|
@@ -403,15 +436,15 @@ copies or substantial portions of the Software.
|
|
|
403
436
|
process.on('SIGINT', async ()=>{
|
|
404
437
|
if (sigintReceived) {
|
|
405
438
|
// Second Ctrl+C — force kill and exit immediately
|
|
406
|
-
if (
|
|
439
|
+
if (this.appProcess && !this.appProcess.killed) this.appProcess.kill('SIGKILL');
|
|
407
440
|
process.exit(1);
|
|
408
441
|
}
|
|
409
442
|
sigintReceived = true;
|
|
410
|
-
//
|
|
411
|
-
//
|
|
443
|
+
// The application already received SIGINT from the process group. Clean up
|
|
444
|
+
// parent-owned resources and wait for it to exit on its own terms.
|
|
412
445
|
fsWatcher.close();
|
|
413
|
-
if (
|
|
414
|
-
|
|
446
|
+
if (this.appProcess && !this.appProcess.killed) {
|
|
447
|
+
this.appProcess.on('exit', async ()=>{
|
|
415
448
|
await new Promise((resolve)=>compiler.close(resolve));
|
|
416
449
|
process.exit(0);
|
|
417
450
|
});
|
|
@@ -436,11 +469,7 @@ copies or substantial portions of the Software.
|
|
|
436
469
|
}
|
|
437
470
|
this.clearConsole();
|
|
438
471
|
this.logger.log('Starting...');
|
|
439
|
-
const start = spawn
|
|
440
|
-
shell: true,
|
|
441
|
-
cwd: this.projectRoot,
|
|
442
|
-
stdio: 'inherit'
|
|
443
|
-
}).on('spawn', this.clearConsole);
|
|
472
|
+
const start = this.spawnApp().on('spawn', this.clearConsole);
|
|
444
473
|
start.on('exit', (code)=>{
|
|
445
474
|
process.exit(code ?? 0);
|
|
446
475
|
});
|
|
@@ -462,6 +491,20 @@ copies or substantial portions of the Software.
|
|
|
462
491
|
}
|
|
463
492
|
}
|
|
464
493
|
/**
|
|
494
|
+
* Reports why creation failed and removes what was written.
|
|
495
|
+
*
|
|
496
|
+
* A directory left behind from a failed attempt is indistinguishable from one the user
|
|
497
|
+
* meant to keep, and the next attempt refuses to start because the name is taken.
|
|
498
|
+
*/ async abortCreate(appPath, error) {
|
|
499
|
+
fs.rmSync(appPath, {
|
|
500
|
+
recursive: true,
|
|
501
|
+
force: true
|
|
502
|
+
});
|
|
503
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
504
|
+
await wait(100);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
465
508
|
* Clears the console on all platforms.
|
|
466
509
|
*/ clearConsole() {
|
|
467
510
|
process.stdout.write('\u001b[3J\u001b[2J\u001b[H');
|
|
@@ -473,13 +516,51 @@ copies or substantial portions of the Software.
|
|
|
473
516
|
this.mainJSPath = path.join(this.projectRoot, 'dist', 'main.js');
|
|
474
517
|
this.webpackConfigPath = path.resolve(__dirname$1, '..', '..', '..', 'webpack.config.js');
|
|
475
518
|
this.generatorCLI = new GeneratorCLI(this.appName);
|
|
476
|
-
this.
|
|
519
|
+
this.appGeneratorHelper = new AppGeneratorHelper();
|
|
520
|
+
this.version = resolveOwnVersion(__dirname$1, packageJson.version);
|
|
521
|
+
/**
|
|
522
|
+
* Binary the application is spawned with, so it runs on the same runtime as the CLI
|
|
523
|
+
* rather than on whichever one happens to be named in the source.
|
|
524
|
+
*/ this.runtime = resolveRuntime(process.env, process.execPath);
|
|
525
|
+
/** The running application, while a watch session owns one. */ this.appProcess = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Whether this file is what the process was started with.
|
|
530
|
+
*
|
|
531
|
+
* The bin ships behind a `node_modules/.bin` symlink, so `argv[1]` is the link while
|
|
532
|
+
* `import.meta.url` is its target; the two only agree once the link is resolved.
|
|
533
|
+
* Anything undecidable counts as a launch, so the published CLI still starts in cases
|
|
534
|
+
* this cannot classify.
|
|
535
|
+
*/ function isProcessEntry() {
|
|
536
|
+
const invoked = process.argv[1];
|
|
537
|
+
if (invoked === undefined) return true;
|
|
538
|
+
try {
|
|
539
|
+
return samePath(fs.realpathSync(invoked), __filename$1);
|
|
540
|
+
} catch {
|
|
541
|
+
return true;
|
|
477
542
|
}
|
|
478
543
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Whether two resolved paths name the same file.
|
|
546
|
+
*
|
|
547
|
+
* Windows paths differ in case and separator without naming different files — npm's
|
|
548
|
+
* shim passes the script path in whatever form it recorded — and a comparison that
|
|
549
|
+
* missed would leave the CLI exiting without a word, since it would decide it was
|
|
550
|
+
* merely being imported.
|
|
551
|
+
*/ function samePath(left, right) {
|
|
552
|
+
const normalise = (value)=>process.platform === 'win32' ? path.resolve(value).toLowerCase() : value;
|
|
553
|
+
return normalise(left) === normalise(right);
|
|
554
|
+
}
|
|
555
|
+
// Importing this module must not launch the command parser: its own tests do exactly
|
|
556
|
+
// that to inspect how the application is spawned.
|
|
557
|
+
if (isProcessEntry()) {
|
|
558
|
+
const cli = new MeoCordCLI();
|
|
559
|
+
cli.run().catch(async (error)=>{
|
|
560
|
+
cli.logger.error('Failed to initialize CLI:', error?.message || error);
|
|
561
|
+
await wait(100);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export { MINIMUM_NODE_MAJOR, MeoCordCLI };
|
|
@@ -99,7 +99,7 @@ export class ButtonInteractionGuard implements GuardInterface {
|
|
|
99
99
|
* @example
|
|
100
100
|
* ```typescript
|
|
101
101
|
* // Method-level usage
|
|
102
|
-
* @Command('profile
|
|
102
|
+
* @Command('profile/{id}', CommandType.BUTTON)
|
|
103
103
|
* @UseGuard(
|
|
104
104
|
* { provide: RateLimiterGuard, params: { limit: 2, window: 3000 } },
|
|
105
105
|
* ButtonInteractionGuard
|
|
@@ -47,8 +47,16 @@ import chalk from 'chalk';
|
|
|
47
47
|
await wait(100);
|
|
48
48
|
process.exit(1);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
loadMeoCordConfig();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Ensures a Discord token is configured.
|
|
54
|
+
*
|
|
55
|
+
* Kept apart from {@link compileAndValidateConfig} because producing a bundle needs no
|
|
56
|
+
* credentials — only connecting to the gateway does. Requiring one to build meant a
|
|
57
|
+
* freshly created application could not be built until a token had been obtained.
|
|
58
|
+
*/ async function validateDiscordToken() {
|
|
59
|
+
if (!loadMeoCordConfig()?.discordToken) {
|
|
52
60
|
console.error(chalk.red('Discord token is missing!'));
|
|
53
61
|
await wait(100);
|
|
54
62
|
process.exit(1);
|
|
@@ -65,4 +73,4 @@ import chalk from 'chalk';
|
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
export { compileAndValidateConfig, findModulePackageDir, setEnvironment };
|
|
76
|
+
export { compileAndValidateConfig, findModulePackageDir, setEnvironment, validateDiscordToken };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
4
|
import { Logger } from '../common/logger.js';
|
|
5
5
|
import '../common/theme.js';
|
|
6
6
|
import { kebabCase, startCase, camelCase } from 'lodash-es';
|
|
@@ -67,12 +67,29 @@ const logger = new Logger('MeoCord');
|
|
|
67
67
|
*/ function generateFile(filePath, content) {
|
|
68
68
|
try {
|
|
69
69
|
fs.writeFileSync(filePath, content);
|
|
70
|
-
logger.log(`
|
|
71
|
-
|
|
70
|
+
logger.log(`Created ${path.relative(process.cwd(), filePath)}`);
|
|
71
|
+
formatWithLocalESLint(filePath);
|
|
72
72
|
} catch (error) {
|
|
73
|
-
logger.error(`Failed to create
|
|
73
|
+
logger.error(`Failed to create ${filePath}`, error);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Formats a generated file with the project's own ESLint, when it has one.
|
|
78
|
+
*
|
|
79
|
+
* Reaching for `npx` instead would start downloading ESLint into a project that
|
|
80
|
+
* deliberately does not have it, once per generated file, with no way to see it happen —
|
|
81
|
+
* the call is not awaited. A project with its own rules still gets them applied.
|
|
82
|
+
*/ function formatWithLocalESLint(filePath) {
|
|
83
|
+
const binary = path.resolve(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? 'eslint.cmd' : 'eslint');
|
|
84
|
+
if (!fs.existsSync(binary)) return;
|
|
85
|
+
execFile(binary, [
|
|
86
|
+
'--fix',
|
|
87
|
+
filePath
|
|
88
|
+
], ()=>{
|
|
89
|
+
// Formatting is a courtesy; a project whose rules reject the template should still
|
|
90
|
+
// end up with the file it asked for.
|
|
91
|
+
});
|
|
92
|
+
}
|
|
76
93
|
/**
|
|
77
94
|
* Builds and returns a template string for a given class name using a specific template file.
|
|
78
95
|
* @param className - The name of the class to insert into the template.
|
|
@@ -6,10 +6,17 @@ const ALL_PACKAGE_MANAGERS = [
|
|
|
6
6
|
'yarn',
|
|
7
7
|
'pnpm'
|
|
8
8
|
];
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The package managers available on this machine.
|
|
11
|
+
*
|
|
12
|
+
* Each candidate is asked for its version rather than looked up with `which`: `which` is
|
|
13
|
+
* a separate binary that a minimal image need not carry, and it is absent on Windows
|
|
14
|
+
* entirely. Running the tool also answers the question actually being asked — whether it
|
|
15
|
+
* works — rather than whether something with that name sits on the path.
|
|
16
|
+
*/ function detectInstalledPMs() {
|
|
10
17
|
return ALL_PACKAGE_MANAGERS.filter((pm)=>{
|
|
11
18
|
try {
|
|
12
|
-
execSync(
|
|
19
|
+
execSync(`${pm} --version`, {
|
|
13
20
|
stdio: 'ignore'
|
|
14
21
|
});
|
|
15
22
|
return true;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs__default from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** The package this CLI ships in, which is where the walk below stops. */ const OWN_PACKAGE_NAME = 'meocord';
|
|
5
|
+
/**
|
|
6
|
+
* The version recorded in the package this CLI ships in.
|
|
7
|
+
*
|
|
8
|
+
* Read at run time rather than compiled in. The release bumps the manifest after the
|
|
9
|
+
* bundle has already been built, so a version baked into the bundle is always the one
|
|
10
|
+
* from before the release that published it.
|
|
11
|
+
*
|
|
12
|
+
* The walk stops at the first manifest naming this package: the repository root when
|
|
13
|
+
* running from source, the installed package root when running from `dist`.
|
|
14
|
+
*
|
|
15
|
+
* @param startDir - Directory to walk up from.
|
|
16
|
+
* @param fallback - Returned when no manifest for this package can be read.
|
|
17
|
+
*/ function resolveOwnVersion(startDir, fallback) {
|
|
18
|
+
let current = path.resolve(startDir);
|
|
19
|
+
while(true){
|
|
20
|
+
try {
|
|
21
|
+
const manifest = JSON.parse(fs__default.readFileSync(path.join(current, 'package.json'), 'utf8'));
|
|
22
|
+
if (manifest.name === OWN_PACKAGE_NAME && typeof manifest.version === 'string') return manifest.version;
|
|
23
|
+
} catch {
|
|
24
|
+
// Nothing readable here; the next directory up may still hold the manifest.
|
|
25
|
+
}
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
if (parent === current) return fallback;
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { resolveOwnVersion };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeoCord Framework
|
|
3
|
+
* Copyright (c) 2025 Ukasyah Rahmatullah Zada
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/ /**
|
|
6
|
+
* Environment variable that pins the binary the application is spawned with.
|
|
7
|
+
*
|
|
8
|
+
* An escape hatch for pinning a specific install, or for running the application under a
|
|
9
|
+
* different runtime than the CLI to compare behaviour. Unset is the normal case.
|
|
10
|
+
*/ const RUNTIME_OVERRIDE_ENV = 'MEOCORD_RUNTIME';
|
|
11
|
+
/**
|
|
12
|
+
* Runners that are themselves JavaScript runtimes.
|
|
13
|
+
*
|
|
14
|
+
* `npm_execpath` names the binary that launched the script. npm, pnpm and yarn set it to
|
|
15
|
+
* a `.js` file, which cannot run the application, so only the runners listed here are
|
|
16
|
+
* read as a runtime. An entry belongs here only once it has been checked against that
|
|
17
|
+
* runner's real environment, since guessing wrong means spawning something that cannot
|
|
18
|
+
* execute the bundle.
|
|
19
|
+
*/ const RUNTIME_RUNNERS = new Set([
|
|
20
|
+
'bun'
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* The runtime that launched the CLI, when the CLI is not itself running on it.
|
|
24
|
+
*
|
|
25
|
+
* `bun run` honours the bin's `#!/usr/bin/env node` shebang, so the CLI lands on node
|
|
26
|
+
* even though the user asked for bun. What they chose is still recoverable: bun sets
|
|
27
|
+
* `npm_config_user_agent` to `bun/<version> …` and `npm_execpath` to its own binary.
|
|
28
|
+
*
|
|
29
|
+
* @param env - Environment the CLI was launched with.
|
|
30
|
+
* @returns The launcher's binary, or `undefined` when it cannot run the application.
|
|
31
|
+
*/ function launcherRuntime(env) {
|
|
32
|
+
const runner = env.npm_config_user_agent?.split('/')[0];
|
|
33
|
+
const launcher = env.npm_execpath?.trim();
|
|
34
|
+
if (runner === undefined || !launcher) return undefined;
|
|
35
|
+
return RUNTIME_RUNNERS.has(runner) ? launcher : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The binary the application should be spawned with.
|
|
39
|
+
*
|
|
40
|
+
* Follows the runtime the user chose rather than naming one. Someone who typed `bun`
|
|
41
|
+
* gets a bun process, and an image built on bun alone stays that way — pinning `node`
|
|
42
|
+
* would oblige them to install a second runtime beside the one they picked, or to
|
|
43
|
+
* remember `--bun` on every command.
|
|
44
|
+
*
|
|
45
|
+
* Preference runs from the most explicit signal to the least: an override, then the
|
|
46
|
+
* runner that launched the CLI, then the binary executing it.
|
|
47
|
+
*
|
|
48
|
+
* @param env - Environment the CLI was launched with.
|
|
49
|
+
* @param execPath - Binary executing the CLI, i.e. `process.execPath`.
|
|
50
|
+
* @returns The binary to spawn.
|
|
51
|
+
*/ function resolveRuntime(env, execPath) {
|
|
52
|
+
const override = env[RUNTIME_OVERRIDE_ENV]?.trim();
|
|
53
|
+
// An override of only whitespace would spawn '' and fail with an ENOENT naming nothing,
|
|
54
|
+
// so it is treated as absent rather than passed through.
|
|
55
|
+
if (override) return override;
|
|
56
|
+
return launcherRuntime(env) ?? execPath;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Runs the built application directly.
|
|
60
|
+
*
|
|
61
|
+
* @param runtime - Binary to run the application with.
|
|
62
|
+
* @param mainJsPath - Absolute path to the built entry file.
|
|
63
|
+
*/ function buildAppCommand(runtime, mainJsPath) {
|
|
64
|
+
return {
|
|
65
|
+
command: runtime,
|
|
66
|
+
args: [
|
|
67
|
+
mainJsPath
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { RUNTIME_OVERRIDE_ENV, buildAppCommand, resolveRuntime };
|
|
@@ -52,8 +52,8 @@ declare class MeoCordApp {
|
|
|
52
52
|
*
|
|
53
53
|
* Built once and cached, so dispatch stays a single ordered walk with an early exit
|
|
54
54
|
* rather than paying to rank anything per interaction. Ordering here is what lets
|
|
55
|
-
* `gi-profile
|
|
56
|
-
* `gi-profile
|
|
55
|
+
* `gi-profile/summary/{ownerId}/{uid}` keep the ids it owns when
|
|
56
|
+
* `gi-profile/{uuid}/{uid}` would also match them — without it, the winner would be
|
|
57
57
|
* whichever controller happened to be registered first.
|
|
58
58
|
*/
|
|
59
59
|
private componentRoutes?;
|
|
@@ -295,7 +295,7 @@ interface GuardWithParams {
|
|
|
295
295
|
* @example
|
|
296
296
|
* ```typescript
|
|
297
297
|
* // Method-level usage
|
|
298
|
-
* @Command('profile
|
|
298
|
+
* @Command('profile/{id}', CommandType.BUTTON)
|
|
299
299
|
* @UseGuard(
|
|
300
300
|
* { provide: RateLimiterGuard, params: { limit: 2, window: 3000 } },
|
|
301
301
|
* ButtonInteractionGuard
|
|
@@ -57,9 +57,11 @@ interface CommandMetadata<T extends string = string> {
|
|
|
57
57
|
dynamicParams?: T[];
|
|
58
58
|
/**
|
|
59
59
|
* How specific this pattern is; higher wins when more than one route matches the
|
|
60
|
-
* same customId. A
|
|
61
|
-
* `gi-profile
|
|
62
|
-
* `gi-profile-summary
|
|
60
|
+
* same customId. A parameter takes whatever fills its segment, so a broad pattern can
|
|
61
|
+
* also match an id a more literal sibling owns — `gi-profile/{uuid}/{uid}` matches
|
|
62
|
+
* `gi-profile/summary/123/456` just as `gi-profile/summary/{ownerId}/{uid}` does — and
|
|
63
|
+
* ranking is what settles it, since declaration order would make the winner depend on
|
|
64
|
+
* file layout.
|
|
63
65
|
*/
|
|
64
66
|
specificity?: number;
|
|
65
67
|
}
|