59 Commits

Author SHA1 Message Date
github-actions[bot]
9045e1a03f chore(main): release 1.0.1 (#120)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-10-05 12:21:23 +05:30
Jacob Nguyen
d9ca5ff3ff fix: multiple bugs (#119) 2023-10-05 12:18:08 +05:30
github-actions[bot]
5a68916479 chore(main): release 1.0.0 (#116)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-06 10:56:59 +05:30
EvolutionX
e8c4764a9e style: pretty pretty 2023-09-05 09:02:18 +05:30
Jacob Nguyen
c785d49fa5 feat(plugins)!: new method to obtain plugins (#114)
BREAKING CHANGE: older versions of cli does not have plugins command functional
2023-09-05 08:58:08 +05:30
40829267c4 fix(extra): dockerfile errors and tsc fallback (#101) 2023-09-01 22:05:21 +05:30
renovate[bot]
1fc69b0a6e chore(deps): lock file maintenance (#104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 10:05:31 +05:30
renovate[bot]
d01e4cb2f1 chore(deps): update all non-major dependencies (#107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-01 09:59:07 +05:30
Evo
5fd2a7b4c4 feat: list subcommand (#113)
* chore: progress

* chore: blaming regex.

permalink: http://whatthecommit.com/7eaa73b94ca6e8f964d99b6f2db6e9e4

* chore:

permalink:

* chore: lol

* chore: accidental commit

permalink: http://whatthecommit.com/7c6c9323d8c243d10cd93c8bbbc55d09

* fix syntx

* fix list not showing up

* prety

* chore: refresh lockfile

* chore: progress

* fix list not showing up

* refactor: cleanup some mess

* Update preprocessor.ts

---------

Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-08-31 15:07:16 -05:00
EvolutionX
42bd528756 chore: for the statistics only
permalink: http://whatthecommit.com/f6cb5f952917639c9e28f94ae495bd59
2023-08-31 10:47:45 +05:30
EvolutionX
c218065a04 chore: refresh lockfile 2023-08-31 10:45:34 +05:30
Jacob Nguyen
2ece63cb8b fix preprocessor.ts dependencies intellisense 2023-08-28 10:57:19 -05:00
Jacob Nguyen
88e2bbf6c8 feat: build app (#112)
* chore: start publish work

* chore: more debug... who overwrote!

permalink: http://whatthecommit.com/2e6bbd4fd1a21e16039ce52216c3c0b4

* update node version requirement (#106)

* update node version requirement

* publish progress

* progress on publish

* style: pretty pretty

* chore: some progress i guess?

* more progress on publish command

* chore: remove magicast

* more progress on command data

* fix: adding extra fields to json output

* fix: stringify again

* rest field cli

* style: run prettier

* chore: friday 5pm

permalink: http://whatthecommit.com/502256b954264d50f29967e1fef8394b

* prettier and more progress on publish

* progress on publishing

* config can be function and publishing seems to be working correctly

* fix, made mistakes

* fix guild command publishing

* fix guild ids not publishing correctly

* edit correctly

* upgrade fire readiers

* refactor and separate some stuff

* ^

* add default_member_permissions

* refator

* refactors

* publish functionality done

* seems like attachment loading works correctly

* extrapolate

* code splitting and faster startup

* fix up bugs

* forgot to merge prompts/plugin

* remove unneeded esbuild plugin for simple define

* build auto generate typings

* fix: template dir after switching commands to dynamic imports

* update preprocessor

* fix: add experimental warning to publish and build

* fix vulnerability

* oops, false alarm

* better sern.build.js config support

* chore: remove pnpm lock

* cleaner build and also some cli options

* fix regression

* add more cli options

* optimize

* no any

---------

Co-authored-by: EvolutionX <evolutionx9777@gmail.com>
2023-08-28 09:46:29 -05:00
EvolutionX
913ff4dc15 docs(readme): update some outdated info 2023-08-17 13:20:06 +05:30
EvolutionX
30179d1ea9 docs(readme): update to latest help 2023-08-17 13:18:35 +05:30
Jacob Nguyen
3f74658b16 fix: credentials precedence (#111)
Co-authored-by: Evo <85353424+EvolutionX-10@users.noreply.github.com>
2023-08-10 18:23:18 +00:00
EvolutionX
0a3fedd1d8 chore: git stash changelog.md license.md readme.md dist node_modules package-lock.json package.json renovate.json src templates tsconfig.json tsup.config.ts
permalink: http://whatthecommit.com/7ad7c71289d30791e2f20c54465142aa
2023-08-10 11:14:55 +05:30
github-actions[bot]
f12d541dfe chore(main): release 0.6.0 (#110)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-09 23:00:36 +05:30
EvolutionX
642bf11608 chore: release 0.6.0
Release-As: 0.6.0
2023-08-09 22:14:55 +05:30
Evo
827ffb7ad9 feat: publish command (#105)
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
Co-authored-by: jacob <jacoobes@sern.dev>
2023-08-09 22:06:50 +05:30
Gary
2348e3e9ab fix: change file path for sern/extras (#109)
Co-authored-by: gary <gary@mini-hoster.thetechgurus.xyz>
2023-08-06 20:37:35 +05:30
Jacob Nguyen
14df4a9b65 chore: Update package.json 2023-06-29 22:57:54 -05:00
renovate[bot]
4bc7d1b081 chore(deps): lock file maintenance (#94)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 08:09:42 +05:30
EvolutionX
acbec3e733 chore: bless seren 2023-06-19 16:57:46 +05:30
github-actions[bot]
dbc3154101 chore(main): release 0.5.1 (#103)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-16 10:38:09 +05:30
EvolutionX
7252c533bc chore: remove yarn 2023-06-16 10:35:39 +05:30
Evo
dce78c0945 feat(init): deprecate init and bump deps (#102)
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-06-16 10:32:56 +05:30
Jacob Nguyen
58fa3253f6 feat: version injector (#90) 2023-03-21 18:47:40 +05:30
renovate[bot]
1421136181 chore(deps): lock file maintenance (#71)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:33:28 -06:00
renovate[bot]
f29c7881fe chore(deps): update actions/setup-node digest to 64ed1c7 (#81)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com>
2023-03-11 23:29:39 -06:00
renovate[bot]
0d37da5689 chore(deps): update actions/checkout digest to ac59398 (#86)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:28:27 -06:00
renovate[bot]
504d36729e chore(deps): update dependency @favware/npm-deprecate to v1.0.7 (#88)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:26:34 -06:00
renovate[bot]
e391fddf20 chore(deps): update dependency @types/prompts to v2.4.3 (#83)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:23:16 -06:00
renovate[bot]
e5d6de4841 chore(deps): update dependency prettier to v2.8.4 (#87)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:19:00 -06:00
renovate[bot]
fc47847eeb chore(deps): update dependency typescript to v4.9.5 (#78)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 23:13:48 -06:00
Jacob Nguyen
386b30aedd chore: update discord link 2023-02-16 11:43:11 -06:00
Duro
f2531a6f9b docs: fix yarn installation step in readme (#84) 2022-10-11 16:59:23 +05:30
69287ab1bd fix: git not installed errors during init (#79)
Co-authored-by: EvolutionX <evolutionx9777@gmail.com>
2022-09-22 12:38:49 +05:30
Evo
aa398a871c chore: i hate you github 2022-09-16 12:16:22 +05:30
Evo
0855abfea3 chore: ffs github 2022-09-16 12:13:07 +05:30
github-actions[bot]
6212fb3588 chore(main): release 0.5.0 (#77)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-16 12:10:23 +05:30
EvolutionX
2018c4381e chore: release 0.5.0
Release-As: 0.5.0
2022-09-16 12:06:24 +05:30
EvolutionX
5b0214a073 chore: for real.
permalink: http://whatthecommit.com/2466dab665a386b7b29018ac71f45033
2022-09-16 12:00:57 +05:30
Jacob Nguyen
6e6ed3136d docs(readme): add banner (#75) 2022-09-16 06:10:42 +05:30
EvolutionX
c302e1e4be chore: cmon github bruh 2022-09-13 10:31:15 +05:30
EvolutionX
e60fb42e3e chore: cmon github 2022-09-13 10:29:22 +05:30
EvolutionX
5e48f27584 chore: update help 2022-09-13 10:26:50 +05:30
EvolutionX
276b9656b8 ci: update to auto deprecate 2022-09-13 10:22:43 +05:30
EvolutionX
75dff367f5 ci: update testing 2.0 2022-09-13 10:10:24 +05:30
EvolutionX
90429523c3 ci: update testing 2022-09-13 10:06:46 +05:30
Tofix.js
bed31c1f49 feat!: re-write to TypeScript
Co-authored-by: Evo <85353424+EvolutionX-10@users.noreply.github.com>
2022-09-13 10:05:02 +05:30
Jacob Nguyen
9271da3076 feat: adds the esm template option + refactors (#73)
* feat: inline gimmeChoices returned value

* feat: add esm option

* feat: add config support for javascript-esm

* feat: add fromCwd.js util

* feat: transform repetitive paths to fromCwd

* feat: remove conditional branch in favor of transform

* refactor: extract condition into function

* style: prettier
2022-08-13 23:15:44 +05:30
Jacob Nguyen
2009d1fdca feat: add engine for node version & remove manual version check (#72)
cheers
2022-08-12 12:01:35 +05:30
Evo
f9aa2c3d20 chore: oops 2022-08-12 11:01:25 +05:30
Evo
3fa93bd903 ci: update again 2022-08-12 11:01:02 +05:30
Evo
e7c50c4d32 ci: update publish.yml 2022-08-12 10:53:09 +05:30
Evo
b1190efee3 chore: update description 2022-08-07 23:24:22 +05:30
renovate[bot]
55b1fff28e chore(deps): lock file maintenance (#69)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-01 08:51:46 +05:30
renovate[bot]
7bcbf55fb5 chore(deps): update dependency @favware/npm-deprecate to v1.0.5 (#68)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-31 10:32:16 +05:30
61 changed files with 5939 additions and 3588 deletions

View File

@@ -12,17 +12,21 @@ jobs:
steps:
- name: Checkout Project
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Use Node.js
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org/'
- name: Install Node.js dependencies
run: npm i
- name: Deprecate versions
- name: Deprecate dev versions
run: npx npm-deprecate --name "*dev*" --package "@sern/cli" --message "This is a deprecated version of @sern/cli@dev. Please use the latest dev version."
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Deprecate pr versions
run: npx npm-deprecate --name "*pr*" --package "@sern/cli" --message "This is a deprecated version of @sern/cli. Please use the latest version."
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -15,16 +15,16 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'
- name: Install Node.js dependencies
run: npm i
run: npm i && npm run build
- name: Link Project
run: npm link

View File

@@ -17,10 +17,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
@@ -37,15 +37,15 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
- name: Install Node.js dependencies
run: npm i
run: npm i && npm run build
- name: Link Project
run: npm link

View File

@@ -2,6 +2,10 @@ name: Publish
on:
workflow_dispatch:
inputs:
prNumber:
description: The number of the PR that is being deployed
required: true
jobs:
Publish:
@@ -10,16 +14,16 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Set up Node.js
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'
- name: Install Node.js dependencies
run: npm i
run: npm i && npm run build
- name: Link Project
run: npm link
@@ -28,6 +32,9 @@ jobs:
run: sern
- name: Publish to npm
run: npm publish
run: |
TAG=$(echo 'pr-${{ github.event.inputs.prNumber }}')
npm version premajor --preid "${TAG}.$(git rev-parse --verify --short HEAD)" --git-tag-version=false
npm publish --tag ${TAG}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -12,15 +12,15 @@ jobs:
with:
release-type: node
bump-patch-for-minor-pre-major: true
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
if: ${{ steps.release.outputs.release_created }}
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
node-version: 17
registry-url: 'https://registry.npmjs.org'
if: ${{ steps.release.outputs.release_created }}
- run: npm i
- run: npm i && npm run build
if: ${{ steps.release.outputs.release_created }}
- run: npm link
if: ${{ steps.release.outputs.release_created }}
@@ -29,4 +29,4 @@ jobs:
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
if: ${{ steps.release.outputs.release_created }}
if: ${{ steps.release.outputs.release_created }}

View File

@@ -1,2 +1,3 @@
.github/
CHANGELOG.md
CHANGELOG.md
dist/

View File

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

View File

@@ -2,6 +2,80 @@
All notable changes to this project will be documented in this file.
## [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)
### ⚠ BREAKING CHANGES
* re-write to TypeScript
### Features
* add engine for node version & remove manual version check ([#72](https://github.com/sern-handler/cli/issues/72)) ([2009d1f](https://github.com/sern-handler/cli/commit/2009d1fdca9a383d219304fd3cb48a622355b7dc))
* adds the esm template option + refactors ([#73](https://github.com/sern-handler/cli/issues/73)) ([9271da3](https://github.com/sern-handler/cli/commit/9271da30764f86225385e8ae6448b99d66687294))
* re-write to TypeScript ([bed31c1](https://github.com/sern-handler/cli/commit/bed31c1f4995bbf7d8d0a483f562788172e29919))
### Miscellaneous Chores
* release 0.5.0 ([2018c43](https://github.com/sern-handler/cli/commit/2018c4381e665b9b0c5e70a203aa0dfd9fdd3cef))
## [0.4.2](https://github.com/sern-handler/cli/compare/v0.4.1...v0.4.2) (2022-07-28)

View File

@@ -1,6 +1,8 @@
# sern CLI
<div align="center">
<img src="https://raw.githubusercontent.com/sern-handler/.github/main/cli.png" width="900px">
</div>
Our CLI allows you to setup and manage Discord bot projects without writing a single line of code!
# Features
😁 **User Friendly** <br>
💦 **Simple** <br>
@@ -14,7 +16,7 @@ npm install -g @sern/cli@latest
```
```sh
yarn add -g @sern/cli@latest
yarn global add @sern/cli@latest
```
```sh
@@ -28,29 +30,34 @@ 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!
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:
-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
init [options] Quickest way to scaffold a new project [DEPRECATED]
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 `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)
Run `npm create @sern/bot` for an interactive setup on a brand new project using our framework.
## Installing Plugins
sern runs on your plugins. Contribute to the [repository](https://github.com/sern-handler/awesome-plugins) and then install the plugins via our cli! <br>
Run `sern plugins` to see all installable options
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.

6870
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,63 @@
{
"name": "@sern/cli",
"version": "0.4.2",
"description": "A CLI for @sern/handler",
"exports": "./src/index.js",
"bin": {
"sern": "./src/index.js"
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --check .",
"fix": "prettier --write ."
},
"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-handler.js.org",
"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.4",
"@types/prompts": "2.0.14",
"prettier": "2.7.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
}
"name": "@sern/cli",
"version": "1.0.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"
}
}

View File

@@ -1,17 +1,17 @@
{
"$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/"
}
]
"$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/"
}
]
}

169
src/commands/build.ts Normal file
View File

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

View File

@@ -1,12 +0,0 @@
import prompts from 'prompts';
import { extraPrompt } from '../prompts/extra.js';
import { create } from '../utilities/create.js';
const { prompt } = prompts;
export async function extra() {
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);
}

12
src/commands/extra.ts Normal file
View File

@@ -0,0 +1,12 @@
import prompt from 'prompts';
import { extraPrompt } from '../prompts/extra.js';
import { create } from '../utilities/create.js';
export async function extra() {
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);
}

View File

@@ -1,19 +0,0 @@
import { cyanBright, green, yellowBright } from 'colorette';
export function help() {
return `
___ ___ _ __ _ __
/ __|/ _ \\ '__| '_ \\
\\__ \\ __/ | | | | |
|___/\\___|_| |_| |_|
Welcome!
If you're new to ${cyanBright('sern')}, run ${yellowBright(
'sern init'
)} for an interactive setup to your new bot project!
${green(
`If you have any ideas, suggestions, bug reports, kindly join our support server: https://discord.gg/xzK5fUKT4r`
)}
`;
}

13
src/commands/help.ts Normal file
View File

@@ -0,0 +1,13 @@
import { cyanBright, green, magentaBright } from 'colorette';
export const help = `
___ ___ _ __ _ __
/ __|/ _ \\ '__| '_ \\
\\__ \\ __/ | | | | |
|___/\\___|_| |_| |_|
Welcome!
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`)}`;

View File

@@ -1,151 +0,0 @@
import prompts from 'prompts';
import ora from 'ora';
import { greenBright, redBright, yellowBright } from 'colorette';
import { execa } from 'execa';
import { findUp } from 'find-up';
import {
cmds_dir,
lang,
main_dir,
gitInit,
which_manager,
skip_install_dep,
name,
} from '../prompts/init.js';
import { npm } from '../utilities/npm.js';
import { cloneRepo, installDeps } from '../utilities/install.js';
import { editDirs, editMain } from '../utilities/edits.js';
import { writeFile } from 'fs/promises';
const { prompt } = prompts;
/**
* @param {{ y: string; sync: string; }} flags
*/
export async function init(flags) {
// * Check if node version is valid
const { version } = process;
const [major, minor] = version.split('.');
const majorNum = parseInt(major.slice(1));
const minorNum = parseInt(minor);
if (majorNum < 16 || (majorNum === 16 && minorNum < 10)) {
console.log(
yellowBright(
`\nYou are using Node ${version}\nPlease upgrade to Node 16.10.x or higher!\n`
)
);
process.exit(1);
}
let data;
let git_init;
let pm;
if (flags.y) {
const projectName = await prompt([name]);
git_init = true;
pm = 'npm';
data = {
name: projectName.name,
lang: 'typescript',
main_dir: 'src',
cmds_dir: 'commands',
};
} else if (flags.sync) {
data = await prompt([lang, main_dir, cmds_dir]);
} else {
data = await prompt([name, lang, main_dir, cmds_dir]);
git_init = (await prompt([gitInit])).gitinit;
pm = await npm();
}
const config = {
language: data.lang,
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;
if (incompleteDataCondition) process.exit(1);
if (!flags.sync) await cloneRepo(data.lang, data.name);
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + data.name,
});
if (!pkg) throw new Error('No package.json found!');
if (pkg) {
await writeFile(pkg.replace('package.json', 'sern.config.json'), file);
}
if (flags.sync) {
console.log('Project was successfully synced!');
process.exit(0);
}
git_init ? await git(data) : console.log(`Skipping git init...\n`);
let choice;
if (pm === 'both') {
const chosen = await prompt([which_manager]);
choice = chosen.manager;
} else {
const chosen = await prompt([skip_install_dep]);
choice = chosen.skip_install_dep ? pm : 'skip';
}
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);
}
/**
* It initializes git
* @param data - The data object that contains the name of the project.
*/
async function git(data) {
const spin = ora({
text: 'Initializing git...',
spinner: 'aesthetic',
}).start();
const exe = await execa('git', ['init', data.name]);
await wait(300);
if (!exe || exe?.failed) {
spin.fail(
`${redBright('Failed')} to initialize git!` +
'\nMaybe you should run git init?'
);
process.exit(1);
}
spin.succeed('Git initialized!');
}
/**
* Wait for a specified number of milliseconds, then return a promise that resolves to undefined.
* @param {number} ms - The number of milliseconds to wait.
* @returns A function that takes a single argument, ms, and returns a promise that resolves after ms
* milliseconds.
*/
async function wait(ms) {
const wait = (await import('util')).promisify(setTimeout);
return wait(ms);
}

123
src/commands/init.ts Normal file
View File

@@ -0,0 +1,123 @@
import { greenBright, redBright, underline, yellowBright } 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 { writeFile } from 'fs/promises';
import { editDirs, editMain } from '../utilities/edits.js';
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();
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 requiredData = flags.sync !== undefined ? 3 : 4;
const receivedData = Object.keys(data).length;
const incompleteDataCondition = receivedData < requiredData;
if (incompleteDataCondition) process.exit(1);
if (!flags.sync) await cloneRepo(data.lang, data.name);
const pkg = await findUp('package.json', {
cwd: process.cwd() + '/' + data.name,
});
if (!pkg) throw new Error('No package.json found!');
await writeFile(pkg.replace('package.json', 'sern.config.json'), file);
if (flags.sync) {
console.log('Project was successfully synced!');
process.exit(0);
}
git_init ? await git(data) : console.log(`Skipping git init...\n`);
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;
}
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);
}
/** It initializes git */
async function git(data: Data) {
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.`);
}
}
/** 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);
}
interface Data {
name: string;
}
interface Flags {
y: boolean;
sync: boolean;
}
interface PromptData {
name: string;
lang: 'typescript' | 'javascript' | 'javascript-esm';
main_dir: string;
cmds_dir: string;
}

67
src/commands/list.ts Normal file
View File

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

View File

@@ -1,48 +0,0 @@
import { pluginsQ } from '../prompts/plugin.js';
import prompts from 'prompts';
import { fetch } from 'undici';
import fs from 'fs';
import { greenBright } from 'colorette';
const { prompt } = prompts;
/**
* Installs plugins to project
*/
export async function plugins() {
/**
* @type {string[]}
*/
const e = (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')
)}`
);
}
/**
* @param {string} url
* @returns File
*/
async function download(url) {
const data = await fetch(url, { method: 'GET' })
.then((res) => res.text())
.catch(() => null);
if (!data) throw new Error('Download failed! Kindly contact developers');
const dir = `${process.cwd()}/src/plugins`;
const filedir = `${process.cwd()}/src/plugins/${url.split('/').pop()}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const file = fs.writeFileSync(filedir, data);
return file;
}

71
src/commands/plugins.ts Normal file
View File

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

42
src/commands/publish.ts Normal file
View File

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

17
src/create-publish.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
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;
}

267
src/create-publish.mts Normal file
View File

@@ -0,0 +1,267 @@
/**
* This file is meant to be run with the esm / cjs esbuild-kit loader to properly import typescript modules
*/
import { readdir, stat, mkdir, writeFile } from 'fs/promises';
import { join, basename, extname, resolve } from 'node:path';
import { pathExistsSync } from 'find-up';
import assert from 'assert';
import { once } from 'node:events';
import * as Rest from './rest';
import type { sernConfig } from './utilities/getConfig';
import type { PublishableData, PublishableModule, Typeable } from './create-publish.d.ts';
import { cyanBright, greenBright, redBright } from 'colorette';
import ora from 'ora';
async function deriveFileInfo(dir: string, file: string) {
const fullPath = join(dir, file);
return {
fullPath,
fileStats: await stat(fullPath),
base: basename(file),
};
}
function isSkippable(filename: string) {
// empty string is for non extension files (directories)
const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
return filename[0] === '!' || !validExtensions.includes(extname(filename));
}
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
try {
const files = await readdir(dir);
for (const file of files) {
const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
if (fileStats.isDirectory()) {
// TODO: refactor so that i dont repeat myself for files (line 71)
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
} else {
yield* readPaths(fullPath, shouldDebug);
}
} else {
if (isSkippable(base)) {
if (shouldDebug) console.info(`ignored: ${fullPath}`);
} else {
yield 'file:///' + fullPath;
}
}
}
} catch (err) {
throw err;
}
}
// recieved sern config
const [{ config, preloads, commandDir }] = await once(process, 'message'),
{ paths } = config as sernConfig;
for (const preload of preloads) {
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 (typeof config === 'function') {
config = config(absPath, commandModule);
}
try {
commandModule = commandModule.getInstance();
} catch {}
if ((PUBLISHABLE & commandModule.type) != 0) {
// assign defaults
const filename = basename(absPath);
const filenameNoExtension = filename.substring(0, filename.lastIndexOf('.'));
commandModule.name ??= filenameNoExtension;
commandModule.description ??= '';
commandModule.absPath = absPath;
modules.push({ commandModule, config });
}
}
const cacheDir = resolve('./.sern');
if (!pathExistsSync(cacheDir)) {
// TODO: add this in verbose flag
// console.log('Making .sern directory: ', cacheDir);
await mkdir(cacheDir);
}
const optionsTransformer = (ops: Array<Typeable>) => {
return ops.map((el) => {
if ('command' in el) {
const { command, ...rest } = el;
return rest;
}
return el;
});
};
const intoApplicationType = (type: number) => {
if (type === 3) {
return 1;
}
return Math.log2(type);
};
const makeDescription = (type: number, desc: string) => {
if (type !== 1 && desc !== '') {
console.warn('Found context menu that has non empty description field. Implictly publishing with empty description');
return '';
}
return desc;
};
const 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,
default_member_permissions: serialize(config?.defaultMemberPermissions),
},
config,
};
};
// We can use these objects to publish to DAPI
const publishableData = modules.map(makePublishData),
token = process.env.token || process.env.DISCORD_TOKEN,
appid = process.env.applicationId || process.env.APPLICATION_ID;
assert(token, 'Could not find a token for this bot in .env or commandline. Do you have DISCORD_TOKEN in env?');
assert(appid, 'Could not find an application id for this bot in .env or commandline. Do you have APPLICATION_ID in env?');
// partition globally published and guilded commands
const [globalCommands, guildedCommands] = publishableData.reduce(
([globals, guilded], module) => {
const isPublishableGlobally = !module.config || !Array.isArray(module.config.guildIds);
if (isPublishableGlobally) {
return [[module, ...globals], guilded];
}
return [globals, [module, ...guilded]];
},
[[], []] as [PublishableModule[], PublishableModule[]]
);
const spin = ora(`Publishing ${cyanBright('Global')} commands`);
globalCommands.length && spin.start();
const rest = Rest.create(appid, token);
const res = await rest.updateGlobal(globalCommands);
let globalCommandsResponse: unknown;
if (res.ok) {
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)}]`);
switch(res.status) {
case 400 :
throw Error("400: Ensure your commands have proper fields and data with left nothing out");
case 404 :
throw Error("Forbidden 404. Is you application id and/or token correct?")
case 429:
throw Error('Chill out homie, too many requests')
}
console.error(
'errors:',
await res.json().then((res) => {
const errors = Object.values(res.errors);
// @ts-ignore
return errors.map((err) => err?.name?._errors);
})
);
console.error(res.statusText);
}
function associateGuildIdsWithData(data: PublishableModule[]): Map<string, PublishableData[]> {
const guildIdMap: Map<string, PublishableData[]> = new Map();
data.forEach((entry) => {
const { data, config } = entry;
const { guildIds } = config || {};
if (guildIds) {
guildIds.forEach((guildId) => {
if (guildIdMap.has(guildId)) {
guildIdMap.get(guildId)?.push(data);
} else {
guildIdMap.set(guildId, [data]);
}
});
}
});
return guildIdMap;
}
const guildCommandMap = associateGuildIdsWithData(guildedCommands);
let guildCommandMapResponse = new Map<string, Record<string, unknown>>();
for (const [guildId, array] of guildCommandMap.entries()) {
const spin = ora(`[${cyanBright(guildId)}] Updating commands for guild`);
spin.start();
const response = await rest.putGuildCommands(guildId, array);
const result = await response.json();
if (response.ok) {
guildCommandMapResponse.set(guildId, result);
spin.succeed(`[${greenBright(guildId)}] Successfully updated commands for guild`);
} else {
spin.fail(`[${redBright(guildId)}] Failed to update commands for guild, Reason: ${result.message}`);
switch(response.status) {
case 400 :
throw Error("400: Ensure your commands have proper fields and data and left nothing out");
case 404 :
throw Error("Forbidden 404. Is you application id and/or token correct?")
case 429:
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,38 +0,0 @@
#!/usr/bin/env node
import { init } from './commands/init.js';
import { help } from './commands/help.js';
import { extra } from './commands/extra.js';
import { Command } from 'commander';
import { version } from './utilities/version.js';
import { plugins } from './commands/plugins.js';
export const program = new Command();
program
.name('sern')
.description(help())
.version(version())
.exitOverride(() => process.exit(0));
program
.command(init.name)
.description('Quickest way to scaffold a new project')
.option('-y', 'Finishes setup as default')
.option('-s, --sync', 'Syncs the project and generates sern.config.json')
.action(init);
program
.command(plugins.name)
.description(
'Install plugins from https://github.com/sern-handler/awesome-plugins'
)
.option('-n --name', 'Name of plugin')
.action(plugins);
program
.command(extra.name)
.description('Easy way to add extra things in your sern project')
.action(extra);
program.parse();

64
src/index.ts Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { yellowBright } from 'colorette';
export const program = new Command();
const importDynamic = async <T extends string>(filename: T) => import(`./commands/${filename}` as const);
declare const __VERSION__: string;
program
.name('sern')
.description(await importDynamic('help.js').then((m) => m.help))
.version(`sern CLI v${__VERSION__}`, '-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)));
program
.command('plugins')
.description('Install plugins from https://github.com/sern-handler/awesome-plugins')
.option('-n --name', 'Name of plugin')
.action((...args) => importDynamic('plugins.js').then((m) => m.plugins(...args)));
program
.command('extra')
.description('Easy way to add extra things in your sern project')
.action((...args) => importDynamic('extra.js').then((m) => m.extra(...args)));
program //
.command('commands')
.description('Defacto way to manage your slash commands')
.addCommand(
new Command('publish')
.description('New way to manage your slash commands')
.option('-W --suppress-warnings', 'suppress experimental warning')
.option('-i, --import [scriptPath...]', 'Prerequire a script to load into publisher')
.option('-t, --token [token]')
.option('--appId [applicationId]')
.argument('[path]', 'path with respect to current working directory that will locate all published files')
.action(async (...args) => importDynamic('publish.js').then((m) => m.publish(...args)))
)
.addCommand(
new Command('list') //
.description('List all slash commands')
.action(async (...args) => importDynamic('list.js').then((m) => m.list(...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 --suppress-warnings', 'suppress experimental warning')
.option('-p --project [filePath]', 'build with this sern.build file')
.option('-e --env', 'path to .env file')
.option('--tsconfig [filePath]', 'Use this tsconfig')
.action(async (...args) => importDynamic('build.js').then((m) => m.build(...args)));
program.parse();

View File

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

View File

@@ -1,18 +0,0 @@
export const extraPrompt = {
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',
},
],
};

20
src/prompts/extra.ts Normal file
View File

@@ -0,0 +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',
},
],
};

View File

@@ -1,96 +0,0 @@
import { blueBright } from 'colorette';
export const lang = {
message: 'What language do you want the project to be in?',
name: 'lang',
type: 'select',
choices: [
{
title: 'JavaScript',
description: 'JS',
value: 'javascript',
},
{
title: 'TypeScript',
description: 'TS - (Recommended)',
value: 'typescript',
},
],
};
export const main_dir = {
message: 'What is the main directory of your project?',
name: 'main_dir',
type: 'text',
initial: 'src',
};
export const cmds_dir = {
message: 'What is the directory of your commands?',
name: 'cmds_dir',
type: 'text',
initial: 'commands',
validate: (/** @type {string} */ dir) =>
dir === 'src' ? 'You can not use src as a directory' : true,
};
/**
* @type {import('prompts').PromptObject}
*/
export const npmInit = {
name: 'npm_init',
type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize npm?`,
initial: true,
};
export const gitInit = {
name: 'gitinit',
type: 'confirm',
message: `Do you want to ${blueBright('me')} to initialize git?`,
initial: true,
};
export const which_manager = {
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 = {
name: 'skip_install_dep',
type: 'confirm',
message: `Do you want ${blueBright('me')} to install dependencies?`,
initial: false,
};
/**
* @type {import('prompts').PromptObject}
*/
export const name = {
message: 'What is your project name?',
name: 'name',
type: 'text',
validate: (/**@type {string}*/ name) =>
name.match('^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$')
? true
: 'Invalid name',
};

92
src/prompts/init.ts Normal file
View File

@@ -0,0 +1,92 @@
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',
},
],
};
export const main_dir: PromptObject = {
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),
};
export const npmInit: PromptObject = {
name: 'npm_init',
type: 'confirm',
message: `Do you want ${blueBright('me')} to initialize npm?`,
initial: true,
};
export const gitInit: PromptObject = {
name: 'gitinit',
type: 'confirm',
message: `Do you want to ${blueBright('me')} to initialize git?`,
initial: true,
};
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',
},
],
};
export const skip_install_dep: PromptObject = {
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'),
};

View File

@@ -1,32 +0,0 @@
import { fetch } from 'undici';
import { getLang } from '../utilities/getLang.js';
async function gimmechoices() {
const lang =
(await getLang().catch(() => null)) === 'typescript'
? 'TypeScript'
: 'JavaScript';
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();
const choices = data.map(
(/** @type {{ name: string; download_url: string; }} */ e) => ({
title: e.name,
value: e.download_url,
})
);
return choices;
}
export async function pluginsQ() {
return {
name: 'list',
type: 'autocompleteMultiselect',
message: 'What plugins do you want to install?',
choices: await gimmechoices(),
min: 1,
};
}

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

@@ -0,0 +1,31 @@
import type { Choice, PromptObject } from 'prompts';
import { fetch } from 'undici';
async function gimmechoices(): Promise<Choice[]> {
const link = `https://raw.githubusercontent.com/sern-handler/awesome-plugins/main/pluginlist.json`;
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,
}));
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;
link: string;
}

41
src/rest.ts Normal file
View File

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

View File

@@ -1,13 +0,0 @@
FROM node:latest
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN tsc --build
RUN node dist/index.js

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

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

View File

@@ -1,54 +0,0 @@
import { URL, fileURLToPath } from 'url';
import { resolve, dirname } from 'node:path';
import { readFile, mkdir, writeFile } from 'fs/promises';
const root = new URL('../', import.meta.url);
const templates = new URL('./templates/', root);
const extraURL = new URL('./extra/', templates);
const extraFolder = fileURLToPath(extraURL);
/**
* It creates a file with the name `name.lang.sern` in the `location` directory
* @param {string} name - The name of the file.
* @param {string} lang - The language you want to use.
* @param {string} location - The location of the file to be created.
* @param {boolean} no_ext - If true, the file will be created without an extension.
* @returns File
*/
export async function create(name, lang, location, no_ext) {
const file = `${name}.${lang}.sern`;
const target = no_ext
? `${location}/${name}`
: `${location}/${name}.${lang}`;
return createFile(file, target);
}
/**
* It reads a file from a template folder, and writes it to a target folder
* @param {string} template - The name of the file to be created.
* @param {string} target - The location of the file to be created.
*/
async function createFile(template, target) {
const location = `${extraFolder}${template}`;
const file = await readFile(location, 'utf8');
await writeFileRecursive(target, file);
}
/**
* It creates a directory recursively, then writes a file to it
* @param {string} target - The path to the file you want to write to.
* @param {string} data - The data to write to the file.
* @returns A promise that resolves to the result of the writeFile function.
*/
async function writeFileRecursive(target, data) {
const resolvedTarget = resolve(target);
const dir = dirname(resolvedTarget);
await mkdir(dir, { recursive: true });
return writeFile(resolvedTarget, data);
}

51
src/utilities/create.ts Normal file
View File

@@ -0,0 +1,51 @@
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);
/**
* It creates a file with the name `name.lang.sern` in the `location` directory
* @param name - The name of the file.
* @param lang - The language you want to use.
* @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`;
const target = no_ext ? `${location}/${name}` : `${location}/${name}.${lang}`;
return createFile(file, target);
}
/**
* It reads a file from a template folder, and writes it to a target folder
* @param template - The name of the file to be created.
* @param target - The location of the file to be created.
*/
async function createFile(template: string, target: string) {
const location = `${extraFolder}${template}`;
const file = await readFile(location, 'utf8');
await writeFileRecursive(target, file);
}
/**
* It creates a directory recursively, then writes a file to it
* @param target - The path to the file you want to write to.
* @param data - The data to write to the file.
* @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);
await mkdir(dir, { recursive: true });
return writeFile(resolvedTarget, data);
}

View File

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

View File

@@ -1,84 +0,0 @@
import { readFile, rename, writeFile } from 'node:fs/promises';
import { findUp } from 'find-up';
/**
* It takes a string, finds the package.json file in the directory of the string, and changes the name
* of the package.json file to the string.
* @param {string} name - The name of the project.
* @returns A promise.
*/
export async function editMain(name) {
const pjLocation = await findUp('package.json', {
cwd: process.cwd() + '/' + name,
});
const output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read your package.json.");
output.name = name;
return writeFile(pjLocation, JSON.stringify(output, null, 2));
}
/**
* It renames the `src` and `commands` directories, and edits the `index.ts` file to reflect the
* changes
* @param {string} srcName - The name of the folder that will contain your main files.
* @param {string} cmds_dirName - The name of the directory where your commands will be stored.
* @param {string} name - The name of the folder you want to edit.
* @param {'javascript' | 'typescript'} lang - The language you want to use.
* @returns void
*/
export async function editDirs(
srcName,
cmds_dirName,
name,
lang = 'typescript'
) {
const path = await findUp('src', {
cwd: process.cwd() + '/' + name,
type: 'directory',
});
const ext = lang === 'typescript' ? 'ts' : 'js';
const newMainDir = path?.replace('src', srcName);
await rename(path, newMainDir);
const cmdsPath = await findUp('commands', {
cwd: process.cwd() + '/' + name + '/' + srcName,
type: 'directory',
});
const index = await findUp(`index.${ext}`, {
cwd: process.cwd() + '/' + name + '/' + srcName,
});
const newCmdsPath = cmdsPath?.replace('commands', cmds_dirName);
await rename(cmdsPath, newCmdsPath);
const tsconfig = await findUp('tsconfig.json', {
cwd: process.cwd() + '/' + name,
});
if (tsconfig) {
const output = JSON.parse(await readFile(tsconfig, 'utf8'));
if (!output) throw new Error("Can't read your tsconfig.json.");
output.compilerOptions.rootDir = srcName;
writeFile(tsconfig, JSON.stringify(output, null, 2));
}
const output = await readFile(index, 'utf8');
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}'`
);
return writeFile(index, edit);
}

80
src/utilities/edits.ts Normal file
View File

@@ -0,0 +1,80 @@
import { findUp } from 'find-up';
import { readFile, rename, writeFile } from 'node:fs/promises';
import { fromCwd } from './fromCwd.js';
/**
* It takes a string, finds the package.json file in the directory of the string, and changes the name
* of the package.json file to the string.
* @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 output = JSON.parse(await readFile(pjLocation, 'utf8'));
if (!output) throw new Error("Can't read your package.json.");
output.name = name;
return writeFile(pjLocation, JSON.stringify(output, null, 2));
}
/**
* It renames the `src` and `commands` directories, and edits the `index.ts` file to reflect the
* changes
* @param srcName - The name of the folder that will contain your main files.
* @param cmds_dirName - The name of the directory where your commands will be stored.
* @param name - The name of the folder you want to edit.
* @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'
) {
const path = (await findUp('src', {
cwd: fromCwd(name),
type: 'directory',
})) as string;
const ext = lang === 'typescript' ? 'ts' : 'js';
const newMainDir = path?.replace('src', srcName);
await rename(path, newMainDir);
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 newCmdsPath = cmdsPath?.replace('commands', cmds_dirName);
await rename(cmdsPath, newCmdsPath);
const tsconfig = await findUp('tsconfig.json', {
cwd: process.cwd() + '/' + name,
});
if (tsconfig) {
const output = JSON.parse(await readFile(tsconfig, 'utf8'));
if (!output) throw new Error("Can't read your tsconfig.json.");
output.compilerOptions.rootDir = srcName;
await writeFile(tsconfig, JSON.stringify(output, null, 2));
}
const output = await readFile(index, 'utf8');
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}'`);
return writeFile(index, edit);
}

5
src/utilities/fromCwd.ts Normal file
View File

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

View File

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

View File

@@ -1,18 +0,0 @@
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 {Promise<string>} The language of the project.
*/
export async function getLang() {
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;
}

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;
}

17
src/utilities/getSern.ts Normal file
View File

@@ -0,0 +1,17 @@
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

@@ -1,89 +0,0 @@
import { execa } from 'execa';
import { redBright } from 'colorette';
import fs from 'fs';
import path from 'path';
import { readFile } from 'fs/promises';
import { findUp } from 'find-up';
import ora from 'ora';
/**
* It installs dependencies from a package.json file
* @param {'skip' | 'npm' | 'yarn'} choice - The package manager to use.
* @param {string} name - The name of the project
* @returns a promise.
*/
export async function installDeps(choice, name) {
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 deps = output.dependencies;
if (!deps) throw new Error("Can't find dependencies.");
if (choice === 'skip') {
return console.log('Dependency installation skipped...');
}
const spin = ora({
text: `Installing dependencies...`,
spinner: 'aesthetic',
}).start();
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!`);
}
/**
* Clone the repo, copy the files from the repo to the new project directory, and delete the repo
* @param {string} lang - The language of the template
* @param {string} name - The name of the project
*/
export async function cloneRepo(lang, name) {
await execa('git', [
'clone',
`https://github.com/sern-handler/templates.git`,
]);
copyRecursiveSync(`templates/templates/${lang}`, name);
fs.rmSync(`templates/`, { recursive: true, force: true });
}
/**
* If the source is a directory, create the destination directory and then recursively copy the
* contents of the source directory to the destination directory. If the source is not a directory,
* copy the source file to the destination file
* @param {string} src - The source path.
* @param {string} dest - The destination folder where the files will be copied to.
*/
export function copyRecursiveSync(src, dest) {
const exists = fs.existsSync(src);
const stats = exists && fs.statSync(src);
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);
}
}
// async function wait(ms) {
// const wait = (await import('util')).promisify(setTimeout);
// return wait(ms);
// }

84
src/utilities/install.ts Normal file
View File

@@ -0,0 +1,84 @@
import { redBright } from 'colorette';
import { execa } from 'execa';
import { findUp } from 'find-up';
import fs from 'fs';
import { readFile } from 'fs/promises';
import ora from 'ora';
import path from 'path';
import type { PackageManagerChoice } from './types';
/**
* It installs dependencies from a package.json file
* @param choice - The package manager to use.
* @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 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.");
if (choice === 'skip') {
return console.log('Dependency installation skipped...');
}
const spin = ora({
text: `Installing dependencies...`,
spinner: 'aesthetic',
}).start();
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!`);
}
/**
* Clone the repo, copy the files from the repo to the new project directory, and delete the repo
* @param lang - The language of the template
* @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);
}
}
/**
* If the source is a directory, create the destination directory and then recursively copy the
* contents of the source directory to the destination directory. If the source is not a directory,
* copy the source file to the destination file
* @param src - The source path.
* @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 stats = (exists && fs.statSync(src)) as fs.Stats;
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);
}
}

View File

@@ -1,25 +0,0 @@
import { execa } from 'execa';
/**
* It checks if you have npm or yarn installed, and returns a string based on the result
* @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 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 'yarn';
}
if (npm_version && yarn_version) {
return 'both';
}
}

25
src/utilities/npm.ts Normal file
View File

@@ -0,0 +1,25 @@
import { execa } from 'execa';
/**
* It checks if you have npm or yarn installed, and returns a string based on the result
* @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 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 'yarn';
}
if (npm_version && yarn_version) {
return 'both';
}
}

View File

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

View File

@@ -0,0 +1,41 @@
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;
}
}

3
src/utilities/require.ts Normal file
View File

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

35
src/utilities/types.ts Normal file
View File

@@ -0,0 +1,35 @@
export type PackageManagerChoice = 'skip' | 'npm' | 'yarn';
export type GuildId = 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;
}

View File

@@ -1,7 +0,0 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export function version() {
const { version: v } = require('../../package.json');
return `SernHandler CLI v${v}`;
}

View File

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

View File

@@ -0,0 +1,15 @@
FROM node:latest
WORKDIR /app
COPY package.json ./
RUN npm install
RUN npm install -D typescript
COPY . .
RUN tsc --build || npx -p typescript tsc --build
CMD node dist/index.js

View File

@@ -1,17 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "NodeNext",
"lib": ["es2017", "dom"],
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": false,
"noImplicitThis": true,
"alwaysStrict": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["test", "src"]
"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
}
}

26
tsup.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const shared = {
entry: [
'src/index.ts',
'src/create-publish.mts',
'src/commands/**',
],
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,
});