7 Commits

Author SHA1 Message Date
jacoobes
088c3eb6ac fix: homepage 2023-03-31 13:08:35 -05:00
jacoobes
68c096e689 chore: use constant instead of function 2023-03-31 12:57:07 -05:00
jacoobes
34bf4d004c merge corectly 2023-03-31 12:40:46 -05:00
jacoobes
309bf5224a merge 2023-03-31 12:40:04 -05:00
jacoobes
4382ce6d5e Merge branch 'main' into plugin-version-management 2023-03-31 12:39:03 -05:00
jacoobes
731017f637 feat: add more methods 2023-02-16 12:07:16 -06:00
Jacob Nguyen
a3f5f1bf52 chore: update discord link (#89) 2023-02-16 11:46:11 -06:00
40 changed files with 5429 additions and 5536 deletions

View File

@@ -1,6 +1,5 @@
{
"tabWidth": 4,
"useTabs": false,
"singleQuote": true,
"printWidth": 140
"tabWidth": 4,
"useTabs": true,
"singleQuote": true
}

View File

@@ -2,55 +2,6 @@
All notable changes to this project will be documented in this file.
## [1.0.0](https://github.com/sern-handler/cli/compare/v0.6.0...v1.0.0) (2023-09-05)
### ⚠ BREAKING CHANGES
* **plugins:** older versions of cli does not have plugins command functional
### Features
* build app ([#112](https://github.com/sern-handler/cli/issues/112)) ([88e2bbf](https://github.com/sern-handler/cli/commit/88e2bbf6c84e3841370a4288181e8ec721ef1925))
* list subcommand ([#113](https://github.com/sern-handler/cli/issues/113)) ([5fd2a7b](https://github.com/sern-handler/cli/commit/5fd2a7b4c4bc92467fbaa26005d261d4ed8b2a13))
* **plugins:** new method to obtain plugins ([#114](https://github.com/sern-handler/cli/issues/114)) ([c785d49](https://github.com/sern-handler/cli/commit/c785d49fa5c0d98261de7d7b0c39f85c21316156))
### Bug Fixes
* credentials precedence ([#111](https://github.com/sern-handler/cli/issues/111)) ([3f74658](https://github.com/sern-handler/cli/commit/3f74658b16b2697df11c7e33172c09c30b7543a8))
* **extra:** dockerfile errors and tsc fallback ([#101](https://github.com/sern-handler/cli/issues/101)) ([4082926](https://github.com/sern-handler/cli/commit/40829267c4e77b316a60604c63bad79124713b89))
## [0.6.0](https://github.com/sern-handler/cli/compare/v0.5.1...v0.6.0) (2023-08-09)
### Features
* publish command ([#105](https://github.com/sern-handler/cli/issues/105)) ([827ffb7](https://github.com/sern-handler/cli/commit/827ffb7ad9252e3cda257bed1febdca2da03e253))
### Bug Fixes
* change file path for sern/extras ([#109](https://github.com/sern-handler/cli/issues/109)) ([2348e3e](https://github.com/sern-handler/cli/commit/2348e3e9ab055fddab3f44d574af79fd7ccd4485))
### Miscellaneous Chores
* release 0.6.0 ([642bf11](https://github.com/sern-handler/cli/commit/642bf11608cf5d9b4256999e3bdb48e762ca88ee))
## [0.5.1](https://github.com/sern-handler/cli/compare/v0.5.0...v0.5.1) (2023-06-16)
### Features
* **init:** deprecate init and bump deps ([#102](https://github.com/sern-handler/cli/issues/102)) ([dce78c0](https://github.com/sern-handler/cli/commit/dce78c0945de6da79bf1e268f29651da0c44c1eb))
* version injector ([#90](https://github.com/sern-handler/cli/issues/90)) ([58fa325](https://github.com/sern-handler/cli/commit/58fa3253f62da9fb66d1b2ae901b568367f065d0))
### Bug Fixes
* git not installed errors during init ([#79](https://github.com/sern-handler/cli/issues/79)) ([69287ab](https://github.com/sern-handler/cli/commit/69287ab1bd0c4960384144f90fea8ebded3b0cc5))
## [0.5.0](https://github.com/sern-handler/cli/compare/v0.4.2...v0.5.0) (2022-09-16)

View File

@@ -30,34 +30,29 @@ When you install the CLI, you can use our commands with **sern** prefix.
```
Usage: sern [options] [command]
Welcome to sern!
If you're new to sern, run sern init for an interactive setup to your new bot project!
___ ___ _ __ _ __
/ __|/ _ \ '__| '_ \
\__ \ __/ | | | | |
|___/\___|_| |_| |_|
Welcome!
If you're new to sern, run npm create @sern/bot for an interactive setup to your new bot project!
If you have any ideas, suggestions, bug reports, kindly join our support server: https://sern.dev/discord
If you have any ideas, suggestions, bug reports, kindly join our support server: https://discord.gg/xzK5fUKT4r
Options:
-v, --version output the version number
-V, --version output the version number
-h, --help display help for command
Commands:
init [options] Quickest way to scaffold a new project [DEPRECATED]
init [options] Quickest way to scaffold a new project
plugins [options] Install plugins from https://github.com/sern-handler/awesome-plugins
extra Easy way to add extra things in your sern project
commands Defacto way to manage your slash commands
help [command] display help for command
```
## Setting Up Your Project
Run `npm create @sern/bot` for an interactive setup on a brand new project using our framework.
Run `sern init (-y)` for an interactive setup on a brand new project using our framework. <br>
Adding the `-y` flag sets up project as default. ( **Note** : the default initiates a typescript project)
## Installing Plugins
sern runs on your plugins. Contribute to our [repository](https://github.com/sern-handler/awesome-plugins) and then install the plugins via our cli! <br>
Run `sern plugins` to see all installable plugins.
sern runs on your plugins. Contribute to the [repository](https://github.com/sern-handler/awesome-plugins) and then install the plugins via our cli! <br>
Run `sern plugins` to see all installable options

8968
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,58 @@
{
"name": "@sern/cli",
"version": "1.0.0",
"description": "Official CLI for @sern/handler",
"exports": "./dist/index.js",
"bin": {
"sern": "./dist/index.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --check .",
"fix": "prettier --write .",
"build": "tsup",
"watch": "tsup --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/cli.git"
},
"keywords": [
"cli",
"discord",
"discord.js",
"sern",
"sern-handler"
],
"author": "EvolutionX",
"license": "MIT",
"bugs": {
"url": "https://github.com/sern-handler/cli/issues"
},
"homepage": "https://sern.dev",
"dependencies": {
"@esbuild-kit/cjs-loader": "^2.4.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"colorette": "2.0.20",
"commander": "11.0.0",
"dotenv": "^16.3.1",
"esbuild": "^0.19.1",
"execa": "7.2.0",
"find-up": "6.3.0",
"glob": "^10.3.3",
"ora": "6.3.1",
"prompts": "2.4.2",
"undici": "5.23.0"
},
"devDependencies": {
"@babel/parser": "^7.22.5",
"@favware/npm-deprecate": "1.0.7",
"@types/prompts": "2.4.4",
"prettier": "2.8.8",
"tsup": "6.7.0",
"typescript": "5.2.2"
},
"engines": {
"node": ">= 18.16.x"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
}
"name": "@sern/cli",
"version": "0.5.0",
"description": "Official CLI for @sern/handler",
"exports": "./dist/index.js",
"bin": {
"sern": "./dist/index.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --check .",
"fix": "prettier --write .",
"build": "tsup",
"watch": "tsc --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sern-handler/cli.git"
},
"keywords": [
"cli",
"discord",
"discord.js",
"sern",
"sern-handler"
],
"author": "EvolutionX",
"license": "MIT",
"bugs": {
"url": "https://github.com/sern-handler/cli/issues"
},
"homepage": "https://sern.dev",
"dependencies": {
"colorette": "^2.0.16",
"commander": "^9.3.0",
"execa": "^6.1.0",
"find-up": "6.3.0",
"ora": "^6.1.0",
"prompts": "2.4.2",
"undici": "^5.6.1"
},
"devDependencies": {
"@favware/npm-deprecate": "1.0.7",
"@types/prompts": "2.4.3",
"esbuild-plugin-version-injector": "^1.0.3",
"prettier": "2.8.4",
"tsup": "^6.6.3",
"typescript": "4.9.5"
},
"engines": {
"node": ">= 16.10.x"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
}
}

View File

@@ -1,17 +1,17 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "group:allNonMajor"],
"major": {
"dependencyDashboardApproval": true
},
"schedule": ["every weekend"],
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/"
}
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"major": {
"dependencyDashboardApproval": true
},
"schedule": ["every weekend"],
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/"
}
]
}

View File

@@ -1,161 +0,0 @@
import esbuild from 'esbuild';
import { getConfig } from '../utilities/getConfig';
import { resolve } from 'node:path';
import { glob } from 'glob';
import { configDotenv } from 'dotenv';
import assert from 'node:assert';
import { imageLoader, validExtensions } from '../plugins/imageLoader';
import defaultEsbuild from '../utilities/defaultEsbuildConfig';
import { require } from '../utilities/require';
import { pathExists, pathExistsSync } from 'find-up';
import { mkdir, writeFile } from 'fs/promises';
import * as Preprocessor from '../utilities/preprocessor';
import { bold, magentaBright } from 'colorette';
type BuildOptions = {
/**
* Define __VERSION__
* This option is a quick switch to defining the __VERSION__ constant which will be a string of the version provided in
* cwd's package.json
*/
defineVersion?: boolean;
/**
* default = esm
*/
format?: 'cjs' | 'esm';
/**
* extra esbuild plugins to build with sern.
*/
esbuildPlugins?: esbuild.Plugin[];
/**
* https://esbuild.github.io/api/#drop-labels
**/
dropLabels?: string[];
/**
* https://esbuild.github.io/api/#define
**/
define?: Record<string, string>;
tsconfig?: string;
/**
* default = 'development'
*/
mode: 'production' | 'development';
/**
* will search for env file. If none exists,
* default to .env.
*/
env?: string;
};
export async function build(options: Record<string, any>) {
if (!options.supressWarnings) {
console.info(`${magentaBright('EXPERIMENTAL')}: This API has not been stabilized. add -W or --suppress-warnings flag to suppress`);
}
const sernConfig = await getConfig();
let buildConfig: Partial<BuildOptions> = {};
const entryPoints = await glob(`./src/**/*{${validExtensions.join(',')}}`, {
//for some reason, my ignore glob wasn't registering correctly'
ignore: {
ignored: (p) => p.name.endsWith('.d.ts'),
},
});
const buildConfigPath = resolve(options.project ?? 'sern.build.js');
const defaultBuildConfig = {
defineVersion: true,
format: options.format ?? 'esm',
mode: options.mode ?? 'development',
dropLabels: [],
tsconfig: options.tsconfig ?? resolve('tsconfig.json'),
env: options.env ?? resolve('.env'),
};
if (pathExistsSync(buildConfigPath)) {
try {
buildConfig = {
...defaultBuildConfig,
...(await import('file:///' + buildConfigPath)).default,
};
} catch (e) {
console.log(e);
process.exit(1);
}
} else {
buildConfig = {
...defaultBuildConfig,
};
console.log('No build config found, defaulting');
}
let env = {} as Record<string, string>;
configDotenv({ path: buildConfig.env, processEnv: env });
if (env.MODE && !env.NODE_ENV) {
console.warn('Use NODE_ENV instead of MODE');
console.warn('MODE has no effect.');
console.warn(`https://nodejs.dev/en/learn/nodejs-the-difference-between-development-and-production/`);
}
if (env.NODE_ENV) {
buildConfig.mode = env.NODE_ENV as 'production' | 'development';
console.log(magentaBright('NODE_ENV:'), 'Found NODE_ENV variable, setting `mode` to this.');
}
assert(buildConfig.mode === 'development' || buildConfig.mode === 'production', 'Mode is not `production` or `development`');
const defaultTsConfig = {
extends: './.sern/tsconfig.json',
};
!buildConfig.tsconfig && console.log('Using default options for tsconfig', defaultTsConfig);
const tsconfigRaw = require(buildConfig.tsconfig!);
sernConfig.language === 'typescript' &&
tsconfigRaw &&
!tsconfigRaw.extends &&
(console.warn('tsconfig does not contain an "extends". Will not use sern automatic path aliasing'),
console.warn('For projects that predate sern build and want to fully integrate, extend the tsconfig generated in .sern'),
console.warn('Extend preexisting tsconfig with top level: "extends": "./.sern/tsconfig.json"'));
console.log(bold('Building with:'));
console.log(' ', magentaBright('defineVersion'), buildConfig.defineVersion);
console.log(' ', magentaBright('format'), buildConfig.format);
console.log(' ', magentaBright('mode'), buildConfig.mode);
console.log(' ', magentaBright('tsconfig'), buildConfig.tsconfig);
console.log(' ', magentaBright('env'), buildConfig.env);
const sernDir = resolve('.sern'),
genDir = resolve(sernDir, 'generated'),
ambientFilePath = resolve(sernDir, 'ambient.d.ts'),
packageJsonPath = resolve('package.json'),
sernTsConfigPath = resolve(sernDir, 'tsconfig.json'),
packageJson = () => require(packageJsonPath);
if (!(await pathExists(genDir))) {
console.log('Making .sern/generated dir, does not exist');
await mkdir(genDir);
}
try {
const defVersion = () => JSON.stringify(packageJson().version);
const define = {
...(buildConfig.define ?? {}),
__DEV__: `${buildConfig.mode === 'development'}`,
__PROD__: `${buildConfig.mode === 'production'}`,
} satisfies Record<string, string>;
buildConfig.defineVersion && Object.assign(define, { __VERSION__: defVersion() });
await Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile);
await Preprocessor.writeAmbientFile(ambientFilePath, define, writeFile);
//https://esbuild.github.io/content-types/#tsconfig-json
await esbuild.build({
entryPoints,
plugins: [imageLoader, ...(buildConfig.esbuildPlugins ?? [])],
...defaultEsbuild(buildConfig.format!, tsconfigRaw),
define,
dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!],
});
} catch (e) {
console.error(e);
process.exit(1);
}
}

View File

@@ -3,10 +3,9 @@ import { extraPrompt } from '../prompts/extra.js';
import { create } from '../utilities/create.js';
export async function extra() {
const extra = await prompt([extraPrompt]);
const extra = await prompt([extraPrompt]);
if (Object.keys(extra).length < 1) process.exit(1);
const lang = extra.extra.includes('typescript') ? 'TS' : 'JS';
await create(extra.extra.split('-')[0], lang, process.cwd(), true);
if (Object.keys(extra).length < 1) process.exit(1);
const lang = extra.extra.includes('typescript') ? 'TS' : 'JS';
await create(extra.extra.split('-')[0], lang, process.cwd(), true);
}

View File

@@ -7,7 +7,10 @@ export const help = `
|___/\\___|_| |_| |_|
Welcome!
If you're new to ${cyanBright('sern')}, run ${magentaBright(
'sern init'
)} for an interactive setup to your new bot project!
If you're new to ${cyanBright('sern')}, run ${magentaBright('npm create @sern/bot')} for an interactive setup to your new bot project!
${green(`If you have any ideas, suggestions, bug reports, kindly join our support server: https://sern.dev/discord`)}`;
${green(
`If you have any ideas, suggestions, bug reports, kindly join our support server: https://sern.dev/discord`
)}`;

View File

@@ -1,9 +1,17 @@
import { greenBright, redBright, underline, yellowBright } from 'colorette';
import { greenBright, redBright, underline } from 'colorette';
import { execa } from 'execa';
import { findUp } from 'find-up';
import ora from 'ora';
import prompt from 'prompts';
import { cmds_dir, gitInit, lang, main_dir, name, skip_install_dep, which_manager } from '../prompts/init.js';
import {
cmds_dir,
gitInit,
lang,
main_dir,
name,
skip_install_dep,
which_manager,
} from '../prompts/init.js';
import { writeFile } from 'fs/promises';
import { editDirs, editMain } from '../utilities/edits.js';
@@ -11,113 +19,118 @@ import { cloneRepo, installDeps } from '../utilities/install.js';
import { npm } from '../utilities/npm.js';
import type { PackageManagerChoice } from '../utilities/types.js';
/** @deprecated Use npm create instead */
export async function init(flags: Flags) {
console.log(`${yellowBright('[WARN]:')} This command is deprecated, use ${greenBright('npm create @sern/bot')} instead`);
let data: PromptData;
let git_init = true; // the default;
let pm = flags.sync ? undefined : flags.y ? 'npm' : await npm();
let data: PromptData;
let git_init = true; // the default;
let pm = flags.sync ? undefined : flags.y ? 'npm' : await npm();
if (flags.y) {
const projectName = await prompt([name]);
git_init = true;
data = {
name: projectName.name,
lang: 'typescript',
main_dir: 'src',
cmds_dir: 'commands',
};
} else if (flags.sync) {
data = (await prompt([lang, main_dir, cmds_dir])) as PromptData;
} else {
data = (await prompt([name, lang, main_dir, cmds_dir])) as PromptData;
git_init = (await prompt([gitInit])).gitinit;
}
if (flags.y) {
const projectName = await prompt([name]);
git_init = true;
data = {
name: projectName.name,
lang: 'typescript',
main_dir: 'src',
cmds_dir: 'commands',
};
} else if (flags.sync) {
data = (await prompt([lang, main_dir, cmds_dir])) as PromptData;
} else {
data = (await prompt([name, lang, main_dir, cmds_dir])) as PromptData;
git_init = (await prompt([gitInit])).gitinit;
}
const language = data.lang === 'javascript-esm' ? 'javascript' : data.lang;
const config = {
language,
paths: {
base: data.main_dir,
commands: data.cmds_dir,
},
};
const file = JSON.stringify(config, null, 2);
const language = data.lang === 'javascript-esm' ? 'javascript' : data.lang;
const config = {
language,
paths: {
base: data.main_dir,
commands: data.cmds_dir,
},
};
const file = JSON.stringify(config, null, 2);
const requiredData = flags.sync !== undefined ? 3 : 4;
const receivedData = Object.keys(data).length;
const incompleteDataCondition = receivedData < requiredData;
const requiredData = flags.sync !== undefined ? 3 : 4;
const receivedData = Object.keys(data).length;
const incompleteDataCondition = receivedData < requiredData;
if (incompleteDataCondition) process.exit(1);
if (incompleteDataCondition) process.exit(1);
if (!flags.sync) await cloneRepo(data.lang, data.name);
if (!flags.sync) await cloneRepo(data.lang, data.name);
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + data.name,
});
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + data.name,
});
if (!pkg) throw new Error('No package.json found!');
if (!pkg) throw new Error('No package.json found!');
await writeFile(pkg.replace('package.json', 'sern.config.json'), file);
await writeFile(pkg.replace('package.json', 'sern.config.json'), file);
if (flags.sync) {
console.log('Project was successfully synced!');
process.exit(0);
}
if (flags.sync) {
console.log('Project was successfully synced!');
process.exit(0);
}
git_init ? await git(data) : console.log(`Skipping git init...\n`);
git_init ? await git(data) : console.log(`Skipping git init...\n`);
let choice: PackageManagerChoice;
let choice: PackageManagerChoice;
if (pm === 'both') {
choice = (await prompt([which_manager])).manager;
} else {
choice = (
(await prompt([skip_install_dep])).skip_install_dep ? pm : 'skip'
) as PackageManagerChoice;
}
if (pm === 'both') {
choice = (await prompt([which_manager])).manager;
} else {
choice = ((await prompt([skip_install_dep])).skip_install_dep ? pm : 'skip') as PackageManagerChoice;
}
await installDeps(choice, data.name);
await editMain(data.name);
await editDirs(data.main_dir, data.cmds_dir, data.name, data.lang);
await installDeps(choice, data.name);
await editMain(data.name);
await editDirs(data.main_dir, data.cmds_dir, data.name, data.lang);
console.log(`${greenBright('Success, project was initialised!')}`);
process.exit(0);
console.log(`${greenBright('Success, project was initialised!')}`);
process.exit(0);
}
/** It initializes git */
async function git(data: Data) {
const spin = ora({
text: 'Initializing git...',
spinner: 'aesthetic',
}).start();
const spin = ora({
text: 'Initializing git...',
spinner: 'aesthetic',
}).start();
try {
await execa('git', ['init', data.name]);
await wait(300);
spin.succeed('Git initialized!');
} catch (error) {
spin.fail(`${redBright('Failed')} to initialize git!\nTry to install it at ${underline('https://git-scm.com')}\nSkipping for now.`);
}
try {
await execa('git', ['init', data.name]);
await wait(300);
spin.succeed('Git initialized!');
} catch (error) {
spin.fail(
`${redBright(
'Failed'
)} to initialize git!\nTry to install it at ${underline(
'https://git-scm.com'
)}\nSkipping for now.`
);
}
}
/** Wait for a specified number of milliseconds, then return a promise that resolves to undefined. */
async function wait(ms: number) {
const wait = (await import('util')).promisify(setTimeout);
return wait(ms);
const wait = (await import('util')).promisify(setTimeout);
return wait(ms);
}
interface Data {
name: string;
name: string;
}
interface Flags {
y: boolean;
sync: boolean;
y: boolean;
sync: boolean;
}
interface PromptData {
name: string;
lang: 'typescript' | 'javascript' | 'javascript-esm';
main_dir: string;
cmds_dir: string;
name: string;
lang: 'typescript' | 'javascript' | 'javascript-esm';
main_dir: string;
cmds_dir: string;
}

View File

@@ -1,67 +0,0 @@
import { blueBright, bold, cyanBright, greenBright, italic, magentaBright, underline } from 'colorette';
import { getSern } from '../utilities/getSern';
import { readFileSync } from 'node:fs';
import type { CommandData, GuildId } from '../utilities/types';
export function list() {
const files = getSern();
if (!files.includes('command-data-remote.json')) {
console.error(`No commands found\nPlease run ${cyanBright('sern commands publish')} to publish your commands`);
process.exit(1);
}
const commands: Record<GuildId, CommandData[]> = JSON.parse(readFileSync('.sern/command-data-remote.json', 'utf-8'));
const globalCommands = commands.global;
delete commands.global;
console.log(bold('Global Commands'));
for (const command of globalCommands) log(command);
console.log('\t');
for (const guildId in commands) {
const guildCommands = commands[guildId];
console.log(`${bold('Guild Commands')} [${underline(cyanBright(guildId))}]`);
for (const command of guildCommands) log(command);
}
}
const AppCommandsType: Record<number, string> = {
1: magentaBright('Slash'),
2: magentaBright('User'),
3: magentaBright('Message'),
};
const AppCommandOptionType: Record<number, string> = {
1: magentaBright('SubCommand'),
2: magentaBright('SubCommand Group'),
3: magentaBright('String'),
4: magentaBright('Integer'),
5: magentaBright('Boolean'),
6: magentaBright('User'),
7: magentaBright('Channel'),
8: magentaBright('Role'),
9: magentaBright('Mentionable'),
10: magentaBright('Number'),
11: magentaBright('Attachment'),
};
function log(command: CommandData) {
console.log(clean(`\t${cyanBright(command.name)} ${italic(command.description)} (${greenBright(command.id)})`));
console.log(`\t Type: ${AppCommandsType[command.type]}`);
if (command.options) {
console.log(`\t Options:`);
for (const option of command.options) {
console.log(`\t ${blueBright(option.name)}: ${AppCommandOptionType[option.type]}`);
if (option.options) {
console.log(`\t Options:`);
for (const subOption of option.options) {
console.log(`\t ${cyanBright(subOption.name)}: ${AppCommandOptionType[subOption.type]}`);
}
}
}
}
}
const clean = (str: string) => str.split(' ').filter(Boolean).join(' ');

View File

@@ -1,71 +1,73 @@
import { greenBright } from 'colorette';
import fs from 'fs';
import prompt from 'prompts';
import { fetch } from 'undici';
import { fetch, type Response } from 'undici';
import { pluginsQ } from '../prompts/plugin.js';
import { fromCwd } from '../utilities/fromCwd.js';
import esbuild from 'esbuild';
import { getLang } from '../utilities/getLang.js';
import { resolve } from 'path';
import { require } from '../utilities/require.js';
interface PluginData {
description: string;
hash: string;
name: string;
author: string[];
link: string;
example: string;
version: '1.0.0';
}
/**
* Installs plugins to project
*/
export async function plugins() {
const e: PluginData[] = (await prompt([await pluginsQ()])).list;
function dispatchSave() {
}
function dispatchInstall() {
}
export async function plugins(options: PluginOptions) {
if(options.save) {
dispatchSave()
}
//Download instead based on names given. Must be a full filename ie: (publish)
if(options.name) {
const pluginSource = await downloa();
}
const e: string[] = (await prompt([await pluginsQ()])).list;
if (!e) process.exit(1);
const lang = await getLang();
for await (const plgData of e) {
const pluginText = await download(plgData.link);
const dir = fromCwd('/src/plugins');
const linkNoExtension = `${process.cwd()}/src/plugins/${plgData.name}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (lang === 'typescript') {
fs.writeFileSync(linkNoExtension + '.ts', pluginText);
} else {
const { type = undefined } = require(resolve('package.json'));
const format = type === undefined || type === 'cjs' ? 'cjs' : 'esm';
const transformResult = await esbuild.transform(pluginText, {
target: 'node18',
format,
loader: 'ts',
banner: `/**\n Partial information: ${plgData.description}\n @author ${plgData.author}\n @example${plgData.example}*/`,
});
if (transformResult.warnings.length > 0) {
console.log(transformResult.warnings.map((msg) => msg.text).join('\n'));
}
console.warn('transforming plugins with js strips comments');
console.warn('We provided some minimal information at top of file, or view the documentation for this plugin here:');
console.warn(plgData.link);
fs.writeFileSync(linkNoExtension + '.js', transformResult.code);
}
for await (const url of e) {
await download(url);
}
const pluginNames = e.map((e) => e.split('/').pop());
console.log(
`Successfully downloaded plugin(s):\n${greenBright(
pluginNames.join('\n')
)}`
);
}
const pluginNames = e.map((data) => {
return 'Installed ' + data.name + ' ' + 'from ' + data.author.join(',');
});
console.log(`Successfully downloaded plugin(s):\n${greenBright(pluginNames.join('\n'))}`);
async function downloa(url: string | URL) {
const formatText = (res: Response) => res.text()
return fetch(url, { method: 'GET' })
.then(formatText)
.catch(() => {
throw Error('Download failed! Kindly contact developers')
})
}
async function download(url: string) {
const data = await fetch(url, { method: 'GET' })
.then((res) => res.text())
.catch(() => null);
const data = await fetch(url, { method: 'GET' })
.then((res) => res.text())
.catch(() => null);
if (!data) throw new Error('Download failed! Kindly contact developers');
if (!data) throw new Error('Download failed! Kindly contact developers');
const dir = fromCwd('/src/plugins');
const filedir = `${process.cwd()}/src/plugins/${url.split('/').pop()}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filedir, data);
}
interface PluginOptions {
name?: string[];
save: boolean
return data;
}

View File

@@ -1,42 +0,0 @@
import { magentaBright } from 'colorette';
import { getConfig } from '../utilities/getConfig';
import { fork } from 'node:child_process';
import { fileURLToPath } from 'url';
export async function publish(commandDir: string | undefined, args: Partial<PublishArgs>) {
if (!args.suppressWarnings) {
console.info(`${magentaBright('EXPERIMENTAL')}: This API has not been stabilized. add -W or --suppress-warnings flag to suppress`);
}
const config = await getConfig();
// pass in args into the command.
const rootPath = new URL('../', import.meta.url),
publishScript = new URL('../dist/create-publish.js', rootPath);
// assign args.import to empty array if non existent
args.import ??= [];
args.token && console.info('token passed through command line');
args.applicationId && console.info('applicationId passed through command line');
commandDir && console.info('Publishing with override path: ', commandDir);
const dotenvLocation = new URL('../node_modules/dotenv/config.js', rootPath),
esmLoader = new URL('../node_modules/@esbuild-kit/esm-loader/dist/index.js', rootPath);
// We dynamically load the create-publish script in a child process so that we can pass the special
// loader flag to require typescript files
const command = fork(fileURLToPath(publishScript), [], {
execArgv: ['--loader', esmLoader.toString(), '-r', fileURLToPath(dotenvLocation), '--no-warnings'],
env: {
token: args.token ?? '',
applicationId: args.applicationId ?? '',
},
});
// send paths object so we dont have to recalculate it in script
command.send({ config, preloads: args.import, commandDir });
}
interface PublishArgs {
suppressWarnings: boolean;
import: string[];
token: string;
applicationId: string;
}

View File

@@ -1,17 +0,0 @@
export interface PublishableData {
name: string;
type: number;
description: string;
absPath: string;
options: Typeable[];
}
export interface Typeable {
type: number;
}
export interface Config {
guildIds?: string[];
}
export interface PublishableModule {
data: PublishableData;
config: Config;
}

View File

@@ -1,242 +0,0 @@
/**
* This file is meant to be run with the esm / cjs esbuild-kit loader to properly import typescript modules
*/
import { readdir, stat, mkdir, writeFile } from 'fs/promises';
import { join, basename, extname, resolve } from 'node:path';
import { pathExistsSync } from 'find-up';
import assert from 'assert';
import { once } from 'node:events';
import * as Rest from './rest';
import type { sernConfig } from './utilities/getConfig';
import type { PublishableData, PublishableModule, Typeable } from './create-publish.d.ts';
import { cyanBright, greenBright, redBright } from 'colorette';
import ora from 'ora';
import type { TheoreticalEnv } from './types/config';
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
}
function isSkippable(filename: string) {
// empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
}
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
// TODO: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
yield 'file:///' + fullPath;
}
}
}
} catch (err) {
throw err;
}
}
// recieved sern config
const [{ config, preloads, commandDir }] = await once(process, 'message'),
{ paths } = config as sernConfig;
for (const preload of preloads) {
await import('file:///' + resolve(preload));
}
const commandsPath = commandDir ? resolve(commandDir) : resolve(paths.base, paths.commands);
const filePaths = readPaths(commandsPath, true);
const modules = [];
const PUBLISHABLE = 0b1110;
for await (const absPath of filePaths) {
let mod = await import(absPath);
let commandModule = mod.default;
let config = mod.config;
if ('default' in commandModule) {
commandModule = commandModule.default;
}
if (typeof config === 'function') {
config = config(absPath, commandModule);
}
try {
commandModule = commandModule.getInstance();
} catch {}
if ((PUBLISHABLE & commandModule.type) != 0) {
// assign defaults
const filename = basename(absPath);
const filenameNoExtension = filename.substring(0, filename.lastIndexOf('.'));
commandModule.name ??= filenameNoExtension;
commandModule.description ??= '';
commandModule.absPath = absPath;
modules.push({ commandModule, config });
}
}
const cacheDir = resolve('./.sern');
if (!pathExistsSync(cacheDir)) {
// TODO: add this in verbose flag
// console.log('Making .sern directory: ', cacheDir);
await mkdir(cacheDir);
}
const optionsTransformer = (ops: Array<Typeable>) => {
return ops.map((el) => {
if ('command' in el) {
const { command, ...rest } = el;
return rest;
}
return el;
});
};
const intoApplicationType = (type: number) => {
if (type === 3) {
return 1;
}
return Math.log2(type);
};
const makeDescription = (type: number, desc: string) => {
if (type !== 1 && desc !== '') {
console.warn('Found context menu that has non empty description field. Implictly publishing with empty description');
return '';
}
return desc;
};
const makePublishData = ({ commandModule, config }: Record<string, Record<string, unknown>>) => {
const applicationType = intoApplicationType(commandModule.type as number);
return {
data: {
name: commandModule.name as string,
type: applicationType,
description: makeDescription(applicationType, commandModule.description as string),
absPath: commandModule.absPath as string,
options: optionsTransformer((commandModule?.options ?? []) as Typeable[]),
dm_permission: config?.dmPermission,
default_member_permissions: config?.defaultMemberPermissions ?? null,
},
config,
};
};
// We can use these objects to publish to DAPI
const publishableData = modules.map(makePublishData),
token = process.env.token || process.env.DISCORD_TOKEN,
appid = process.env.applicationId || process.env.APPLICATION_ID;
assert(token, 'Could not find a token for this bot in .env or commandline. Do you have DISCORD_TOKEN in env?');
assert(appid, 'Could not find an application id for this bot in .env or commandline. Do you have APPLICATION_ID in env?');
// partition globally published and guilded commands
const [globalCommands, guildedCommands] = publishableData.reduce(
([globals, guilded], module) => {
const isPublishableGlobally = !module.config || !Array.isArray(module.config.guildIds);
if (isPublishableGlobally) {
return [[module, ...globals], guilded];
}
return [globals, [module, ...guilded]];
},
[[], []] as [PublishableModule[], PublishableModule[]]
);
const spin = ora(`Publishing ${cyanBright('Global')} commands`);
globalCommands.length && spin.start();
const rest = Rest.create(appid, token);
const res = await rest.updateGlobal(globalCommands);
let globalCommandsResponse: unknown;
if (res.ok) {
spin.succeed(`All ${cyanBright('Global')} commands published`);
globalCommandsResponse = await res.json();
} else {
spin.fail(`Failed to publish global commands [Code: ${redBright(res.status)}]`);
if (res.status === 429) {
throw Error('Chill out homie, too many requests');
}
console.error(
'errors:',
await res.json().then((res) => {
const errors = Object.values(res.errors);
// @ts-ignore
return errors.map((err) => err?.name?._errors);
})
);
console.error(res.statusText);
}
function associateGuildIdsWithData(data: PublishableModule[]): Map<string, PublishableData[]> {
const guildIdMap: Map<string, PublishableData[]> = new Map();
data.forEach((entry) => {
const { data, config } = entry;
const { guildIds } = config || {};
if (guildIds) {
guildIds.forEach((guildId) => {
if (guildIdMap.has(guildId)) {
guildIdMap.get(guildId)?.push(data);
} else {
guildIdMap.set(guildId, [data]);
}
});
}
});
return guildIdMap;
}
const guildCommandMap = associateGuildIdsWithData(guildedCommands);
let guildCommandMapResponse = new Map<string, Record<string, unknown>>();
for (const [guildId, array] of guildCommandMap.entries()) {
const spin = ora(`[${cyanBright(guildId)}] Updating commands for guild`);
spin.start();
const response = await rest.putGuildCommands(guildId, array);
const result = await response.json();
if (response.ok) {
guildCommandMapResponse.set(guildId, result);
spin.succeed(`[${greenBright(guildId)}] Successfully updated commands for guild`);
} else {
spin.fail(`[${redBright(guildId)}] Failed to update commands for guild, Reason: ${result.message}`);
throw Error(result.message);
}
}
const remoteData = {
global: globalCommandsResponse,
...Object.fromEntries(guildCommandMapResponse),
};
await writeFile(resolve(cacheDir, 'command-data-remote.json'), JSON.stringify(remoteData, null, 4), 'utf8');
// TODO: add this in a verbose flag
// console.info('View json output in ' + resolve(cacheDir, 'command-data-remote.json'));
process.exit(0);

View File

@@ -1,64 +1,40 @@
#!/usr/bin/env node
import { extra } from './commands/extra.js';
import { help } from './commands/help.js';
import { init } from './commands/init.js';
import { Command } from 'commander';
import { yellowBright } from 'colorette';
import { plugins } from './commands/plugins.js';
export const program = new Command();
const importDynamic = async <T extends string>(filename: T) => import(`./commands/${filename}` as const);
declare const __VERSION__: string;
const version: string = '[VI]{{inject}}[/VI]';
program
.name('sern')
.description(await importDynamic('help.js').then((m) => m.help))
.version(`sern CLI v${__VERSION__}`, '-v, --version')
.exitOverride(() => process.exit(0));
.name('sern')
.description(help)
.version(`sern CLI v${version}`)
.exitOverride(() => process.exit(0));
program
.command('init')
.description(`Quickest way to scaffold a new project ${yellowBright('[DEPRECATED]')}`)
.option('-y', 'Finishes setup as default')
.option('-s, --sync', 'Syncs the project and generates sern.config.json')
.action(async (...args) => importDynamic('init.js').then((m) => m.init(...args)));
.command(init.name)
.description('Quickest way to scaffold a new project')
.option('-y', 'Finishes setup as default')
.option('-s, --sync', 'Syncs the project and generates sern.config.json')
.action(init);
program
.command('plugins')
.description('Install plugins from https://github.com/sern-handler/awesome-plugins')
.option('-n --name', 'Name of plugin')
.action((...args) => importDynamic('plugins.js').then((m) => m.plugins(...args)));
program
.command('extra')
.description('Easy way to add extra things in your sern project')
.action((...args) => importDynamic('extra.js').then((m) => m.extra(...args)));
program //
.command('commands')
.description('Defacto way to manage your slash commands')
.addCommand(
new Command('publish')
.description('New way to manage your slash commands')
.option('-W --suppress-warnings', 'suppress experimental warning')
.option('-i, --import [scriptPath...]', 'Prerequire a script to load into publisher')
.option('-t, --token [token]')
.option('--appId [applicationId]')
.argument('[path]', 'path with respect to current working directory that will locate all published files')
.action(async (...args) => importDynamic('publish.js').then((m) => m.publish(...args)))
const pluginCommand = program.command(plugins.name)
pluginCommand
.description(
'Get plugins from https://github.com/sern-handler/awesome-plugins'
)
.addCommand(
new Command('list') //
.description('List all slash commands')
.action(async (...args) => importDynamic('list.js').then((m) => m.list(...args)))
);
.option('-n --name <string...>', 'Name(s) of plugin to install')
.option('-S --save', 'Save and keep plugins updated')
.action(plugins);
program
.command('build')
.description('Build your bot')
.option('-f --format [fmt]', 'The module system of your application. `cjs` or `esm`', 'esm')
.option('-m --mode [mode]', 'the mode for sern to build in. `production` or `development`', 'development')
.option('-W --suppress-warnings', 'suppress experimental warning')
.option('-p --project [filePath]', 'build with this sern.build file')
.option('-e --env', 'path to .env file')
.option('--tsconfig [filePath]', 'Use this tsconfig')
.action(async (...args) => importDynamic('build.js').then((m) => m.build(...args)));
.command(extra.name)
.description('Easy way to add extra things in your sern project')
.action(extra);
program.parse();

View File

@@ -1,39 +0,0 @@
import fs from 'fs/promises';
import path from 'node:path';
import { require } from '../utilities/require.js';
import { type Plugin } from 'esbuild';
import { basename } from 'node:path';
export const validExtensions = ['.ts', '.js', '.json', '.png', '.jpg', '.jpeg', '.webp'];
//https://github.com/evanw/esbuild/issues/1051
export const imageLoader = {
name: 'attachment-loader',
setup: (b) => {
const filter = new RegExp(`\.${validExtensions.slice(3).join('|')}$`);
b.onResolve({ filter }, (args) => {
//if the module is being imported, resolve the path and transform to the js stub
if (args.importer) {
const newPath = path
.format({ ...path.parse(args.path), base: '', ext: '.js' })
.split(path.sep)
.join(path.posix.sep);
return { path: newPath, namespace: 'attachment-loader', external: true };
}
// if the file is actually the attachment, resolve the full dir
return { path: require.resolve(args.path, { paths: [args.resolveDir] }), namespace: 'attachment-loader' };
});
b.onLoad({ filter: /.*/, namespace: 'attachment-loader' }, async (args) => {
const base64 = await fs.readFile(args.path).then((s) => s.toString('base64'));
return {
contents: `
var __toBuffer = (base64) => Buffer.from(base64, "base64");
module.exports = {
name: '${basename(args.path)}',
attachment: __toBuffer("${base64}")
}`,
};
});
},
} satisfies Plugin;

View File

@@ -1,20 +1,20 @@
import type { PromptObject } from 'prompts';
export const extraPrompt: PromptObject = {
message: 'What extra feature do you want to add?',
name: 'extra',
type: 'select',
choices: [
{
title: 'Dockerfile (TypeScript)',
description: 'Dockerfile for TypeScript',
value: 'Dockerfile-typescript',
selected: true,
},
{
title: 'Dockerfile (JavaScript)',
description: 'Dockerfile for JavaScript',
value: 'Dockerfile-javascript',
},
],
message: 'What extra feature do you want to add?',
name: 'extra',
type: 'select',
choices: [
{
title: 'Dockerfile (TypeScript)',
description: 'Dockerfile for TypeScript',
value: 'Dockerfile-typescript',
selected: true,
},
{
title: 'Dockerfile (JavaScript)',
description: 'Dockerfile for JavaScript',
value: 'Dockerfile-javascript',
},
],
};

View File

@@ -2,91 +2,95 @@ import { blueBright } from 'colorette';
import type { PromptObject } from 'prompts';
export const lang: PromptObject = {
message: 'What language do you want the project to be in?',
name: 'lang',
type: 'select',
choices: [
{
title: 'JavaScript',
description: 'JS',
value: 'javascript',
},
{
title: 'JavaScript (ESM)',
description: 'JS',
value: 'javascript-esm',
},
{
title: 'TypeScript',
description: 'TS - (Recommended)',
value: 'typescript',
},
],
message: 'What language do you want the project to be in?',
name: 'lang',
type: 'select',
choices: [
{
title: 'JavaScript',
description: 'JS',
value: 'javascript',
},
{
title: 'JavaScript (ESM)',
description: 'JS',
value: 'javascript-esm',
},
{
title: 'TypeScript',
description: 'TS - (Recommended)',
value: 'typescript',
},
],
};
export const main_dir: PromptObject = {
message: 'What is the main directory of your project?',
name: 'main_dir',
type: 'text',
initial: 'src',
message: 'What is the main directory of your project?',
name: 'main_dir',
type: 'text',
initial: 'src',
};
export const cmds_dir: PromptObject = {
message: 'What is the directory of your commands?',
name: 'cmds_dir',
type: 'text',
initial: 'commands',
validate: (dir: string) => (dir === 'src' ? 'You can not use src as a directory' : true),
message: 'What is the directory of your commands?',
name: 'cmds_dir',
type: 'text',
initial: 'commands',
validate: (dir: string) =>
dir === 'src' ? 'You can not use src as a directory' : true,
};
export const npmInit: PromptObject = {
name: 'npm_init',
type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize npm?`,
initial: true,
name: 'npm_init',
type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize npm?`,
initial: true,
};
export const gitInit: PromptObject = {
name: 'gitinit',
type: 'confirm',
message: `Do you want to ${blueBright('me')} to initialize git?`,
initial: true,
name: 'gitinit',
type: 'confirm',
message: `Do you want to ${blueBright('me')} to initialize git?`,
initial: true,
};
export const which_manager: PromptObject = {
message: `Which manager do you want to use?`,
name: 'manager',
type: 'select',
choices: [
{
title: 'NPM',
description: 'Default Package Manager',
selected: true,
value: 'npm',
},
{
title: 'Yarn',
description: 'Yarn Package Manager',
value: 'yarn',
},
{
title: 'Skip',
description: 'Skip selection',
value: 'skip',
},
],
message: `Which manager do you want to use?`,
name: 'manager',
type: 'select',
choices: [
{
title: 'NPM',
description: 'Default Package Manager',
selected: true,
value: 'npm',
},
{
title: 'Yarn',
description: 'Yarn Package Manager',
value: 'yarn',
},
{
title: 'Skip',
description: 'Skip selection',
value: 'skip',
},
],
};
export const skip_install_dep: PromptObject = {
name: 'skip_install_dep',
type: 'confirm',
message: `Do you want ${blueBright('me')} to install dependencies?`,
initial: false,
name: 'skip_install_dep',
type: 'confirm',
message: `Do you want ${blueBright('me')} to install dependencies?`,
initial: false,
};
export const name: PromptObject = {
message: 'What is your project name?',
name: 'name',
type: 'text',
validate: (name: string) => (name.match('^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$') ? true : 'Invalid name'),
message: 'What is your project name?',
name: 'name',
type: 'text',
validate: (name: string) =>
name.match('^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$')
? true
: 'Invalid name',
};

View File

@@ -1,31 +1,45 @@
import type { Choice, PromptObject } from 'prompts';
import { fetch } from 'undici';
import { getLang } from '../utilities/getLang.js';
function upperCase(string: string | null) {
if (string === null) {
console.error('Lang property not found!');
process.exit(0);
}
return string === 'typescript' ? 'TypeScript' : 'JavaScript';
}
async function gimmechoices(): Promise<Choice[]> {
const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`;
const lang = upperCase(await getLang().catch(() => null));
const resp = await fetch(link).catch(() => null);
if (!resp) return [{ title: 'No plugins found!', value: '', disabled: true }];
const data = (await resp.json()) as Data[];
const choices = data.map((e) => ({
title: e.name,
value: e,
}));
const link = `https://api.github.com/repos/sern-handler/awesome-plugins/contents/${lang}`;
return choices;
const resp = await fetch(link).catch(() => null);
if (!resp)
return [{ title: 'No plugins found!', value: '', disabled: true }];
const data = (await resp.json()) as Data[];
const choices = data.map((e) => ({
title: e.name,
value: e.download_url,
}));
return choices;
}
export async function pluginsQ(): Promise<PromptObject> {
return {
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: await gimmechoices(),
min: 1,
};
return {
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: await gimmechoices(),
min: 1,
};
}
interface Data {
name: string;
link: string;
name: string;
download_url: string;
}

View File

@@ -1,41 +0,0 @@
import type { PublishableModule } from './create-publish.d.ts';
const baseURL = new URL('https://discord.com/api/v10/applications/');
const excludedKeys = new Set(['command', 'absPath']);
const publishablesIntoJson = (ps: PublishableModule[]) =>
JSON.stringify(
ps.map((module) => module.data),
(key, value) => (excludedKeys.has(key) ? undefined : value),
4
);
export const create = (appid: string, token: string) => {
const globalURL = new URL(`${appid}/commands`, baseURL);
const headers = {
Authorization: 'Bot ' + token,
'Content-Type': 'application/json',
};
return {
updateGlobal: (commands: PublishableModule[]) =>
fetch(globalURL, {
method: 'PUT',
body: publishablesIntoJson(commands),
headers,
}),
getGuildCommands: (id: string) => {
const guildCommandURL = new URL(`${appid}/guilds/${id}/commands`, baseURL);
return fetch(guildCommandURL, { headers });
},
putGuildCommands: (guildId: string, guildCommand: unknown) => {
const guildCommandURL = new URL(`${appid}/guilds/${guildId}/commands`, baseURL);
return fetch(guildCommandURL, {
method: 'PUT',
body: JSON.stringify(guildCommand),
headers,
});
},
};
};

19
src/types/config.d.ts vendored
View File

@@ -1,19 +0,0 @@
export interface sernConfig {
language: 'typescript' | 'javascript';
paths: {
base: string;
commands: string;
};
scripts?: {
prepublish?: string;
};
buildPath: string;
rest?: Record<string, Record<string, unknown>>;
}
export interface TheoreticalEnv {
DISCORD_TOKEN: string;
APPLICATION_ID: string;
MODE: 'PROD' | 'DEV';
[name: string]: string;
}

View File

@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath, URL } from 'url';
const root = new URL('../../', import.meta.url);
const templates = new URL('./templates/', root);
const extraURL = new URL('./extra/', templates);
const extraFolder = fileURLToPath(extraURL);
@@ -13,13 +14,19 @@ const extraFolder = fileURLToPath(extraURL);
* @param location - The location of the file to be created.
* @param no_ext - If true, the file will be created without an extension.
*/
export async function create(
name: string,
lang: string,
location: string,
no_ext: boolean
) {
const file = `${name}.${lang}.sern`;
export async function create(name: string, lang: string, location: string, no_ext: boolean) {
const file = `${name}.${lang}.sern`;
const target = no_ext
? `${location}/${name}`
: `${location}/${name}.${lang}`;
const target = no_ext ? `${location}/${name}` : `${location}/${name}.${lang}`;
return createFile(file, target);
return createFile(file, target);
}
/**
@@ -28,11 +35,11 @@ export async function create(name: string, lang: string, location: string, no_ex
* @param target - The location of the file to be created.
*/
async function createFile(template: string, target: string) {
const location = `${extraFolder}${template}`;
const location = `${extraFolder}${template}`;
const file = await readFile(location, 'utf8');
const file = await readFile(location, 'utf8');
await writeFileRecursive(target, file);
await writeFileRecursive(target, file);
}
/**
@@ -42,10 +49,10 @@ async function createFile(template: string, target: string) {
* @returns A promise that resolves to the result of the writeFile function.
*/
async function writeFileRecursive(target: string, data: string) {
const resolvedTarget = resolve(target);
const dir = dirname(resolvedTarget);
const resolvedTarget = resolve(target);
const dir = dirname(resolvedTarget);
await mkdir(dir, { recursive: true });
await mkdir(dir, { recursive: true });
return writeFile(resolvedTarget, data);
return writeFile(resolvedTarget, data);
}

View File

@@ -1,12 +0,0 @@
import type esbuild from 'esbuild';
import { resolve } from 'path';
export default (format: 'cjs' | 'esm', tsconfigRaw: unknown) =>
({
platform: 'node',
format,
tsconfigRaw: tsconfigRaw as esbuild.TsconfigRaw,
logLevel: 'info',
minify: false,
outdir: resolve('dist'),
} satisfies esbuild.BuildOptions);

View File

@@ -8,16 +8,16 @@ import { fromCwd } from './fromCwd.js';
* @param name - The name of the project.
*/
export async function editMain(name: string) {
const pjLocation = (await findUp('package.json', {
cwd: fromCwd('/' + name),
})) as string;
const pjLocation = (await findUp('package.json', {
cwd: fromCwd('/' + name),
})) as string;
const output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read your package.json.");
const output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read your package.json.");
output.name = name;
output.name = name;
return writeFile(pjLocation, JSON.stringify(output, null, 2));
return writeFile(pjLocation, JSON.stringify(output, null, 2));
}
/**
@@ -29,52 +29,55 @@ export async function editMain(name: string) {
* @param lang - The language you want to use.
*/
export async function editDirs(
srcName: string,
cmds_dirName: string,
name: string,
lang: 'javascript' | 'typescript' | 'javascript-esm' = 'typescript'
srcName: string,
cmds_dirName: string,
name: string,
lang: 'javascript' | 'typescript' | 'javascript-esm' = 'typescript'
) {
const path = (await findUp('src', {
cwd: fromCwd(name),
type: 'directory',
})) as string;
const path = (await findUp('src', {
cwd: fromCwd(name),
type: 'directory',
})) as string;
const ext = lang === 'typescript' ? 'ts' : 'js';
const ext = lang === 'typescript' ? 'ts' : 'js';
const newMainDir = path?.replace('src', srcName);
await rename(path, newMainDir);
const newMainDir = path?.replace('src', srcName);
await rename(path, newMainDir);
const cmdsPath = (await findUp('commands', {
cwd: fromCwd(name, srcName),
type: 'directory',
})) as string;
const cmdsPath = (await findUp('commands', {
cwd: fromCwd(name, srcName),
type: 'directory',
})) as string;
const index = (await findUp(`index.${ext}`, {
cwd: fromCwd(name, srcName),
})) as string;
const index = (await findUp(`index.${ext}`, {
cwd: fromCwd(name, srcName),
})) as string;
const newCmdsPath = cmdsPath?.replace('commands', cmds_dirName);
await rename(cmdsPath, newCmdsPath);
const newCmdsPath = cmdsPath?.replace('commands', cmds_dirName);
await rename(cmdsPath, newCmdsPath);
const tsconfig = await findUp('tsconfig.json', {
cwd: process.cwd() + '/' + name,
});
const tsconfig = await findUp('tsconfig.json', {
cwd: process.cwd() + '/' + name,
});
if (tsconfig) {
const output = JSON.parse(await readFile(tsconfig, 'utf8'));
if (!output) throw new Error("Can't read your tsconfig.json.");
output.compilerOptions.rootDir = srcName;
if (tsconfig) {
const output = JSON.parse(await readFile(tsconfig, 'utf8'));
if (!output) throw new Error("Can't read your tsconfig.json.");
output.compilerOptions.rootDir = srcName;
await writeFile(tsconfig, JSON.stringify(output, null, 2));
}
await writeFile(tsconfig, JSON.stringify(output, null, 2));
}
const output = await readFile(index, 'utf8');
const output = await readFile(index, 'utf8');
const oldfold = ext === 'ts' ? 'dist' : 'src';
const newfold = ext === 'ts' ? 'dist' : srcName;
const oldfold = ext === 'ts' ? 'dist' : 'src';
const newfold = ext === 'ts' ? 'dist' : srcName;
const regex = new RegExp(`commands: '${oldfold}/commands'`);
const edit = output.replace(regex, `commands: '${newfold}/${cmds_dirName}'`);
const regex = new RegExp(`commands: '${oldfold}/commands'`);
const edit = output.replace(
regex,
`commands: '${newfold}/${cmds_dirName}'`
);
return writeFile(index, edit);
return writeFile(index, edit);
}

View File

@@ -1,5 +1,5 @@
import path from 'path';
export function fromCwd(...dir: string[]) {
return path.join(...[process.cwd(), ...dir]);
return path.join(...[process.cwd(), ...dir]);
}

View File

@@ -1,24 +0,0 @@
import { readFile } from 'node:fs/promises';
import { findUp } from 'find-up';
import assert from 'node:assert';
export async function getConfig(): Promise<sernConfig> {
const sernLocation = await findUp('sern.config.json');
assert(sernLocation, "Can't find sern.config.json");
const output = JSON.parse(await readFile(sernLocation, 'utf8')) as sernConfig;
assert(output, "Can't read your sern.config.json.");
return output;
}
export interface sernConfig {
language: 'typescript' | 'javascript';
defaultPrefix?: string;
paths: {
base: string;
commands: string;
events?: string;
};
buildPath: string;
}

View File

@@ -6,13 +6,13 @@ import { readFile } from 'node:fs/promises';
* @returns The language of the project.
*/
export async function getLang(): Promise<'typescript' | 'javascript'> {
const sernLocation = await findUp('sern.config.json');
const sernLocation = await findUp('sern.config.json');
if (!sernLocation) throw new Error("Can't find sern.config.json");
if (!sernLocation) throw new Error("Can't find sern.config.json");
const output = JSON.parse(await readFile(sernLocation, 'utf8'));
const output = JSON.parse(await readFile(sernLocation, 'utf8'));
if (!output) throw new Error("Can't read your sern.config.json.");
if (!output) throw new Error("Can't read your sern.config.json.");
return output.language;
return output.language;
}

View File

@@ -1,17 +0,0 @@
import { readdirSync } from 'node:fs';
import { fromCwd } from './fromCwd';
import { redBright, cyanBright } from 'colorette';
export function getSern() {
let files: string[] = [];
try {
const sern = fromCwd('.sern');
files = readdirSync(sern);
} catch (error) {
console.error(`${redBright('Error:')} Could not locate ${cyanBright('.sern')} directory`);
process.exit(1);
} finally {
return files;
}
}

View File

@@ -13,34 +13,34 @@ import type { PackageManagerChoice } from './types';
* @param name - The name of the project
*/
export async function installDeps(choice: PackageManagerChoice, name: string) {
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + name,
});
if (!pkg) throw new Error('No package.json found!');
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + name,
});
if (!pkg) throw new Error('No package.json found!');
const output = JSON.parse(await readFile(pkg, 'utf8'));
if (!output) throw new Error("Can't read file.");
const output = JSON.parse(await readFile(pkg, 'utf8'));
if (!output) throw new Error("Can't read file.");
const deps = output.dependencies;
if (!deps) throw new Error("Can't find dependencies.");
const deps = output.dependencies;
if (!deps) throw new Error("Can't find dependencies.");
if (choice === 'skip') {
return console.log('Dependency installation skipped...');
}
if (choice === 'skip') {
return console.log('Dependency installation skipped...');
}
const spin = ora({
text: `Installing dependencies...`,
spinner: 'aesthetic',
}).start();
const spin = ora({
text: `Installing dependencies...`,
spinner: 'aesthetic',
}).start();
const result = await execa(choice, ['install'], {
cwd: process.cwd() + '/' + name,
}).catch(() => null);
const result = await execa(choice, ['install'], {
cwd: process.cwd() + '/' + name,
}).catch(() => null);
if (!result || result?.failed) {
spin.fail(`${redBright('Failed')} to install dependencies!`);
process.exit(1);
} else spin.succeed(`Dependencies installed!`);
if (!result || result?.failed) {
spin.fail(`${redBright('Failed')} to install dependencies!`);
process.exit(1);
} else spin.succeed(`Dependencies installed!`);
}
/**
@@ -49,14 +49,21 @@ export async function installDeps(choice: PackageManagerChoice, name: string) {
* @param name - The name of the project
*/
export async function cloneRepo(lang: string, name: string) {
try {
await execa('git', ['clone', `https://github.com/sern-handler/templates.git`]);
copyRecursiveSync(`templates/templates/${lang}`, name);
fs.rmSync(`templates/`, { recursive: true, force: true });
} catch (error) {
console.log(`${redBright('✖ Failed')} to clone github templates repo. Install git and try again!`);
process.exit(1);
}
try {
await execa('git', [
'clone',
`https://github.com/sern-handler/templates.git`,
]);
copyRecursiveSync(`templates/templates/${lang}`, name);
fs.rmSync(`templates/`, { recursive: true, force: true });
} catch (error) {
console.log(
`${redBright(
'✖ Failed'
)} to clone github templates repo. Install git and try again!`
);
process.exit(1);
}
}
/**
@@ -67,18 +74,21 @@ export async function cloneRepo(lang: string, name: string) {
* @param dest - The destination folder where the files will be copied to.
*/
export function copyRecursiveSync(src: string, dest: string) {
const exists = fs.existsSync(src);
const exists = fs.existsSync(src);
const stats = (exists && fs.statSync(src)) as fs.Stats;
const stats = (exists && fs.statSync(src)) as fs.Stats;
const isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
const isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(
path.join(src, childItemName),
path.join(dest, childItemName)
);
});
} else {
fs.copyFileSync(src, dest);
}
}

View File

@@ -5,21 +5,21 @@ import { execa } from 'execa';
* @returns A promise that resolves to a string.
*/
export async function npm() {
const npm = await execa('npm', ['-v']).catch(() => null);
const npm_version = npm?.stdout;
const npm = await execa('npm', ['-v']).catch(() => null);
const npm_version = npm?.stdout;
const yarn = await execa('yarn', ['-v']).catch(() => null);
const yarn_version = yarn?.stdout;
const yarn = await execa('yarn', ['-v']).catch(() => null);
const yarn_version = yarn?.stdout;
if (npm_version && !yarn_version) {
return 'npm';
}
if (npm_version && !yarn_version) {
return 'npm';
}
if (!npm_version && yarn_version) {
return 'yarn';
}
if (!npm_version && yarn_version) {
return 'yarn';
}
if (npm_version && yarn_version) {
return 'both';
}
if (npm_version && yarn_version) {
return 'both';
}
}

View File

@@ -1,90 +0,0 @@
const declareConstType = (name: string, type: string) => String.raw`declare var ${name}: ${type}`;
const processEnvType = (env: NodeJS.ProcessEnv) => {
const entries = Object.keys(env);
const envBuilder = new StringWriter();
for (const key of entries) {
envBuilder.tab();
envBuilder.tab();
envBuilder.envField(key);
}
return envBuilder.build();
};
const determineJSONType = (s: string) => {
return typeof JSON.parse(s);
};
type FileWriter = (path: string, content: string, format: BufferEncoding) => Promise<void>;
const writeAmbientFile = async (path: string, define: Record<string, string>, writeFile: FileWriter) => {
const fileContent = new StringWriter();
for (const [k, v] of Object.entries(define)) {
fileContent.varDecl(k, v);
}
fileContent
.println('declare namespace NodeJS {')
.tab()
.println('interface ProcessEnv {')
.envFields(process.env)
.tab()
.println('}')
.println('}');
await writeFile(path, fileContent.build(), 'utf8');
};
const writeTsConfig = async (format: 'cjs' | 'esm', configPath: string, fw: FileWriter) => {
//maybe better way to do this
const target = format === 'esm' ? { target: 'esnext' } : {};
const sernTsConfig = {
compilerOptions: {
moduleResolution: 'node',
strict: true,
skipLibCheck: true,
...target,
rootDirs: ['./generated', '../src'],
},
include: ['./ambient.d.ts', '../src'],
};
await fw(configPath, JSON.stringify(sernTsConfig, null, 3), 'utf8');
};
class StringWriter {
private fileString = '';
tab() {
this.fileString += ' ';
return this;
}
varDecl(name: string, type: string) {
this.fileString += declareConstType(name, determineJSONType(type)) + '\n';
return this;
}
println(data: string) {
this.fileString += data + '\n';
return this;
}
envField(key: string) {
if (/\s|\(|\)/g.test(key)) {
this.fileString += `"${key}": string`;
} else {
this.fileString += key + ':' + 'string';
}
this.fileString += '\n';
return this;
}
envFields(env: NodeJS.ProcessEnv) {
this.fileString += processEnvType(env);
return this;
}
build() {
return this.fileString;
}
}
export { writeAmbientFile, writeTsConfig };

View File

@@ -1,41 +0,0 @@
import { readdir, stat } from 'fs/promises';
import { basename, join, extname } from 'path';
function isSkippable(filename: string) {
//empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
}
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
}
export async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
//Todo: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
yield 'file:///' + fullPath;
}
}
}
} catch (err) {
throw err;
}
}

View File

@@ -1,3 +0,0 @@
import { createRequire } from 'node:module';
export const require = createRequire(import.meta.url);

View File

@@ -1,35 +1,10 @@
export type PackageManagerChoice = 'skip' | 'npm' | 'yarn';
export type GuildId = string;
export interface Config {
language: string;
paths : {
base: string;
commands: string;
}
export interface CommandData {
id: string;
application_id: string;
version: string;
default_member_permissions?: string;
type: number;
name: string;
name_localizations?: Record<string, string>;
description: string;
description_localizations?: Record<string, string>;
dm_permission: boolean;
guild_id: string;
nsfw: boolean;
options?: OptionData[];
}
interface OptionData {
type: number;
name: string;
name_localizations?: Record<string, string>;
description: string;
description_localizations?: Record<string, string>;
required?: boolean;
choices?: ChoiceData[];
options?: OptionData[];
}
interface ChoiceData {
name: string;
value: string | number;
}

6
src/utilities/version.ts Normal file
View File

@@ -0,0 +1,6 @@
import { requiree } from "..";
export function version() {
const { version: v } = requiree('../../package.json');
return `@sern/cli v${v}`;
}

View File

@@ -8,4 +8,4 @@ RUN npm install
COPY . .
CMD node src/index.js
RUN node src/index.js

View File

@@ -6,10 +6,8 @@ COPY package.json ./
RUN npm install
RUN npm install -D typescript
COPY . .
RUN tsc --build || npx -p typescript tsc --build
RUN tsc --build
CMD node dist/index.js
RUN node dist/index.js

View File

@@ -1,18 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"declarationMap": true,
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true,
"importsNotUsedAsValues": "error",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

View File

@@ -1,25 +1,19 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
import { defineConfig } from 'tsup'
import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
const shared = {
entry: ['src/index.ts', 'src/create-publish.mts', 'src/commands/**', 'sern-tsconfig.json'],
entry: ['src/index.ts'],
platform: 'node',
clean: true,
sourcemap: true,
};
export default defineConfig({
format: 'esm',
target: 'node18',
tsconfig: './tsconfig.json',
outDir: './dist',
treeshake: true,
bundle: true,
esbuildPlugins: [],
platform: 'node',
splitting: true,
define: {
__VERSION__: `"${createRequire(import.meta.url)('./package.json').version}"`,
},
loader: {
'.json': 'file',
},
...shared,
});
export default defineConfig(
{
format: 'esm',
target: 'node16',
tsconfig: './tsconfig.json',
outDir: './dist',
treeshake: true,
esbuildPlugins: [esbuildPluginVersionInjector()],
...shared,
}
)