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
48 changed files with 5485 additions and 6106 deletions

View File

@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org/'

View File

@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'

View File

@@ -11,25 +11,25 @@ on:
- main
jobs:
# Lint:
# name: Linting
# runs-on: ubuntu-latest
Lint:
name: Linting
runs-on: ubuntu-latest
# steps:
# - name: Check out Git repository
# uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
steps:
- name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
# - name: Set up Node.js
# uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
# with:
# node-version: 17
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
# Prettier must be in `package.json`
# - name: Install Node.js dependencies
# run: npm i
- name: Install Node.js dependencies
run: npm i
# - name: Run Prettier
# run: npm run format
- name: Run Prettier
run: npm run format
Test:
name: Testing
@@ -40,9 +40,9 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 20
node-version: 17
- name: Install Node.js dependencies
run: npm i && npm run build

View File

@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'

View File

@@ -14,7 +14,7 @@ jobs:
bump-patch-for-minor-pre-major: true
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
if: ${{ steps.release.outputs.release_created }}
- uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'

View File

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

View File

@@ -2,152 +2,6 @@
All notable changes to this project will be documented in this file.
## [1.4.0](https://github.com/sern-handler/cli/compare/v1.3.5...v1.4.0) (2025-02-07)
### Features
* watch command execute on successful build ([#151](https://github.com/sern-handler/cli/issues/151)) ([1695956](https://github.com/sern-handler/cli/commit/1695956350a55bb4a28839b427d1bf46cf518b01))
## [1.3.5](https://github.com/sern-handler/cli/compare/v1.3.4...v1.3.5) (2025-01-29)
### Bug Fixes
* Update Dockerfile.TS.sern ([#149](https://github.com/sern-handler/cli/issues/149)) ([fca3c76](https://github.com/sern-handler/cli/commit/fca3c7601604215af9b6d66a137370064ae656e1))
## [1.3.4](https://github.com/sern-handler/cli/compare/v1.3.3...v1.3.4) (2025-01-22)
### Bug Fixes
* tsconfig generator ([#145](https://github.com/sern-handler/cli/issues/145)) ([23d70c6](https://github.com/sern-handler/cli/commit/23d70c6e03860d1a69e3bffedd6910aba82071ab))
## [1.3.3](https://github.com/sern-handler/cli/compare/v1.3.2...v1.3.3) (2024-08-07)
### Bug Fixes
* bunpnpminstall ([9c2d6d7](https://github.com/sern-handler/cli/commit/9c2d6d7389c41a071ab86570cebf987b02bbf450))
## [1.3.2](https://github.com/sern-handler/cli/compare/v1.3.1...v1.3.2) (2024-07-31)
### Bug Fixes
* make tsconfig work with comments/trailing commas ([#142](https://github.com/sern-handler/cli/issues/142)) ([53ca446](https://github.com/sern-handler/cli/commit/53ca446a38076ed7b165dbbb41f346f028b7e394))
## [1.3.1](https://github.com/sern-handler/cli/compare/v1.3.0...v1.3.1) (2024-07-27)
### Miscellaneous Chores
* release 1.3.1 ([2bb169f](https://github.com/sern-handler/cli/commit/2bb169f600172af8c8316b6c9420684a4de34e0d))
## [1.3.0](https://github.com/sern-handler/cli/compare/v1.2.1...v1.3.0) (2024-07-27)
### Features
* plugin, no prompt, bug fixes ([#139](https://github.com/sern-handler/cli/issues/139)) ([20364fa](https://github.com/sern-handler/cli/commit/20364fa20d1f3bf70a1c0cfefbc1d6c9365a4925))
### Bug Fixes
* fix publish command for bun & pnpm ([#137](https://github.com/sern-handler/cli/issues/137)) ([d581142](https://github.com/sern-handler/cli/commit/d581142f082ed888036e58aa33e9d88d84d34c2f))
## [1.2.1](https://github.com/sern-handler/cli/compare/v1.2.0...v1.2.1) (2024-02-08)
### Bug Fixes
* better error messages for publish ([d1832e4](https://github.com/sern-handler/cli/commit/d1832e44ce1b10aeb5b9dc902b7d35ab51c41ff3))
## [1.2.0](https://github.com/sern-handler/cli/compare/v1.1.0...v1.2.0) (2024-01-29)
### Features
* make application id optional, thanks [@trueharuu](https://github.com/trueharuu) ([#130](https://github.com/sern-handler/cli/issues/130)) ([2d6e65a](https://github.com/sern-handler/cli/commit/2d6e65a1e6073f605aab192b8cea33037a04af2c))
## [1.1.0](https://github.com/sern-handler/cli/compare/v1.0.3...v1.1.0) (2024-01-28)
### Features
* command clear ([#128](https://github.com/sern-handler/cli/issues/128)) ([303ac02](https://github.com/sern-handler/cli/commit/303ac0280c7c7c55f2670d49c9685b911670bc05))
## [1.0.3](https://github.com/sern-handler/cli/compare/v1.0.2...v1.0.3) (2023-12-25)
### Bug Fixes
* intellisense for esm build ts ([#126](https://github.com/sern-handler/cli/issues/126)) ([a5cb668](https://github.com/sern-handler/cli/commit/a5cb66828eae47d3eac5b86c7e67c01dbed500d5))
## [1.0.2](https://github.com/sern-handler/cli/compare/v1.0.1...v1.0.2) (2023-11-04)
### Bug Fixes
* better error handling ([5dbf2a8](https://github.com/sern-handler/cli/commit/5dbf2a87dcb0001993fe126d9002bc82c8108e24))
* build mkdir errors ([#122](https://github.com/sern-handler/cli/issues/122)) ([f1d7d6c](https://github.com/sern-handler/cli/commit/f1d7d6c911bc54ed3ca3e39eefbc7de6ee33b10d))
## [1.0.1](https://github.com/sern-handler/cli/compare/v1.0.0...v1.0.1) (2023-10-05)
### Bug Fixes
* multiple bugs ([#119](https://github.com/sern-handler/cli/issues/119)) ([d9ca5ff](https://github.com/sern-handler/cli/commit/d9ca5ff3ff2f304a6d75f2b7a296bd900431d41f))
## [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

@@ -3,11 +3,11 @@
</div>
# Features
- Manage discord application commands from the command line.
- Install plugins from the community.
- Really fast startup times (I think).
- Deploy with premade docker configurations.
- Inhouse build tool based on esbuild built for sern applications, nearly **zero** config.
😁 **User Friendly** <br>
💦 **Simple** <br>
🌱 **Efficient** <br>
💪 **Powerful** <br>
## Installation
@@ -30,54 +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.
## Development
```sh
git clone https://github.com/sern-handler/cli.git
```
## insall i
```sh
npm i
```
## build it
```sh
npm run build
```
## make it usable globally
- if sern is installed globally already, you may need to uninstall it.
```sh
npm link
```
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.4.1",
"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,246 +0,0 @@
import esbuild from 'esbuild';
import { getConfig } from '../utilities/getConfig';
import p from 'node:path';
import { glob } from 'glob';
import { configDotenv } from 'dotenv';
import assert from 'node:assert';
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';
import { parseTsConfig } from '../utilities/parseTsconfig';
import { execa, type ExecaChildProcess } from 'execa';
import { InvalidArgumentError } from 'commander';
const VALID_EXTENSIONS = ['.ts', '.js' ];
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';
/**
* 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;
/**
* flag: default false
*/
sourcemap?: boolean;
watch?: {
/**
* command to run.
* defaults to your package
* manager's start command.
*/
command?: string;
}
};
const CommandHandlerPlugin = (buildConfig: Partial<BuildOptions>, ambientFilePath: string, sernTsConfigPath: string) => {
return {
name: "commandHandler",
setup(build) {
const options = build.initialOptions
const defVersion = () => JSON.stringify(require(p.resolve('package.json')).version);
// should fix a type error
options.define = {
...(buildConfig.define ?? {}),
__DEV__: `${buildConfig.mode === 'development'}`,
__PROD__: `${buildConfig.mode === 'production'}`,
__VERSION__: `${buildConfig.defineVersion ? `${defVersion()}` : 'undefined'}`
}
Preprocessor.writeTsConfig(buildConfig.format!, sernTsConfigPath, writeFile);
Preprocessor.writeAmbientFile(ambientFilePath, options.define!, writeFile);
}
} as esbuild.Plugin
}
const CommandOnEndPlugin = (watching: boolean, watchCommand?: string) => {
// for some reason it runs the command twice on first build
let isFirstBuild = true;
let currentProcess: ExecaChildProcess | null = null;
let restartTimeout: NodeJS.Timeout | null = null;
return {
name: 'watchRunCommand',
setup(build: esbuild.PluginBuild) {
build.onEnd(async (result) => {
if (!watching || result.errors.length !== 0) return;
if (isFirstBuild === true) {
isFirstBuild = false;
return;
}
if (watchCommand === '') {
console.log('[watch] no command provided, skipping');
return;
}
if (currentProcess) {
console.log('[watch] stopping previous process...');
currentProcess.cancel()
currentProcess = null;
}
const cmd = watchCommand || (() => {
if (pathExistsSync('package-lock.json')) return 'npm start';
if (pathExistsSync('yarn.lock')) return 'yarn start';
if (pathExistsSync('pnpm-lock.yaml')) return 'pnpm start';
if (pathExistsSync('bun.lockb')) return 'bun start';
if (pathExistsSync('bun.lock')) return 'bun start';
throw new Error('[watch] default package manager start command not found');
})();
// Clear any pending restart
if (restartTimeout) clearTimeout(restartTimeout);
console.log('[watch] debouncing command for 1.5 seconds...');
// Set new debounced timeout
restartTimeout = setTimeout(() => {
console.log(`[watch] running command: ${cmd}`);
currentProcess = execa(cmd, { stdio: 'inherit', shell: true });
currentProcess.catch(error => {
if (error.isCanceled) return;
console.error(`[watch] command execution error: ${error.message}`);
});
}, 1500);
});
}
} as esbuild.Plugin;
};
const resolveBuildConfig = (path: string | undefined, language: string) => {
if (language === 'javascript') {
return path ?? 'jsconfig.json'
}
return path ?? 'tsconfig.json'
}
export async function build(options: Record<string, any>) {
//console.log(options)
if (!options.supressWarnings) {
console.info(`${magentaBright('EXPERIMENTAL')}: This API has not been stabilized. add -W or --suppress-warnings flag to suppress`);
}
// if watch was not enabled and watchCommand is present
if(!options.watch && options.watchCommand) {
throw new InvalidArgumentError("enable watch to use --watch-command")
}
const sernConfig = await getConfig();
let buildConfig: BuildOptions;
const buildConfigPath = p.resolve(options.project ?? 'sern.build.js');
const defaultBuildConfig = {
defineVersion: true,
format: options.format ?? 'esm',
mode: options.mode ?? 'development',
dropLabels: [],
sourcemap: options.sourceMaps,
tsconfig: resolveBuildConfig(options.tsconfig, sernConfig.language),
env: options.env ?? '.env',
include: [],
watch: {
command: options.watchCommand
}
};
// merging configuration with sern.build.js, if exists buildConfigPath
if (pathExistsSync(buildConfigPath)) {
let fileConfig;
try { fileConfig=await import('file:///' + buildConfigPath).then(r=>r.default) }
catch(e) {
console.error("Could not find buildConfigPath")
throw e;
}
//throwable, buildConfigPath may not exist, todo, merge
buildConfig = { ...defaultBuildConfig, ...fileConfig };
} else {
buildConfig = defaultBuildConfig;
console.log('No build config found, defaulting');
}
configDotenv({ path: buildConfig.env });
if (process.env.NODE_ENV) {
buildConfig.mode = process.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', 'NODE_ENV is not `production` or `development`');
try {
let config = await parseTsConfig(buildConfig.tsconfig!);
config?.extends && console.warn("Extend the generated tsconfig")
} catch(e) {
console.error("no tsconfig / jsconfig found");
console.error(`Please create a ${sernConfig.language === 'javascript' ? 'jsconfig.json' : 'tsconfig.json' }`);
console.error('It should have at least extend the generated one sern makes.\n \
{ "extends": "./.sern/tsconfig.json" }');
throw e;
}
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);
console.log(' ', magentaBright('sourceMaps'), buildConfig.sourcemap);
const sernDir = p.resolve('.sern'),
[ambientFilePath, sernTsConfigPath, genDir] = // resolves the file paths in the .sern dir
['ambient.d.ts', 'tsconfig.json', 'generated'].map(f => p.resolve(sernDir, f));
if (!(await pathExists(genDir))) {
console.log('Making .sern/generated dir, does not exist');
await mkdir(genDir, { recursive: true });
}
const entryPoints = await glob(`src/**/*{${VALID_EXTENSIONS.join(',')}}`,{
ignore: {
ignored: (p) => p.name.endsWith('.d.ts'),
}
});
//https://esbuild.github.io/content-types/#tsconfig-json
const ctx = await esbuild.context({
entryPoints,
plugins: [
CommandHandlerPlugin(buildConfig, ambientFilePath, sernTsConfigPath),
CommandOnEndPlugin(options.watch, buildConfig.watch?.command)
],
sourcemap: buildConfig.sourcemap,
...defaultEsbuild(buildConfig.format!, buildConfig.tsconfig),
dropLabels: [buildConfig.mode === 'production' ? '__DEV__' : '__PROD__', ...buildConfig.dropLabels!],
});
await ctx.rebuild()
if (options.watch) {
await ctx.watch()
} else {
await ctx.dispose()
}
}

View File

@@ -1,60 +0,0 @@
import * as Rest from '../rest.js'
import assert from 'node:assert'
import dotenv from 'dotenv'
import ora from 'ora';
import type { CommandData, GuildId } from '../utilities/types.js';
import { readFileSync, writeFile } from 'node:fs'
import { resolve } from 'node:path'
import prompts from 'prompts';
const getConfirmation = (args: Record<string,any> ) => {
if(args.yes) {
return args.yes
} else {
return prompts({
type: 'confirm',
name: 'confirmation',
message: 'Are you sure you want to delete ALL your application commands?',
initial: true
}, { onCancel: () => (console.log("Cancelled operation ( ̄┰ ̄*)"), process.exit(1)) })
.then(response => response.confirmation);
}
}
export async function commandClear(args: Record<string,any>) {
dotenv.configDotenv({ path: args.env || resolve('.env') })
const token = process.env.token || process.env.DISCORD_TOKEN;
assert(token, 'Could not find a token for this bot in .env or commandline. Do you have DISCORD_TOKEN in env?');
const confirmation = await getConfirmation(args);
if (confirmation) {
const spin = ora({
text: `Deleting ALL application commands...`,
spinner: 'aesthetic',
}).start();
const rest = await Rest.create(token);
let guildCommands: Record<GuildId, CommandData[]>
try {
guildCommands = JSON.parse(readFileSync('.sern/command-data-remote.json', 'utf-8'))
await rest.updateGlobal([]);
delete guildCommands.global
for (const guildId in guildCommands) {
await rest.putGuildCommands(guildId, []);
}
writeFile('.sern/command-data-remote.json', "{}", (err) => {
if(err) {
spin.fail("Error happened while writing to json:");
console.error(err)
process.exit(1)
}
})
spin.succeed();
} catch(e) {
spin.fail("Something went wrong. ");
throw e;
}
} else {
console.log('Operation canceled. ( ̄┰ ̄*)');
}
}

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,69 +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;
if(globalCommands) {
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,104 +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 { getConfig } from '../utilities/getConfig.js';
import type { PromptObject } from 'prompts';
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: string;
}
const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`;
export async function fetchPluginData(): Promise<PluginData[]> {
return fetch(link)
.then(res => res.json())
.then(data => (data as PluginData[]))
.catch(() => [])
}
export function pluginsQ(choices: PluginData[]): PromptObject[] {
return [{
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: choices.map(e => ({ title: e.name, value: e })),
min: 1,
}];
}
/**
* Installs plugins to project
*/
export async function plugins(args: string[], opts: Record<string, unknown>) {
const plugins = await fetchPluginData();
let selectedPlugins : PluginData[];
if(args.length) {
const normalizedArgs = args.map(str => str.toLowerCase())
console.log("Trying to find plugins to install...");
const results = plugins.reduce((acc, cur) => {
if(normalizedArgs.includes(cur.name.toLowerCase())) {
return [...acc, cur]
}
return acc;
}, [] as PluginData[]);
selectedPlugins = results;
} else {
selectedPlugins = (await prompt(pluginsQ(plugins))).list;
}
if (!selectedPlugins.length) {
process.exit(1);
}
const { language } = await getConfig();
for await (const plgData of selectedPlugins) {
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 (language === '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);
}
}
const pluginNames = selectedPlugins.map((data) => {
return 'Installed ' + data.name + ' ' + 'from ' + data.author.join(',');
});
console.log(`Successfully downloaded plugin(s):\n${greenBright(pluginNames.join('\n'))}`);
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);
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')
)}`
);
}
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,40 +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 ??= [];
commandDir && console.info('Publishing with override path: ', commandDir);
const isBunOrPnpm = rootPath.pathname.includes('.bun') || rootPath.pathname.includes('.pnpm');
const esmLoader = new URL(`${isBunOrPnpm ? '../../' : '../'}node_modules/@esbuild-kit/esm-loader/dist/index.js`, rootPath);
await import('dotenv/config');
// 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(), '--no-warnings'],
});
// 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,265 +0,0 @@
/**
* This file is meant to be run with the esm / cjs esbuild-kit loader to properly import typescript modules
*/
import { readdir, mkdir, writeFile } from 'fs/promises';
import { basename, resolve, posix as pathpsx } 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 { inspect } from 'node:util'
import ora from 'ora';
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
const files = await readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = pathpsx.join(dir, file.name);
if (file.isDirectory()) {
if (!file.name.startsWith('!')) {
yield* readPaths(fullPath, shouldDebug);
}
} else if (!file.name.startsWith('!')) {
yield "file:///"+resolve(fullPath);
}
}
}
// recieved sern config
const [{ config, preloads, commandDir }] = await once(process, 'message'),
{ paths } = config as SernConfig;
for (const preload of preloads) {
console.log("preloading: ", preload);
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 ((PUBLISHABLE & commandModule.type) != 0) {
// assign defaults
const filename = basename(absPath);
const filenameNoExtension = filename.substring(0, filename.lastIndexOf('.'));
commandModule.name ??= filenameNoExtension;
commandModule.description ??= '';
commandModule.meta = {
absPath
}
commandModule.absPath = absPath;
if (typeof config === 'function') {
config = config(absPath, commandModule);
}
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 serialize = (permissions: unknown) => {
if(typeof permissions === 'bigint' || typeof permissions === 'number') {
return permissions.toString();
}
if(Array.isArray(permissions)) {
return permissions
.reduce((acc, cur) => acc | cur, BigInt(0))
.toString()
}
return null;
}
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 ?? false,
default_member_permissions: serialize(config?.defaultMemberPermissions),
//@ts-ignore
integration_types: (config?.integrationTypes ?? ['Guild']).map(
(s: string) => {
if(s === "Guild") {
return "0"
} else if (s == "User") {
return "1"
} else {
throw Error("IntegrationType is not one of Guild (0) or User (1)");
}
}),
//@ts-ignore
contexts: config?.contexts ?? undefined,
name_localizations: commandModule.name_localizations,
description_localizations: commandModule.description_localizations
},
config,
};
};
// We can use these objects to publish to DAPI
const publishableData = modules.map(makePublishData),
token = process.env.token || process.env.DISCORD_TOKEN;
assert(token, 'Could not find a token for this bot in .env or commandline. Do you have DISCORD_TOKEN 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 = await Rest.create(token);
const res = await rest.updateGlobal(globalCommands);
let globalCommandsResponse: unknown;
if (res.ok) {
globalCommands.length && spin.succeed(`All ${cyanBright('Global')} commands published`);
globalCommandsResponse = await res.json();
} else {
spin.fail(`Failed to publish global commands [Code: ${redBright(res.status)}]`);
let err: Error
console.error("Status Text ", res.statusText);
switch(res.status) {
case 400 : {
const validation_errors = await res.json()
console.error('errors:', inspect(validation_errors, { depth: Infinity }));
console.error("Modules with validation errors:"
+ inspect(Object.keys(validation_errors.errors).map(idx => globalCommands[idx as any])))
throw Error("400: Ensure your commands have proper fields and data with nothing left out");
}
case 404 : {
console.error('errors:', inspect(await res.json(), { depth: Infinity }));
throw Error("Forbidden 404. Is you application id and/or token correct?")
}
case 429: {
console.error('errors:', inspect(await res.json(), { depth: Infinity }));
err = Error('Chill out homie, too many requests')
} break;
default: {
console.error('errors:', inspect(await res.json(), { depth: Infinity }));
throw Error(res.status.toString() + " error")
}
}
}
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}`);
switch(response.status) {
case 400 : {
console.error(inspect(result, { depth: Infinity }))
console.error("Modules with validation errors:"
+ inspect(Object.keys(result.errors).map(idx => array[idx as any])))
throw Error("400: Ensure your commands have proper fields and data and nothing left out");
}
case 404 : {
console.error(inspect(result, { depth: Infinity }))
throw Error("Forbidden 404. Is you application id and/or token correct?")
}
case 429: {
console.error(inspect(result, { depth: Infinity }))
throw Error('Chill out homie, too many requests')
}
}
}
}
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,69 +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);
const pluginCommand = program.command(plugins.name)
pluginCommand
.description(
'Get plugins from https://github.com/sern-handler/awesome-plugins'
)
.option('-n --name <string...>', 'Name(s) of plugin to install')
.option('-S --save', 'Save and keep plugins updated')
.action(plugins);
program
.command('plugins')
.description('Install plugins from https://github.com/sern-handler/awesome-plugins')
.argument('[names...]', 'Names of plugins to install')
.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]')
.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)))
).addCommand(
new Command('list') //
.description('List all slash commands')
.action(async (...args) => importDynamic('list.js').then((m) => m.list(...args))))
.addCommand(
new Command('clear')
.description('Clear and reset commands-data-remote.json and the api')
.option('-y, --yes', "Say yes to all prompts")
.option('-e, --env [path]', "Supply a path to a .env")
.action(async (...args) => importDynamic('command-clear.js').then((m) => m.commandClear(...args))));
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 --watch')
.option('--watch-command [cmd]', 'the command for sern to watch. if watch is not enabled, an error is thrown', '')
.option('-W --suppress-warnings', 'suppress experimental warning')
.option('-p --project [filePath]', 'build with the provided sern.build file')
.option('-e --env', 'path to .env file')
.option('--source-maps', 'Whether to add source-maps to configuration', false)
.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,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 ${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',
};

45
src/prompts/plugin.ts Normal file
View File

@@ -0,0 +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 lang = upperCase(await getLang().catch(() => null));
const link = `https://api.github.com/repos/sern-handler/awesome-plugins/contents/${lang}`;
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,
};
}
interface Data {
name: string;
download_url: string;
}

View File

@@ -1,51 +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[]) => {
const s = JSON.stringify(
ps.map((module) => module.data),
(key, value) => (excludedKeys.has(key) ? undefined : value), 4);
return s;
}
export const create = async (token: string) => {
const headers = {
Authorization: 'Bot ' + token,
'Content-Type': 'application/json',
};
let me;
let appid: string;
try {
me = await fetch(new URL('@me', baseURL), { headers }).then(res => res.json());
appid = me.id;
} catch(e) {
console.log("Something went wrong while trying to fetch your application:");
throw e;
}
const globalURL = new URL(`${appid}/commands`, baseURL);
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,
});
},
};
};

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

@@ -1,16 +0,0 @@
export interface sernConfig {
language: 'typescript' | 'javascript';
paths: {
base: string;
commands: string;
};
buildPath: string;
rest?: Record<string, Record<string, unknown>>;
}
export interface TheoreticalEnv {
DISCORD_TOKEN: string;
APPLICATION_ID?: string;
MODE: 'production' | 'environment';
[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,11 +0,0 @@
import type esbuild from 'esbuild';
import { resolve } from 'path';
export default (format: 'cjs' | 'esm', tsconfig: string|undefined, outdir='dist') => ({
platform: 'node',
format,
tsconfig,
logLevel: 'info',
minify: false,
outdir: resolve(outdir),
} satisfies esbuild.BuildOptions);

View File

@@ -1,7 +1,6 @@
import { findUp } from 'find-up';
import { readFile, rename, writeFile } from 'node:fs/promises';
import { fromCwd } from './fromCwd.js';
import { parseTsConfig } from './parseTsconfig.js';
/**
* It takes a string, finds the package.json file in the directory of the string, and changes the name
@@ -9,16 +8,16 @@ import { parseTsConfig } from './parseTsconfig.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));
}
/**
@@ -30,54 +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 = await parseTsConfig(tsconfig);
if (!output) throw new Error("Can't read your tsconfig.json.");
if (!output.compilerOptions) throw new Error("Can't find compilerOptions in 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;
// This will strip comments/trailing commas from the tsconfig.json file
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,27 +0,0 @@
import { findUp } from 'find-up';
import { readFile } from 'node:fs/promises';
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;
};
build?: {
}
}

18
src/utilities/getLang.ts Normal file
View File

@@ -0,0 +1,18 @@
import { findUp } from 'find-up';
import { readFile } from 'node:fs/promises';
/**
* It finds the sern.config.json file, reads it, and returns the language property
* @returns The language of the project.
*/
export async function getLang(): Promise<'typescript' | 'javascript'> {
const sernLocation = await findUp('sern.config.json');
if (!sernLocation) throw new Error("Can't find sern.config.json");
const output = JSON.parse(await readFile(sernLocation, 'utf8'));
if (!output) throw new Error("Can't read your sern.config.json.");
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,50 +0,0 @@
import { resolve } from 'node:path';
import { readFile } from 'node:fs/promises';
interface CompilerOptions {
target: string;
module: string;
lib: string[];
allowJs: boolean;
checkJs: boolean;
jsx: string;
declaration: boolean;
sourceMap: boolean;
outDir: string;
rootDir: string;
strict: boolean;
esModuleInterop: boolean;
forceConsistentCasingInFileNames: boolean;
noEmit: boolean;
importHelpers: boolean;
isolatedModules: boolean;
moduleResolution: string;
resolveJsonModule: boolean;
noEmitHelpers: boolean;
}
interface TsConfig {
compilerOptions: CompilerOptions;
files: string[];
include: string[];
exclude: string[];
extends: string;
}
const cleanJson = (json: string) =>
json
.replace(/\/\/.*$/gm, '')
.replace(/\/\*[\s\S]*?\*\//gm, '')
.replace(/,\s*([}\]])/g, '$1');
export const parseTsConfig = async (path: string) => {
const absPath = resolve(path);
const fileContent = await readFile(absPath, 'utf-8');
const cleanContent = cleanJson(fileContent);
try {
return JSON.parse(cleanContent) as Partial<TsConfig>;
} catch (e) {
return null;
}
};

View File

@@ -1,92 +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()
.tab()
.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 sernTsConfig = {
compilerOptions: {
//module determines top level await. CJS doesn't have that abliity afaik
module: format === 'cjs' ? 'node' : 'esnext',
moduleResolution: 'node',
strict: true,
skipLibCheck: true,
target: 'esnext',
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 env field has space or parens, wrap key in ""
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,92 +0,0 @@
import { readdir, stat } from 'fs/promises';
import { basename, join, parse, dirname } from 'path';
import assert from 'assert';
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
* commonjs, javascript :
* ```js
* exports = commandModule({ })
*
* //or
* exports.default = commandModule({ })
* ```
* esm javascript, typescript, and commonjs typescript
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let fileModule = await import(absPath);
let commandModule = fileModule.default;
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in commandModule ) {
commandModule = commandModule.default;
}
return { module: commandModule } as T;
}
export const fmtFileName = (fileName: string) => parse(fileName).name;
export const getfilename = (path: string) => fmtFileName(basename(path));
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return { fullPath,
fileStats: await stat(fullPath),
base: basename(file) };
}
function parseWildcardName(filename: string): string | null {
const wildcardMatch = filename.match(/\[(.*?)\]/);
return wildcardMatch ? wildcardMatch[1] : null;
}
export class RouteEntry {
public import_path: string
public filename: string
public parent: string
public wildcardName: string | null;
constructor(public route: string) {
this.import_path = "file:///"+route
this.filename = getfilename(this.route)
this.parent = dirname(this.route);
this.wildcardName = parseWildcardName(this.filename);
}
}
export interface ReadPathsConfig {
dir: string
onDir?: (dir: string) => Promise<boolean>|boolean
onEntry?: (etry: RouteEntry) => RouteEntry
}
export async function* readPaths(
config: ReadPathsConfig
): AsyncGenerator<RouteEntry> {
const files = await readdir(config.dir);
for (const file of files) {
const { fullPath, fileStats } = await deriveFileInfo(config.dir, file);
if (fileStats.isDirectory()) {
if(config.onDir && await config.onDir(fullPath)) {
yield* readPaths({ ...config, dir: fullPath });
} else {
yield* readPaths({ ...config, dir: fullPath });
}
} else {
const nowindowsPath = fullPath.replace(/\\/g, '/');
if(config.onEntry) {
yield config.onEntry(new RouteEntry(nowindowsPath));
} else {
yield new RouteEntry(nowindowsPath);
}
}
}
}

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

@@ -1,117 +0,0 @@
// i got this idea from chooks22
"use modules";
import {
InteractionResponseFlags,
InteractionType,
verifyKey,
InteractionResponseType
} from 'discord-interactions';
//this will import all the modules statically
class JsonResponse extends Response {
constructor(body, init) {
const jsonBody = JSON.stringify(body);
init = init || {
headers: {
'content-type': 'application/json;charset=UTF-8',
},
};
super(jsonBody, init);
}
}
function createContext(rawcontext) {
return rawcontext
}
async function executeModule(
emitter,
logger,
errHandler,
{ module, task, args },
) {
try {
await module.execute(args);
//emitter.emit('module.activate', /*resultPayload(PayloadType.Success, module)*/);
} catch(e) {
throw e
}
}
async function applyPlugins(module, payload) {
let success = true;
for (const plg of module.onEvent) {
const res = await plg.execute(payload);
if(!res.isOk()) {
success = false;
}
}
return success;
}
const router = Router();
/**
* A simple :wave: hello page to verify the worker is working.
*/
router.get('/', (request, env) => {
return new Response(`👋 ${env.DISCORD_APPLICATION_ID}`);
});
/**
* Main route for all requests sent from Discord. All incoming messages will
* include a JSON payload described here:
* https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object
*/
router.post('/', async (request, env) => {
const { isValid, interaction } = await server.verifyDiscordRequest(
request,
env,
);
if (!isValid || !interaction) {
return new Response('Bad request signature.', { status: 401 });
}
if (interaction.type === InteractionType.PING) {
// The `PING` message is used during the initial webhook handshake, and is
// required to configure the webhook in the developer portal.
return new JsonResponse({
type: InteractionResponseType.PONG,
});
}
if(interaction.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {
"use autocomplete";
} else if (interaction.type === InteractionType.APPLICATION_COMMAND) {
"use slash";
}
console.error('Unknown Type');
return new JsonResponse({ error: 'Unknown Type' }, { status: 400 });
});
router.all('*', () => new Response('Not Found.', { status: 404 }));
async function verifyDiscordRequest(request, env) {
const signature = request.headers.get('x-signature-ed25519');
const timestamp = request.headers.get('x-signature-timestamp');
const body = await request.text();
const isValidRequest =
signature &&
timestamp &&
verifyKey(body, signature, timestamp, env.DISCORD_PUBLIC_KEY);
if (!isValidRequest) {
return { isValid: false };
}
return { interaction: JSON.parse(body), isValid: true };
}
const server = {
verifyDiscordRequest: verifyDiscordRequest,
fetch: async function (request, env) {
return router.handle(request, env);
},
};
export default server;

View File

@@ -1,7 +1,5 @@
FROM node:latest
RUN npm install -g @sern/cli
WORKDIR /app
COPY package.json ./
@@ -10,6 +8,4 @@ RUN npm install
COPY . .
RUN sern build
CMD node .
RUN node src/index.js

View File

@@ -1,7 +1,5 @@
FROM node:latest
RUN npm install -g @sern/cli typescript@latest
WORKDIR /app
COPY package.json ./
@@ -10,6 +8,6 @@ RUN npm install
COPY . .
RUN sern 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,27 +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/**',
],
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}"`,
},
...shared,
});
export default defineConfig(
{
format: 'esm',
target: 'node16',
tsconfig: './tsconfig.json',
outDir: './dist',
treeshake: true,
esbuildPlugins: [esbuildPluginVersionInjector()],
...shared,
}
)