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 uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with: with:
node-version: 17 node-version: 17
registry-url: 'https://registry.npmjs.org/' registry-url: 'https://registry.npmjs.org/'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,152 +2,6 @@
All notable changes to this project will be documented in this file. 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) ## [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> </div>
# Features # Features
- Manage discord application commands from the command line.
- Install plugins from the community. 😁 **User Friendly** <br>
- Really fast startup times (I think). 💦 **Simple** <br>
- Deploy with premade docker configurations. 🌱 **Efficient** <br>
- Inhouse build tool based on esbuild built for sern applications, nearly **zero** config. 💪 **Powerful** <br>
## Installation ## Installation
@@ -30,54 +30,29 @@ When you install the CLI, you can use our commands with **sern** prefix.
``` ```
Usage: sern [options] [command] 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!
___ ___ _ __ _ __ If you have any ideas, suggestions, bug reports, kindly join our support server: https://discord.gg/xzK5fUKT4r
/ __|/ _ \ '__| '_ \
\__ \ __/ | | | | |
|___/\___|_| |_| |_|
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
Options: Options:
-v, --version output the version number -V, --version output the version number
-h, --help display help for command -h, --help display help for command
Commands: 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 plugins [options] Install plugins from https://github.com/sern-handler/awesome-plugins
extra Easy way to add extra things in your sern project 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 help [command] display help for command
``` ```
## Setting Up Your Project ## 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 ## 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> 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 plugins. Run `sern plugins` to see all installable options
## 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
```

8968
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,17 +1,17 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "group:allNonMajor"], "extends": ["config:base"],
"major": { "major": {
"dependencyDashboardApproval": true "dependencyDashboardApproval": true
}, },
"schedule": ["every weekend"], "schedule": ["every weekend"],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true "enabled": true
}, },
"packageRules": [ "packageRules": [
{ {
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/" "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'; import { create } from '../utilities/create.js';
export async function extra() { export async function extra() {
const extra = await prompt([extraPrompt]); const extra = await prompt([extraPrompt]);
if (Object.keys(extra).length < 1) process.exit(1); if (Object.keys(extra).length < 1) process.exit(1);
const lang = extra.extra.includes('typescript') ? 'TS' : 'JS'; const lang = extra.extra.includes('typescript') ? 'TS' : 'JS';
await create(extra.extra.split('-')[0], lang, process.cwd(), true);
await create(extra.extra.split('-')[0], lang, process.cwd(), true);
} }

View File

@@ -7,7 +7,10 @@ export const help = `
|___/\\___|_| |_| |_| |___/\\___|_| |_| |_|
Welcome! 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 { execa } from 'execa';
import { findUp } from 'find-up'; import { findUp } from 'find-up';
import ora from 'ora'; import ora from 'ora';
import prompt from 'prompts'; 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 { writeFile } from 'fs/promises';
import { editDirs, editMain } from '../utilities/edits.js'; 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 { npm } from '../utilities/npm.js';
import type { PackageManagerChoice } from '../utilities/types.js'; import type { PackageManagerChoice } from '../utilities/types.js';
/** @deprecated Use npm create instead */
export async function init(flags: Flags) { 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; if (flags.y) {
let git_init = true; // the default; const projectName = await prompt([name]);
let pm = flags.sync ? undefined : flags.y ? 'npm' : await npm(); 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 language = data.lang === 'javascript-esm' ? 'javascript' : data.lang;
const projectName = await prompt([name]); const config = {
git_init = true; language,
data = { paths: {
name: projectName.name, base: data.main_dir,
lang: 'typescript', commands: data.cmds_dir,
main_dir: 'src', },
cmds_dir: 'commands', };
}; const file = JSON.stringify(config, null, 2);
} 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 requiredData = flags.sync !== undefined ? 3 : 4;
const config = { const receivedData = Object.keys(data).length;
language, const incompleteDataCondition = receivedData < requiredData;
paths: {
base: data.main_dir,
commands: data.cmds_dir,
},
};
const file = JSON.stringify(config, null, 2);
const requiredData = flags.sync !== undefined ? 3 : 4; if (incompleteDataCondition) process.exit(1);
const receivedData = Object.keys(data).length;
const incompleteDataCondition = receivedData < requiredData;
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', { if (!pkg) throw new Error('No package.json found!');
cwd: process.cwd() + '/' + data.name,
});
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) { git_init ? await git(data) : console.log(`Skipping git init...\n`);
console.log('Project was successfully synced!');
process.exit(0);
}
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') { await installDeps(choice, data.name);
choice = (await prompt([which_manager])).manager; await editMain(data.name);
} else { await editDirs(data.main_dir, data.cmds_dir, data.name, data.lang);
choice = ((await prompt([skip_install_dep])).skip_install_dep ? pm : 'skip') as PackageManagerChoice;
}
await installDeps(choice, data.name); console.log(`${greenBright('Success, project was initialised!')}`);
await editMain(data.name); process.exit(0);
await editDirs(data.main_dir, data.cmds_dir, data.name, data.lang);
console.log(`${greenBright('Success, project was initialised!')}`);
process.exit(0);
} }
/** It initializes git */ /** It initializes git */
async function git(data: Data) { async function git(data: Data) {
const spin = ora({ const spin = ora({
text: 'Initializing git...', text: 'Initializing git...',
spinner: 'aesthetic', spinner: 'aesthetic',
}).start(); }).start();
try { try {
await execa('git', ['init', data.name]); await execa('git', ['init', data.name]);
await wait(300); await wait(300);
spin.succeed('Git initialized!'); spin.succeed('Git initialized!');
} catch (error) { } catch (error) {
spin.fail(`${redBright('Failed')} to initialize git!\nTry to install it at ${underline('https://git-scm.com')}\nSkipping for now.`); 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. */ /** Wait for a specified number of milliseconds, then return a promise that resolves to undefined. */
async function wait(ms: number) { async function wait(ms: number) {
const wait = (await import('util')).promisify(setTimeout); const wait = (await import('util')).promisify(setTimeout);
return wait(ms); return wait(ms);
} }
interface Data { interface Data {
name: string; name: string;
} }
interface Flags { interface Flags {
y: boolean; y: boolean;
sync: boolean; sync: boolean;
} }
interface PromptData { interface PromptData {
name: string; name: string;
lang: 'typescript' | 'javascript' | 'javascript-esm'; lang: 'typescript' | 'javascript' | 'javascript-esm';
main_dir: string; main_dir: string;
cmds_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 { greenBright } from 'colorette';
import fs from 'fs'; import fs from 'fs';
import prompt from 'prompts'; 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 { 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 * 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) => { function dispatchSave() {
return 'Installed ' + data.name + ' ' + 'from ' + data.author.join(',');
}); }
console.log(`Successfully downloaded plugin(s):\n${greenBright(pluginNames.join('\n'))}`);
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) { async function download(url: string) {
const data = await fetch(url, { method: 'GET' }) const data = await fetch(url, { method: 'GET' })
.then((res) => res.text()) .then((res) => res.text())
.catch(() => null); .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 #!/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 { Command } from 'commander';
import { yellowBright } from 'colorette'; import { plugins } from './commands/plugins.js';
export const program = new Command(); export const program = new Command();
const importDynamic = async <T extends string>(filename: T) => import(`./commands/${filename}` as const); const version: string = '[VI]{{inject}}[/VI]';
declare const __VERSION__: string;
program program
.name('sern') .name('sern')
.description(await importDynamic('help.js').then((m) => m.help)) .description(help)
.version(`sern CLI v${__VERSION__}`, '-v, --version') .version(`sern CLI v${version}`)
.exitOverride(() => process.exit(0)); .exitOverride(() => process.exit(0));
program program
.command('init') .command(init.name)
.description(`Quickest way to scaffold a new project ${yellowBright('[DEPRECATED]')}`) .description('Quickest way to scaffold a new project')
.option('-y', 'Finishes setup as default') .option('-y', 'Finishes setup as default')
.option('-s, --sync', 'Syncs the project and generates sern.config.json') .option('-s, --sync', 'Syncs the project and generates sern.config.json')
.action(async (...args) => importDynamic('init.js').then((m) => m.init(...args))); .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 program
.command('plugins') .command(extra.name)
.description('Install plugins from https://github.com/sern-handler/awesome-plugins') .description('Easy way to add extra things in your sern project')
.argument('[names...]', 'Names of plugins to install') .action(extra);
.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)));
program.parse(); program.parse();

View File

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

View File

@@ -2,91 +2,95 @@ import { blueBright } from 'colorette';
import type { PromptObject } from 'prompts'; import type { PromptObject } from 'prompts';
export const lang: PromptObject = { export const lang: PromptObject = {
message: 'What language do you want the project to be in?', message: 'What language do you want the project to be in?',
name: 'lang', name: 'lang',
type: 'select', type: 'select',
choices: [ choices: [
{ {
title: 'JavaScript', title: 'JavaScript',
description: 'JS', description: 'JS',
value: 'javascript', value: 'javascript',
}, },
{ {
title: 'JavaScript (ESM)', title: 'JavaScript (ESM)',
description: 'JS', description: 'JS',
value: 'javascript-esm', value: 'javascript-esm',
}, },
{ {
title: 'TypeScript', title: 'TypeScript',
description: 'TS - (Recommended)', description: 'TS - (Recommended)',
value: 'typescript', value: 'typescript',
}, },
], ],
}; };
export const main_dir: PromptObject = { export const main_dir: PromptObject = {
message: 'What is the main directory of your project?', message: 'What is the main directory of your project?',
name: 'main_dir', name: 'main_dir',
type: 'text', type: 'text',
initial: 'src', initial: 'src',
}; };
export const cmds_dir: PromptObject = { export const cmds_dir: PromptObject = {
message: 'What is the directory of your commands?', message: 'What is the directory of your commands?',
name: 'cmds_dir', name: 'cmds_dir',
type: 'text', type: 'text',
initial: 'commands', initial: 'commands',
validate: (dir: string) => (dir === 'src' ? 'You can not use src as a directory' : true), validate: (dir: string) =>
dir === 'src' ? 'You can not use src as a directory' : true,
}; };
export const npmInit: PromptObject = { export const npmInit: PromptObject = {
name: 'npm_init', name: 'npm_init',
type: 'confirm', type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize npm?`, message: `Do you want ${blueBright('me')} to initialize npm?`,
initial: true, initial: true,
}; };
export const gitInit: PromptObject = { export const gitInit: PromptObject = {
name: 'gitinit', name: 'gitinit',
type: 'confirm', type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize git?`, message: `Do you want to ${blueBright('me')} to initialize git?`,
initial: true, initial: true,
}; };
export const which_manager: PromptObject = { export const which_manager: PromptObject = {
message: `Which manager do you want to use?`, message: `Which manager do you want to use?`,
name: 'manager', name: 'manager',
type: 'select', type: 'select',
choices: [ choices: [
{ {
title: 'NPM', title: 'NPM',
description: 'Default Package Manager', description: 'Default Package Manager',
selected: true, selected: true,
value: 'npm', value: 'npm',
}, },
{ {
title: 'Yarn', title: 'Yarn',
description: 'Yarn Package Manager', description: 'Yarn Package Manager',
value: 'yarn', value: 'yarn',
}, },
{ {
title: 'Skip', title: 'Skip',
description: 'Skip selection', description: 'Skip selection',
value: 'skip', value: 'skip',
}, },
], ],
}; };
export const skip_install_dep: PromptObject = { export const skip_install_dep: PromptObject = {
name: 'skip_install_dep', name: 'skip_install_dep',
type: 'confirm', type: 'confirm',
message: `Do you want ${blueBright('me')} to install dependencies?`, message: `Do you want ${blueBright('me')} to install dependencies?`,
initial: false, initial: false,
}; };
export const name: PromptObject = { export const name: PromptObject = {
message: 'What is your project name?', message: 'What is your project name?',
name: 'name', name: 'name',
type: 'text', type: 'text',
validate: (name: string) => (name.match('^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$') ? true : 'Invalid name'), 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 { dirname, resolve } from 'node:path';
import { fileURLToPath, URL } from 'url'; import { fileURLToPath, URL } from 'url';
const root = new URL('../../', import.meta.url); const root = new URL('../../', import.meta.url);
const templates = new URL('./templates/', root); const templates = new URL('./templates/', root);
const extraURL = new URL('./extra/', templates); const extraURL = new URL('./extra/', templates);
const extraFolder = fileURLToPath(extraURL); const extraFolder = fileURLToPath(extraURL);
@@ -13,13 +14,19 @@ const extraFolder = fileURLToPath(extraURL);
* @param location - The location of the file to be created. * @param location - The location of the file to be created.
* @param no_ext - If true, the file will be created without an extension. * @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 target = no_ext
const file = `${name}.${lang}.sern`; ? `${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. * @param target - The location of the file to be created.
*/ */
async function createFile(template: string, target: string) { 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. * @returns A promise that resolves to the result of the writeFile function.
*/ */
async function writeFileRecursive(target: string, data: string) { async function writeFileRecursive(target: string, data: string) {
const resolvedTarget = resolve(target); const resolvedTarget = resolve(target);
const dir = dirname(resolvedTarget); 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 { findUp } from 'find-up';
import { readFile, rename, writeFile } from 'node:fs/promises'; import { readFile, rename, writeFile } from 'node:fs/promises';
import { fromCwd } from './fromCwd.js'; 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 * 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. * @param name - The name of the project.
*/ */
export async function editMain(name: string) { export async function editMain(name: string) {
const pjLocation = (await findUp('package.json', { const pjLocation = (await findUp('package.json', {
cwd: fromCwd('/' + name), cwd: fromCwd('/' + name),
})) as string; })) as string;
const output = JSON.parse(await readFile(pjLocation, 'utf8')); const output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read your package.json."); 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. * @param lang - The language you want to use.
*/ */
export async function editDirs( export async function editDirs(
srcName: string, srcName: string,
cmds_dirName: string, cmds_dirName: string,
name: string, name: string,
lang: 'javascript' | 'typescript' | 'javascript-esm' = 'typescript' lang: 'javascript' | 'typescript' | 'javascript-esm' = 'typescript'
) { ) {
const path = (await findUp('src', { const path = (await findUp('src', {
cwd: fromCwd(name), cwd: fromCwd(name),
type: 'directory', type: 'directory',
})) as string; })) as string;
const ext = lang === 'typescript' ? 'ts' : 'js'; const ext = lang === 'typescript' ? 'ts' : 'js';
const newMainDir = path?.replace('src', srcName); const newMainDir = path?.replace('src', srcName);
await rename(path, newMainDir); await rename(path, newMainDir);
const cmdsPath = (await findUp('commands', { const cmdsPath = (await findUp('commands', {
cwd: fromCwd(name, srcName), cwd: fromCwd(name, srcName),
type: 'directory', type: 'directory',
})) as string; })) as string;
const index = (await findUp(`index.${ext}`, { const index = (await findUp(`index.${ext}`, {
cwd: fromCwd(name, srcName), cwd: fromCwd(name, srcName),
})) as string; })) as string;
const newCmdsPath = cmdsPath?.replace('commands', cmds_dirName); const newCmdsPath = cmdsPath?.replace('commands', cmds_dirName);
await rename(cmdsPath, newCmdsPath); await rename(cmdsPath, newCmdsPath);
const tsconfig = await findUp('tsconfig.json', { const tsconfig = await findUp('tsconfig.json', {
cwd: process.cwd() + '/' + name, cwd: process.cwd() + '/' + name,
}); });
if (tsconfig) { if (tsconfig) {
const output = await parseTsConfig(tsconfig); const output = JSON.parse(await readFile(tsconfig, 'utf8'));
if (!output) throw new Error("Can't read your tsconfig.json."); 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;
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 oldfold = ext === 'ts' ? 'dist' : 'src';
const newfold = ext === 'ts' ? 'dist' : srcName; const newfold = ext === 'ts' ? 'dist' : srcName;
const regex = new RegExp(`commands: '${oldfold}/commands'`); const regex = new RegExp(`commands: '${oldfold}/commands'`);
const edit = output.replace(regex, `commands: '${newfold}/${cmds_dirName}'`); 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'; import path from 'path';
export function fromCwd(...dir: string[]) { 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 * @param name - The name of the project
*/ */
export async function installDeps(choice: PackageManagerChoice, name: string) { export async function installDeps(choice: PackageManagerChoice, name: string) {
const pkg = await findUp('package.json', { const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + name, cwd: process.cwd() + '/' + name,
}); });
if (!pkg) throw new Error('No package.json found!'); if (!pkg) throw new Error('No package.json found!');
const output = JSON.parse(await readFile(pkg, 'utf8')); const output = JSON.parse(await readFile(pkg, 'utf8'));
if (!output) throw new Error("Can't read file."); if (!output) throw new Error("Can't read file.");
const deps = output.dependencies; const deps = output.dependencies;
if (!deps) throw new Error("Can't find dependencies."); if (!deps) throw new Error("Can't find dependencies.");
if (choice === 'skip') { if (choice === 'skip') {
return console.log('Dependency installation skipped...'); return console.log('Dependency installation skipped...');
} }
const spin = ora({ const spin = ora({
text: `Installing dependencies...`, text: `Installing dependencies...`,
spinner: 'aesthetic', spinner: 'aesthetic',
}).start(); }).start();
const result = await execa(choice, ['install'], { const result = await execa(choice, ['install'], {
cwd: process.cwd() + '/' + name, cwd: process.cwd() + '/' + name,
}).catch(() => null); }).catch(() => null);
if (!result || result?.failed) { if (!result || result?.failed) {
spin.fail(`${redBright('Failed')} to install dependencies!`); spin.fail(`${redBright('Failed')} to install dependencies!`);
process.exit(1); process.exit(1);
} else spin.succeed(`Dependencies installed!`); } else spin.succeed(`Dependencies installed!`);
} }
/** /**
@@ -49,14 +49,21 @@ export async function installDeps(choice: PackageManagerChoice, name: string) {
* @param name - The name of the project * @param name - The name of the project
*/ */
export async function cloneRepo(lang: string, name: string) { export async function cloneRepo(lang: string, name: string) {
try { try {
await execa('git', ['clone', `https://github.com/sern-handler/templates.git`]); await execa('git', [
copyRecursiveSync(`templates/templates/${lang}`, name); 'clone',
fs.rmSync(`templates/`, { recursive: true, force: true }); `https://github.com/sern-handler/templates.git`,
} catch (error) { ]);
console.log(`${redBright('✖ Failed')} to clone github templates repo. Install git and try again!`); copyRecursiveSync(`templates/templates/${lang}`, name);
process.exit(1); 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. * @param dest - The destination folder where the files will be copied to.
*/ */
export function copyRecursiveSync(src: string, dest: string) { 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(); const isDirectory = exists && stats.isDirectory();
if (isDirectory) { if (isDirectory) {
fs.mkdirSync(dest); fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) { fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); copyRecursiveSync(
}); path.join(src, childItemName),
} else { path.join(dest, childItemName)
fs.copyFileSync(src, dest); );
} });
} else {
fs.copyFileSync(src, dest);
}
} }

View File

@@ -5,21 +5,21 @@ import { execa } from 'execa';
* @returns A promise that resolves to a string. * @returns A promise that resolves to a string.
*/ */
export async function npm() { export async function npm() {
const npm = await execa('npm', ['-v']).catch(() => null); const npm = await execa('npm', ['-v']).catch(() => null);
const npm_version = npm?.stdout; const npm_version = npm?.stdout;
const yarn = await execa('yarn', ['-v']).catch(() => null); const yarn = await execa('yarn', ['-v']).catch(() => null);
const yarn_version = yarn?.stdout; const yarn_version = yarn?.stdout;
if (npm_version && !yarn_version) { if (npm_version && !yarn_version) {
return 'npm'; return 'npm';
} }
if (!npm_version && yarn_version) { if (!npm_version && yarn_version) {
return 'yarn'; return 'yarn';
} }
if (npm_version && yarn_version) { if (npm_version && yarn_version) {
return 'both'; 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 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 FROM node:latest
RUN npm install -g @sern/cli
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json ./
@@ -10,6 +8,4 @@ RUN npm install
COPY . . COPY . .
RUN sern build RUN node src/index.js
CMD node .

View File

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

View File

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

View File

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