diff --git a/.gitignore b/.gitignore index 25c8fdb..4d68ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -node_modules -package-lock.json \ No newline at end of file +**/node_modules/ +package-lock.json +.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..00ad71f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/README.md b/README.md index cd3d2fd..19b73ad 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,507 @@ -# string-commands +# string-commands v2 rewrite -A powerful command handler and parser for all your needs. Includes checks, custom arguments, middlewares and more. +String Commands is a new experimental command handler with differient ideas. -## Features +This `v2` branch is a full rewrite, in typescript. -- Easy to use -- Recursive folder importing -- Compatability with older commands -- Configurable messages (no defaults) -- Command checks (requirements) -- Middlewares -- Argument handling with custom argument support +## Goals -## Examples +- Customizability +- Extensible +- Async by default -For the best example please see [consoleExample.js](/examples/consoleExample.js) +## Example -## Usage +See tested and working examples: +- [Console REPL](./examples/stdin.ts) +- [discord.js with Slash Commands](./examples/discordjs-slash.ts) -### Installing +```js +let handler = new CommandHandler(); + +// Set up middlewares depending on what you need: + +handler + .use(MultiPrefix({ prefixes: ["!", ".", "/"] })) + .use(SplitString()) + .use(CommandResolver()) + .use(CommandExecutor()) + +// You can also define your own middlewares + +let globalNumber = 0; +handler.use({ + id: "command-number", + run: (ctx) => ({ ..ctx, number: globalNumber++ }), +}) + +// Add your commands + +handler.add({ + name: "hello", + run: ({ number }) => { + console.log(`Hi! This is execution #${number}, provided by the custom middleware.`); + } +}) + +// and run them + +handler.run({ + input: "hello", +}) +``` + +<<<<<<< Updated upstream +## TODO + +- [ ] CommandHandler + - [x] run + - [x] add + - [x] use + - [ ] addFolder + - [ ] remove + - [ ] removeFolder +- [ ] Core middlewares + - [x] Split string + - [x] Command resolver + - [ ] Aliases + - [x] Executor + - [ ] Command checks +- [ ] Argument system + - [ ] reader impl + - [ ] extensible parsers +- [ ] Adapters + - [ ] lowdb + - [ ] i18next + - [ ] discord.js +- [ ] Utilities + - [ ] Pretty printer +- [ ] Documentation + - [ ] Core middlewares +======= +### Creating a Command Handler + +You can pass an object of options into the CommandHandler. + +```js +let handler = new CommandHandler({ + // inputs must also begin with prefix + // you can set this to an empty string + prefix: "!", + + // by default, log uses console + // set to false to disable logging + log: false, + // or put in your own logger + log: myLogger, + + // you can also put the functions here to overwrite them, + // instead of overriding them after initialization + // note that these are still optional + transformCommand: () => {}, + buildArguments: () => {}, +}); +``` + +### Writing Commands + +Commands are just objects that must have two properties: + +- `name` (string) The name of the command +- `run` (function) The 'runner function' + +**Example Command:** + +```js +let myCommand = { + name: "ping", + run: (ctx, args) => { + console.log("Pong!"); + }, +}; +``` + +#### Runner Functions + +By default, the arguments of the runner functions are as follows: + +```js +run: (ctx, args) => {} +``` + +The arguments of the Runner Functions are defined using `CommandHandler#buildArguments` + +You can change the order, remove, or add new params to the runner functions by overwriting `CommandHandler#buildArguments` like so: + +```js +handler.buildArguments = (b) => { + return [b.args, b.ctx.username]; +} + +// which would make you be able to define the runner function as: +let run = (args, username) => {}; +``` + +### Registering Commands + +There are two ways to register commands: + +**1. using an object:** + +```js +handler.registerCommand(cmd); +handler.registerCommand({ ... }); +``` + +**2. from a folder (recursively):** + +```js +// path is "./commands" by default +handler.registerCommands(); +handler.registerCommands(path); +handler.registerCommands("./src/cmds"); +``` + +#### Registering old commands + +If your project has old commands from another command handler that has differient command objects, you can still import them. + +You can set the `CommandHandler#transformCommand` to a helper function that would 'transform' the exported object into a valid command object. + +**Example:** + +```js +let oldCommand = { + help: { + name: "ping" + }, + execute: async () => {}, +}; + +handler.transformCommand = (obj) => { + if(!obj.name) obj.name = obj.help.name; + if(!obj.run) obj.run = obj.execute; + return obj; +} +``` + +### Checks + +Your commands can also have custom checks. Its recommended to make the checks once and reuse them for commands. + +Command Checks are just the same as runner functions, but they must return an object with these props: + +- `pass` (boolean) - set to true if check succeeded +- `message` (string) - if it failed, the message explaining why + +```js +{ + checks: [ + (ctx, args) => { + // Im an useless check! Gonna make it run! + return { pass: true }; + }, + + (ctx, args) => { + // just you wait until you hear that im after you + return { + pass: false, + message: "Hardcoded Failiure", + }; + }, + ] +} +``` -You can install this package using +### ArgumentParser -```sh -npm i string-commands +The argument parser is a complex system that parses and validates given arguments for you. This means no more if-else checks for arguments in every command :D + +Argument parser will look into `args` of your command objects. + +**Examples:** + +Arguments are also just objects. They must have a `type`, the rest are **parser options** + +Arguments can have custom names using `name` + +For example, in the code below, the `rest: true` field is a parser option + +```js +{ + name: "say", + args: [{ + type: "text", + name: "yourMessage", + rest: true, + }], + run: () => {}, +} ``` -And then import using +**String Resolving:** + +You can also put strings instead of objects, but the side effect is that you cant define other parser options. ```js -import { CommandHandler } from "string-commands"; +args: ["text"] + +// a ":" can be used to give it a name +args: ["yourMessage:text"] + +// it can also be marked optional and required +args: ["[text]", ""] + +// you can use three dots in the end to set `rest: true` +args: ["text..."] + +// you can also combine it like so: +args: ["..."] + +// you can also also turn the whole array into a string +args: " [comment:string]..." ``` -### Documentation +#### Special Parser Options + +**`rest` (boolean)** - if set to true, consumes the rest of the input with it +**`optional` (boolean)** - if set to true, this argument is considered optional +**`name` (string)** - define the name of the argument + +#### Native Parsers + +##### ArgumentParser: text + +Options: + +- `min` (number) - Minimum characters +- `max` (number) - Maximum characters + +##### ArgumentParser: number + +Options: + +- `min` (number) +- `max` (number) +- `isInt` (boolean = false) - If the number should be an integer -See these for docs: +##### ArgumentParser: bool -- [Command Handler](./docs/CommandHandler.md) -- [Commands](./docs/Commands.md) -- [Usages](./docs/Usages.md) -- [Middlewares](./docs/Middlewares.md) +Options: + +- `acceptNull` (boolean|string) - Will accept values not true or false as `null`. Set to `"strict"` to strictly check if value is `"null"` or not. + +#### Writing Custom Argument Parsers + +Arguments parsers are internally called **Usage Parser**s. + +A usage parser is an object, like an argument, but with a `parse` function. + +```js +// usage parser: "that" +{ + // underlying type + type: "text", + + // options to pass into underlying type + max: 1024, + + // the parse function + async parse(ctx) { + // the user input or parsed value + ctx.arg; + + // the options + ctx.opts.foo; + + // "custom name" + ctx.name; + } +} + +args: [{ type: "that", name: "custom name", foo: 1 }] +``` + +**What should I return?** + +If the parsing etc was successful, return an object with `parsed` as your value. + +If there were any errors etc, return an object with `fail: true` and `message` set to the error message. + +```js +// it doesnt have to be a variable btw +return { parsed: myValue }; + +return { + fail: true, + message: "Your argument failed the vibe check.", +} +``` + +Usage parsers can easily inherit other parsers using `type`. + +ArgumentParser automatically parses the lowest type and builds up from there. + +This means if you have an usage parser with type set to `"number"`, `ctx.arg` will be a number instead of a string. + +**Inheritance Example:** + +```js +const specialChars = "!'^+%&/()=?_-*>£#$½{[]}\\".split(""); + +handler.registerUsages({ + char: { + type: "text", + min: 1, + max: 1, + }, + + specialChar: { + type: "char", + async parse(ctx) { + if(specialChars.includes(ctx.arg)) { + return { parsed: ctx.arg }; + } else { + return { + fail: true, + message: "Your char isnt special.", + } + }; + }, + }, +}) +``` + +#### Registering Custom Argument Parsers + +```js +handler.registerUsage(usage); +handler.registerUsage({ ... }); + +// obj: Object +handler.registerUsages(obj); +handler.registerUsages({ + name1: usage1, + ... +}); +``` ## TODO -- [x] Complete typings -- [x] Middleware +- [ ] Discord.js Plugin +- [ ] Discord.js Slash commands plugin - [ ] Subcommands -- [ ] Database Middlewares -- [ ] Permissions Middleware +- [ ] Permissions +>>>>>>> Stashed changes -## Changelog +## Concepts -**v1.1.0:** +<<<<<<< Updated upstream +### Context +======= +**v1.0.0:** +>>>>>>> Stashed changes -- :warning: **BREAKING:** In `ExecutorContext` (ctx in `failedChecksMessage(ctx)`/now `on("failedChecks", (ctx)=>{})`), the **`checks`** property is now `CommandCheckResult[]` instead of `string[]`. This allows Command Checks to supply additional information about the failed checks, such as - - Codes for custom error messages - - Additional context information (for example, you could supply the user's score or something so your failed checks handler doesnt have to fetch it from a database again, or maybe supply the needed threshold etc) -- :warning: The `invalidUsageMessage` and `failedChecksMessage` functions have been removed. Please use the `invalidUsage` and `failedChecks` events instead. -- Default prefix is now `""` (empty string) +Command resolving, execution etc are all made possible using Contexts. -- Added [Middlewares](./docs/Middlewares.md) -- Added `index.d.ts` declarations file that's needlessly complex (and also incomplete) -- Added more documentation +The `BaseContext` contains `{ handler, input }` -**v1.0.0:** +Every middleware gets the last context and adds/removes/modifies properties. + +For example, the `CommandResolver` middleware requires `{ commandName, handler }` in the context and adds `{ rootCommand, targetCommand }` to the context. + +## Docs + +### CommandHandler + +```js +// Create a new Command Handler +let handler = new CommandHandler(); + +``` + +### Middleware: Inspect + +**Options** `fn: (ctx: T) => void` + +Inspects the current context, useful for debugging + +```js +handler + // Logs { handler: CommandHandler, ... } + .use(Inspect()) + + // Custom function + .use(Inspect((ctx) => { ... })) +``` + +### Middleware: Prefix + +`input: string` => `input: string` + +**Options:** `{ prefix: string }` + +Ignore runs where the input does not start with `prefix` and strip it when it does. + +```js +handler + .use(Inspect()) // { input: "!help", ... } + .use(Prefix({ prefix: "!" })) + .use(Inspect()) // { input: "help", ... } + +handler.run({ input: "!help" }) +``` + +### Middleware: MultiPrefix + +`input: string` => `input: string` + +**Options:** `{ prefixes: string[] }` + +Same as `Prefix` middleware, but supports multiple. + +### Middleware: SplitString + +**Requires:** `{ input: string }` + +**Outputs:** `{ commandName: string, commandArgs: string }` + +Splits the first word of the input to be able to pass it into a command resolver + +```js +handler + .use(SplitString()) + .use(Inspect()) // { commandName: "roll", commandArgs: "1d6", ... } + +handler.run({ input: "roll 1d6" }) +``` + +### Middleware: ContextStatic + +**Options:** any object + +This utility middleware adds the properties given via options to the context. + +```js +handler + .use(ContextStatic({ a: 1 })) + .use(Inspect()) // { a: 1, ... } +``` + +### Middleware: CommandExecutor + +**Requires:** `{ targetCommand, handler }` + +This middleware executes aka calls the `run` method of the command. + +### Middleware: CommandResolver + +**Requires:** `{ commandName, handler }` + +**Outputs:** `{ rootCommand, targetCommand }` + +This middleware resolves the command based on the `commandName` property. + +If a command is not found, the `commandNotFound` reply is invoked. -- Created project -- Added documentation +`targetCommand` is usually equal to `rootCommand` unless there are subcommands, in which case the resolved subcommand is assigned to `targetCommand`. diff --git a/docs/Commands.md b/docs/Commands.md index a8b2129..3a9efd8 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -54,23 +54,31 @@ let myCommand = { Your commands can also have custom checks. Its recommended to make the checks once and reuse them for commands. -Command Checks are just the same as the runner functions, but they must return an object with these props: +Command Checks are similar to runner functions, but there's a differience. + +The function's first argument is the [`ExecutorContext`](./CommandHandler.md#executorcontext) and the rest are [`Runner Args`](./CommandHandler.md#runnerargs), like this: `(execCtx, a, b, c) => {...}`. This is for other packages to be easily imported and used. + +**Migrating to 1.2.0:** just add an `_` argument to your checks, its that easy (`function(a,b){}` => `function(_,a,b){}`) + +The command checks need to return a `CommandCheckResult`, which is something like this: - `pass` (boolean) - set to true if check succeeded - `message` (string) - if it failed, the message explaining why +Example: `{ pass: false, message: "Thou shall not pass" }` + They can also be **asynchronous** ```js { checks: [ - (ctx, args) => { + (execCtx) => { // Im an useless check! Gonna make it run! return { pass: true }; }, - (ctx, args) => { - // just you wait until you hear that im after you + async (execCtx) => { + // haha No. return { pass: false, message: "Hardcoded Failiure", @@ -80,4 +88,35 @@ They can also be **asynchronous** } ``` +For catching when commands fail checks, see [here](./CommandHandler.md#failed-checks) + +You can also add other extra properties to the `CommandCheckResult` for more customization: + +```js +{ + checks: [ + ({ ctx }) => { + + if(ctx.cool < 5) { + return { + pass: false, + message: "Not cool enough", + code: "UNCOOL", + coolness: ctx.cool, + }; + } else { + return { pass: true } + }; + + }, + ] +} + +handler.on("failedChecks", ({ checks }) => { + if(checks[0].code == "UNCOOL") { + // do custom message stuff + } +}) +``` + [^ Back to top](#commands) diff --git a/docs/PermissionsPlugin.md b/docs/PermissionsPlugin.md new file mode 100644 index 0000000..2503473 --- /dev/null +++ b/docs/PermissionsPlugin.md @@ -0,0 +1,61 @@ +# Permissions Plugin + +## Permissions + +Permissions are defined are strings with dots for categorization. + +For example: + +```js +"dosomething" +"posts.create" +"posts.comments.reply" +``` + +Permissions can have a "*" to signify the user has every permission in a category: + +```js +"posts.*" + includes: + - "posts.comments" + - "posts.create" + - "posts.likes.remove" +``` + +## Using the middleware + +First, import the plugin. Then register it using `CommandHandler#use` + +```js +import { Permissions } from "string-commands/permissions"; + +// ... + +handler.use(Permissions()); +``` + +By default, the plugin fetches user permissions from CustomContext. If the permissions are in any of these paths: + +- + +Then you can skip the next part. + +### Get user permissions using CustomContext + +When creating the middleware, you can pass an object of options into `Permissions`. + +```js +handler.use(Permissions({ + getPermissions: async () => {}, +})); +``` + +The `getPermissions` function property should return the user/entity's posessed permissions. + +For example: + +```js +handler.use(Permissions({ + getPermissions: async () => {}, +})); +``` diff --git a/examples/consoleExample.js b/examples/consoleExample.js index 12f390a..9e8edc1 100644 --- a/examples/consoleExample.js +++ b/examples/consoleExample.js @@ -4,114 +4,72 @@ import { CommandHandler } from "../src/index.js"; This example is a simple console commands handler */ -let username = "guest"; - let handler = new CommandHandler({ - prefix: "", - buildArguments: (build) => [build.args], + prefix: "", + buildArguments: (build) => [build.args], }); -handler.on("invalidUsage", ({ command, errors }) => { - console.log("/!\\ Invalid Usage!"); - console.log("Usage: " + handler.prettyPrint(command)); - console.log(errors.map((x) => "- " + x.message).join("\n")); -}); +handler.invalidUsageMessage = ({ command, errors }) => { + console.log("/!\\ Invalid Usage!"); + console.log("Usage: " + handler.prettyPrint(command)); + console.log(errors.map(x => "- " + x.message).join("\n")); +}; -handler.on("failedChecks", ({ checks }) => { - console.log("(x) Error: Failed Checks:"); - console.log(checks.map((x) => "- " + x.message).join("\n")); -}); +handler.failedChecksMessage = ({ command, errors }) => { + console.log("(x) Error: Failed Checks:"); + console.log(errors.map(x => "- " + x.message).join("\n")); +}; // -- commands -- handler.registerCommand({ - name: "help", - desc: "Shows commands", - async run(args) { - handler.Commands.forEach((cmd) => { - console.log("> " + cmd.name + " : " + cmd.desc); - if (cmd.args && cmd.args.length) - console.log(" Usage: " + handler.prettyPrint(cmd)); - }); - }, -}); - -handler.registerCommand({ - name: "say", - desc: "Repeats your words", - args: [ - { - type: "text", - rest: true, - }, - ], - async run([text]) { - // Because rest: true, it all gets collected to the first element - console.log(text); - }, -}); - -handler.registerCommand({ - name: "add", - desc: "Add two numbers", - args: [ - { - type: "number", - name: "a", - }, - { - type: "number", - name: "b", - }, - ], - async run([a, b]) { - let sum = a + b; - console.log(a + " + " + b + " = " + sum); - }, + name: "help", + desc: "Shows commands", + async run(args) { + handler.Commands.forEach((cmd) => { + console.log("> " + cmd.name); + console.log(" " + cmd.desc); + console.log(" Usage: " + handler.prettyPrint(cmd)); + }) + }, }); handler.registerCommand({ - name: "exit", - desc: "Exit this example", - async run() { - console.log("OK, bye!"); - process.exit(); - }, + name: "say", + desc: "Repeats your words", + args: [{ + type: "text", + rest: true, + }], + async run([text]) { + // Because rest: true, it all gets collected to the first element + console.log(text); + }, }); handler.registerCommand({ - name: "su", - desc: "Switch user", - args: ["uname:string"], - async run([uname]) { - username = uname; - console.log("Welcome back, " + username + "!"); - }, + name: "add", + desc: "Add two numbers", + args: " ", + async run([a, b]) { + let sum = a + b; + console.log(a + " + " + b + " = " + sum); + }, }); handler.registerCommand({ - name: "make_sandwich", - desc: "No (unless you're 'root')", - checks: [ - async () => { - if (username == "root") { - // Okay. - return { pass: true }; - } else { - return { pass: false, message: "What? Make it yourself." }; - } - }, - ], - async run() { - console.log("Ok, heres your virtual sandwich:"); - console.log(" 🥪 "); - }, + name: "exit", + desc: "Exit this example", + async run() { + console.log("OK, bye!"); + process.exit(); + }, }); var stdin = process.openStdin(); stdin.addListener("data", (d) => { - let input = d.toString().trim(); - handler.run(input); + let input = d.toString().trim(); + handler.run(input); }); -handler.run("help"); +handler.run("help"); \ No newline at end of file diff --git a/examples/discordbot/bot.js b/examples/discordbot/bot.js new file mode 100644 index 0000000..e69de29 diff --git a/examples/discordjs-slash.ts b/examples/discordjs-slash.ts new file mode 100644 index 0000000..a80dc97 --- /dev/null +++ b/examples/discordjs-slash.ts @@ -0,0 +1,48 @@ +import { Client } from "discord.js"; +import { DiscordFramework } from "../src/extensions/discordjs-slash"; +import { config } from "dotenv"; + +config(); + +let fw = new DiscordFramework({ + client: new Client({ + intents: [ + "Guilds" + ] + }), +}); + +fw.registerEvents(); + +fw.slashCommands.add({ + name: "test", + description: "tests stuff", + run({ interaction }) { + interaction.reply({ + content: "hello world", + ephemeral: true, + }); + }, +}) + +fw.slashCommands.add({ + name: "list", + description: "manage lists", + subcommands: { + create: { + description: "creat", + run({ interaction }) { + interaction.reply({ content: "no" }) + }, + } + } +}) + +fw.publishSlashCommandsGuild(process.env.GUILD_ID as string); + +fw.client.on("ready", () => { + console.log("Bot is ready!"); +}); + +fw.login(); + diff --git a/examples/middlewareExample.js b/examples/middlewareExample.js deleted file mode 100644 index 83d9972..0000000 --- a/examples/middlewareExample.js +++ /dev/null @@ -1,122 +0,0 @@ -import { CommandHandler } from "../src/index.js"; - -/* -This example is like consoleExample.js but with middlewares -*/ - -let handler = new CommandHandler(); - -handler.buildArguments = ({ ctx, args }) => [ctx, args]; - -// Initialize a dummy database -let dummyDB = { - dennis: 12, - may: 20, - skyrina: 3, - voltrex: 5, - julia: 16, - guest: 5, -}; - -let currentUser = "guest"; - -// The middleware -handler.use({ - id: "dummydb", - before: "run", - run(execCtx, next) { - execCtx.ctx.balance = dummyDB[currentUser]; - execCtx.ctx.setBalance = (x) => (dummyDB[currentUser] = x); - execCtx.ctx.setBalanceOf = (u, x) => (dummyDB[u] = x); - execCtx.ctx.getBalances = () => dummyDB; - - next(); - }, -}); - -// Pre-defined checks: - -const Checks = { - /** - * Generate a command check - * @param {number} n Index of the argument to check for an user - * @returns {import("../src").CommandCheck} - */ - userMustExist: (n) => { - return async (ctx, args) => { - if (Object.keys(dummyDB).includes(args[n])) { - return { pass: true }; - } else { - return { pass: false, message: "User doesn't exist" }; - } - }; - }, -}; - -handler.addCommand({ - name: "baltop", - desc: "List top balances", - run(ctx, args) { - console.log("== Bal Top =="); - console.log( - Object.entries(ctx.getBalances()) - .map(([name, bal], i) => `${i + 1}. ${name}: ${bal}`) - .join("\n"), - ); - console.log("== Bal Top =="); - }, -}); - -handler.addCommand({ - name: "bal", - desc: "Shows your balance", - run(ctx, args) { - console.log("You have " + ctx.balance + " money."); - }, -}); - -handler.addCommand({ - name: "login", - args: ["user:string"], - checks: [Checks.userMustExist(0)], - desc: "Login as another user", - run(ctx, [user]) { - currentUser = user; - console.log(`Welcome back ${currentUser}`); - }, -}); - -handler.addCommand({ - name: "pay", - args: ["user:string", "amount:number"], - checks: [ - Checks.userMustExist(0), - async (ctx, [user, amount]) => { - // todo check if balance > amount - return { pass: true }; - }, - ], - run(ctx, [user, amount]) { - ctx.setBalanceOf(user, ctx.balance + amount); - ctx.setBalance(ctx.balance - amount); - }, -}); - -handler.registerCommand({ - name: "help", - desc: "this;", - async run() { - handler.Commands.forEach((cmd) => { - console.log("> " + cmd.name + " : " + cmd.desc); - if (cmd.args && cmd.args.length) - console.log(" Usage: " + handler.prettyPrint(cmd)); - }); - }, -}); - -var stdin = process.openStdin(); -stdin.addListener("data", (d) => { - let input = d.toString().trim(); - handler.run(input, {}); -}); -console.log("Type 'help' for a list of commands"); diff --git a/examples/stdin.ts b/examples/stdin.ts new file mode 100644 index 0000000..2c73537 --- /dev/null +++ b/examples/stdin.ts @@ -0,0 +1,48 @@ +import { createInterface } from "readline"; +import { CommandHandler } from "../src"; +import { CommandExecutor, CommandResolver, ContextStatic, SplitString } from "../src/middlewares"; + +let handler = new CommandHandler() + .use(SplitString()) + .use(CommandResolver()) + .use(ContextStatic({ appName: "exampleApp" })) + .use(CommandExecutor()); + +handler.add({ + name: "help", + description: "Show a list of commands", + run({ handler }, []) { + console.log(`Available commands:`); + for (let [name, cmd] of handler.commands.entries()) { + console.log(` - ${name} : ${cmd.description}`); + } + }, +}); + +handler.add({ + name: "hello", + description: "world", + run({}, []) { + console.log("world!"); + }, +}); + +const rl = createInterface({ + input: process.stdin, + output: process.stdout, + prompt: "> ", +}) + +rl.on("line", async (line) => { + await handler.run({ + input: line.trim(), + }); + rl.prompt(); +}); + +rl.on("close", () => { + console.log('exiting...'); + process.exit(0); +}); + +rl.prompt(); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..b413e10 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/package.json b/package.json index 42ac3de..63f7fa2 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,62 @@ { - "name": "string-commands", - "version": "1.1.1", - "description": "A powerful command handler and parser for all your needs. Includes checks, custom arguments, middlewares and more.", - "type": "module", - "main": "./src/index.js", - "exports": { - "discordjs": "./plugins/discord.js", - "default": "./src/index.js" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/TheAlan404/string-commands.git" - }, - "keywords": [ - "command-handler", - "command-handlers", - "command", - "handler", - "parser", - "commands", - "string-manipulation", - "command-pattern", - "command-line-parser", - "discord.js", - "discord" - ], - "author": "alan404", - "license": "GPL-3.0-or-later", - "bugs": { - "url": "https://github.com/TheAlan404/string-commands/issues" - }, - "homepage": "https://github.com/TheAlan404/string-commands#readme", - "devDependencies": { - "prettier": "^2.6.2" - }, - "prettier": { - "semi": true, - "singleQuote": false, - "trailingComma": "all", - "htmlWhitespaceSensitivity": "ignore", - "useTabs": true, - "tabWidth": 4 - } -} + "name": "string-commands", + "version": "2.0.0", + "description": "A new way to handle commands", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "tsx": "tsx" + }, + "homepage": "https://github.com/TheAlan404/string-commands#readme", + "dependencies": { + "@alan404/enum": "^0.2.1", + "tiny-typed-emitter": "^2.1.0" + }, + "optionalDependencies": { + "discord.js": "^14.14.1", + "dotenv": "^16.4.1", + "lowdb": "^7.0.1", + "react-reconciler": "^0.29.0" + }, + "devDependencies": { + "@types/node": "^20.11.6", + "prettier": "^2.6.2", + "simplytyped": "^3.3.0", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + }, + "prettier": { + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "htmlWhitespaceSensitivity": "ignore", + "useTabs": true, + "tabWidth": 4 + }, + "repository": { + "type": "git", + "url": "git+https://github.com/TheAlan404/string-commands.git" + }, + "keywords": [ + "command-handler", + "command-handlers", + "command", + "handler", + "parser", + "commands", + "string-manipulation", + "command-pattern", + "command-line-parser", + "discord.js", + "discord" + ], + "author": "alan404", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/TheAlan404/string-commands/issues" + } +} \ No newline at end of file diff --git a/plugins/discord.js b/plugins/discord.js deleted file mode 100644 index fce79ab..0000000 --- a/plugins/discord.js +++ /dev/null @@ -1,163 +0,0 @@ -import { CommandHandler } from "../src/CommandHandler.js"; -import { Client, channelMention, ChannelType } from "discord.js"; - -/** - * @type {Object} - */ -const DiscordUsages = { - _mentionable: { - type: "text", - async parse(ctx) { - return ctx.arg.replace(/[<@#!&>]/g, ""); - }, - }, - - user: { - type: "_mentionable", - async parse(ctx) { - let user = ctx.context.client.users - .fetch(ctx.arg) - .catch(() => null); - - if (!user) { - return ctx.fail(`${ctx.opts.bot ? "Bot" : "User"} not found!`); - } - - if (ctx.opts.bot && !user.bot) - return ctx.fail( - `${ctx.style.bold(user.username)} isn't a bot!`, - ); - else if (!opts.bot && user.bot) - return ctx.fail(`${ctx.style.bold(user.username)} is a bot!`); - - return { parsed: user }; - }, - }, - - member: { - type: "user", - async parse(ctx) { - const { guild } = ctx.context.message; - const member = await guild.members - .fetch(ctx.arg.id) - .catch(() => null); - - if (!member) - return ctx.fail( - `${ctx.style.bold( - ctx.arg.username, - )} isn't on ${ctx.style.bold(guild.name)}!`, - ); - - return { parsed: member }; - }, - }, - - role: { - type: "_mentionable", - async parse(ctx) { - const { guild } = ctx.context.message; - const role = await guild.roles.fetch(ctx.arg).catch(() => null); - - if (!role) return ctx.fail(`Role not found!`); - - return { parsed: role }; - }, - }, - - channel: { - type: "_mentionable", - async parse(ctx) { - const { guild } = ctx.context.message; - const channel = await guild.channels - .fetch(ctx.arg) - .catch(() => null); - - if (!channel) return ctx.fail(`Channel not found!`); - - if ( - ctx.opts.channelType !== undefined && - ctx.opts.channelType != channel.type - ) { - return ctx.fail( - `${channelMention(channel.id)} isn't ${ctx.style.bold( - ChannelType[ctx.opts.channelType], - )}`, - ); - } - - return { parsed: channel }; - }, - }, - - textChannel: { - type: "channel", - channelType: ChannelType.GuildText, - }, - - voiceChannel: { - type: "channel", - channelType: ChannelType.GuildVoice, - }, -}; - -const DiscordChecks = { - requirePermissions(permissions) {}, - - async requireGuildOwner(client, msg, args) { - return msg.guild.ownerId == msg.author.id - ? { - pass: true, - } - : { - pass: false, - message: "You must be the owner of this guild", - }; - }, -}; - -/** - * @typedef {strcmd.CommandHandlerOptions} DiscordCommandHandlerOptions - */ - -class DiscordCommandHandler extends CommandHandler { - /** - * - * @param {Client} client The discord client - * @param {DiscordCommandHandlerOptions} opts - Options for the command handler - */ - constructor(client, opts = {}) { - super( - Object.assign( - { - argumentParser: { - styling: { - arg: (x) => `\`${x}\``, - bold: (x) => `**${x}**`, - }, - }, - }, - opts, - ), - ); - this.client = client; - this.client.handler = this; - - for (let [id, usage] of Object.entries(DiscordUsages)) - this.registerUsage(id, usage); - } - - buildArguments({ args, ctx }) { - return [ctx.client, ctx.message, args]; - } - - // handles discord.js messages - handleMessage(msg) { - this.run(msg.content, { - message: msg, - client: this.client, - }); - } -} - -export { DiscordCommandHandler, DiscordUsages }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6f157af --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,651 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@alan404/enum': + specifier: ^0.2.1 + version: 0.2.1 + tiny-typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + optionalDependencies: + discord.js: + specifier: ^14.14.1 + version: 14.14.1 + dotenv: + specifier: ^16.4.1 + version: 16.4.1 + lowdb: + specifier: ^7.0.1 + version: 7.0.1 + react-reconciler: + specifier: ^0.29.0 + version: 0.29.0(react@18.2.0) + devDependencies: + '@types/node': + specifier: ^20.11.6 + version: 20.11.6 + prettier: + specifier: ^2.6.2 + version: 2.8.8 + simplytyped: + specifier: ^3.3.0 + version: 3.3.0(typescript@5.3.3) + tsx: + specifier: ^4.7.0 + version: 4.7.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + '@alan404/enum@0.2.1': + resolution: {integrity: sha512-cZcDbVcBrvv1xWnjA8UjqPVh2dfKThli3QeiACMBUBOi3Jbhh8K64KrKmHBKzwEn9tAasyESXLeN8D+sjWxeEg==} + + '@discordjs/builders@1.7.0': + resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.0.0': + resolution: {integrity: sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.3.3': + resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.2.0': + resolution: {integrity: sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==} + engines: {node: '>=16.11.0'} + + '@discordjs/util@1.0.2': + resolution: {integrity: sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==} + engines: {node: '>=16.11.0'} + + '@discordjs/ws@1.0.2': + resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} + engines: {node: '>=16.11.0'} + + '@esbuild/aix-ppc64@0.19.11': + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.19.11': + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.19.11': + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.19.11': + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.19.11': + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.11': + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.19.11': + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.11': + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.19.11': + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.19.11': + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.19.11': + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.19.11': + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.19.11': + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.19.11': + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.11': + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.19.11': + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.19.11': + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.19.11': + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.19.11': + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.19.11': + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.19.11': + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.19.11': + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.19.11': + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@fastify/busboy@2.1.0': + resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} + engines: {node: '>=14'} + + '@sapphire/async-queue@1.5.2': + resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@3.9.6': + resolution: {integrity: sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g==} + engines: {node: '>=v18'} + + '@sapphire/snowflake@3.5.1': + resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@types/node@20.11.6': + resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + + '@types/ws@8.5.9': + resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} + + '@vladfrangu/async_event_emitter@2.2.4': + resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + discord-api-types@0.37.61: + resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} + + discord.js@14.14.1: + resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} + engines: {node: '>=16.11.0'} + + dotenv@16.4.1: + resolution: {integrity: sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==} + engines: {node: '>=12'} + + esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + + magic-bytes.js@1.8.0: + resolution: {integrity: sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q==} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + react-reconciler@0.29.0: + resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^18.2.0 + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + + simplytyped@3.3.0: + resolution: {integrity: sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==} + peerDependencies: + typescript: '>=2.8.0' + + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + + ts-mixer@6.0.3: + resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + tsx@4.7.0: + resolution: {integrity: sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici@5.27.2: + resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} + engines: {node: '>=14.0'} + + ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@alan404/enum@0.2.1': {} + + '@discordjs/builders@1.7.0': + dependencies: + '@discordjs/formatters': 0.3.3 + '@discordjs/util': 1.0.2 + '@sapphire/shapeshift': 3.9.6 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.3 + tslib: 2.6.2 + optional: true + + '@discordjs/collection@1.5.3': + optional: true + + '@discordjs/collection@2.0.0': + optional: true + + '@discordjs/formatters@0.3.3': + dependencies: + discord-api-types: 0.37.61 + optional: true + + '@discordjs/rest@2.2.0': + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@sapphire/snowflake': 3.5.1 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + magic-bytes.js: 1.8.0 + tslib: 2.6.2 + undici: 5.27.2 + optional: true + + '@discordjs/util@1.0.2': + optional: true + + '@discordjs/ws@1.0.2': + dependencies: + '@discordjs/collection': 2.0.0 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@sapphire/async-queue': 1.5.2 + '@types/ws': 8.5.9 + '@vladfrangu/async_event_emitter': 2.2.4 + discord-api-types: 0.37.61 + tslib: 2.6.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + optional: true + + '@esbuild/aix-ppc64@0.19.11': + optional: true + + '@esbuild/android-arm64@0.19.11': + optional: true + + '@esbuild/android-arm@0.19.11': + optional: true + + '@esbuild/android-x64@0.19.11': + optional: true + + '@esbuild/darwin-arm64@0.19.11': + optional: true + + '@esbuild/darwin-x64@0.19.11': + optional: true + + '@esbuild/freebsd-arm64@0.19.11': + optional: true + + '@esbuild/freebsd-x64@0.19.11': + optional: true + + '@esbuild/linux-arm64@0.19.11': + optional: true + + '@esbuild/linux-arm@0.19.11': + optional: true + + '@esbuild/linux-ia32@0.19.11': + optional: true + + '@esbuild/linux-loong64@0.19.11': + optional: true + + '@esbuild/linux-mips64el@0.19.11': + optional: true + + '@esbuild/linux-ppc64@0.19.11': + optional: true + + '@esbuild/linux-riscv64@0.19.11': + optional: true + + '@esbuild/linux-s390x@0.19.11': + optional: true + + '@esbuild/linux-x64@0.19.11': + optional: true + + '@esbuild/netbsd-x64@0.19.11': + optional: true + + '@esbuild/openbsd-x64@0.19.11': + optional: true + + '@esbuild/sunos-x64@0.19.11': + optional: true + + '@esbuild/win32-arm64@0.19.11': + optional: true + + '@esbuild/win32-ia32@0.19.11': + optional: true + + '@esbuild/win32-x64@0.19.11': + optional: true + + '@fastify/busboy@2.1.0': + optional: true + + '@sapphire/async-queue@1.5.2': + optional: true + + '@sapphire/shapeshift@3.9.6': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + optional: true + + '@sapphire/snowflake@3.5.1': + optional: true + + '@types/node@20.11.6': + dependencies: + undici-types: 5.26.5 + + '@types/ws@8.5.9': + dependencies: + '@types/node': 20.11.6 + optional: true + + '@vladfrangu/async_event_emitter@2.2.4': + optional: true + + discord-api-types@0.37.61: + optional: true + + discord.js@14.14.1: + dependencies: + '@discordjs/builders': 1.7.0 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.3.3 + '@discordjs/rest': 2.2.0 + '@discordjs/util': 1.0.2 + '@discordjs/ws': 1.0.2 + '@sapphire/snowflake': 3.5.1 + '@types/ws': 8.5.9 + discord-api-types: 0.37.61 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + tslib: 2.6.2 + undici: 5.27.2 + ws: 8.14.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + optional: true + + dotenv@16.4.1: + optional: true + + esbuild@0.19.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + + fast-deep-equal@3.1.3: + optional: true + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.7.2: + dependencies: + resolve-pkg-maps: 1.0.0 + + js-tokens@4.0.0: + optional: true + + lodash.snakecase@4.1.1: + optional: true + + lodash@4.17.21: + optional: true + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + optional: true + + lowdb@7.0.1: + dependencies: + steno: 4.0.2 + optional: true + + magic-bytes.js@1.8.0: + optional: true + + prettier@2.8.8: {} + + react-reconciler@0.29.0(react@18.2.0): + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + optional: true + + react@18.2.0: + dependencies: + loose-envify: 1.4.0 + optional: true + + resolve-pkg-maps@1.0.0: {} + + scheduler@0.23.0: + dependencies: + loose-envify: 1.4.0 + optional: true + + simplytyped@3.3.0(typescript@5.3.3): + dependencies: + typescript: 5.3.3 + + steno@4.0.2: + optional: true + + tiny-typed-emitter@2.1.0: {} + + ts-mixer@6.0.3: + optional: true + + tslib@2.6.2: + optional: true + + tsx@4.7.0: + dependencies: + esbuild: 0.19.11 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.3.3: {} + + undici-types@5.26.5: {} + + undici@5.27.2: + dependencies: + '@fastify/busboy': 2.1.0 + optional: true + + ws@8.14.2: + optional: true diff --git a/src/_/Command.ts b/src/_/Command.ts new file mode 100644 index 0000000..bf9eef3 --- /dev/null +++ b/src/_/Command.ts @@ -0,0 +1,6 @@ +export type CommandMetadata = { + name: string, + description?: string, +}; + + diff --git a/src/builtin/index.ts b/src/builtin/index.ts new file mode 100644 index 0000000..03f1840 --- /dev/null +++ b/src/builtin/index.ts @@ -0,0 +1 @@ +export * from "./middlewares" diff --git a/src/builtin/middlewares/_/CommandExecutor.ts b/src/builtin/middlewares/_/CommandExecutor.ts new file mode 100644 index 0000000..8fba0cc --- /dev/null +++ b/src/builtin/middlewares/_/CommandExecutor.ts @@ -0,0 +1,19 @@ +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; +import { CommandReplierCtx } from "./CommandReplier"; +import { CommandResolverCtx } from "./CommandResolver"; +import { SplitStringCtx } from "./SplitString"; + +export const CommandExecutor = () => (async (ctx: C): Promise => { + let { targetCommand, handler } = ctx; + + try { + await targetCommand.run(ctx, []); + } catch(e) { + handler.emit("commandError", e, ctx); + return; + } + + return ctx; +}); diff --git a/src/builtin/middlewares/_/CommandReplier.ts b/src/builtin/middlewares/_/CommandReplier.ts new file mode 100644 index 0000000..3884d21 --- /dev/null +++ b/src/builtin/middlewares/_/CommandReplier.ts @@ -0,0 +1,10 @@ +import { BaseContext } from "../../_/Context"; +import { MiddlewareFactory } from "../../../core/middleware/Middleware"; + +export interface ReplyData extends Record { + type: string, +} + +export interface CommandReplierCtx { + reply?: (data: T, ctx: BaseContext & CommandReplierCtx) => PromiseLike, +} diff --git a/src/builtin/middlewares/_/CommandResolver.ts b/src/builtin/middlewares/_/CommandResolver.ts new file mode 100644 index 0000000..03e5389 --- /dev/null +++ b/src/builtin/middlewares/_/CommandResolver.ts @@ -0,0 +1,36 @@ +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; +import { CommandReplierCtx } from "./CommandReplier"; +import { SplitStringCtx } from "./SplitString"; + +export type ReplyCommandNotFound = { + type: "commandNotFound", + commandName: string, +}; + +export interface CommandResolverCtx { + rootCommand: BaseCommand, + targetCommand: BaseCommand, +} + +export const CommandResolver = () => (async & BaseContext)>(ctx: T): Promise => { + let { handler, commandName, reply } = ctx; + + if (!handler.commands.has(commandName)) { + reply?.({ + type: "commandNotFound", + commandName, + }, ctx); + return; + } + + let rootCommand = handler.commands.get(commandName); + + return { + ...ctx, + rootCommand, + // TODO: resolve subcommands + targetCommand: rootCommand, + }; +}); diff --git a/src/builtin/middlewares/_/MultiPrefix.ts b/src/builtin/middlewares/_/MultiPrefix.ts new file mode 100644 index 0000000..89ce44a --- /dev/null +++ b/src/builtin/middlewares/_/MultiPrefix.ts @@ -0,0 +1,16 @@ +import { BaseContext } from "../../_/Context"; + +export const MultiPrefix = ({ + prefixes = ["!"], +}: { + prefixes: string[], +}) => (async (ctx: T): Promise => { + let { input } = ctx; + + if(!prefixes.some(p => input.startsWith(p))) return; + + return { + ...ctx, + input: input.slice(prefixes.find(p => input.startsWith(p)).length), + }; +}); diff --git a/src/builtin/middlewares/_/Prefix.ts b/src/builtin/middlewares/_/Prefix.ts new file mode 100644 index 0000000..6d22340 --- /dev/null +++ b/src/builtin/middlewares/_/Prefix.ts @@ -0,0 +1,16 @@ +import { BaseContext } from "../../_/Context"; + +export const Prefix = ({ + prefix = "!", +}: { + prefix: string, +}) => (async (ctx: T): Promise => { + let { input } = ctx; + + if(!input.startsWith(prefix)) return; + + return { + ...ctx, + input: input.slice(prefix.length), + }; +}); diff --git a/src/builtin/middlewares/_/SplitString.ts b/src/builtin/middlewares/_/SplitString.ts new file mode 100644 index 0000000..60d1509 --- /dev/null +++ b/src/builtin/middlewares/_/SplitString.ts @@ -0,0 +1,20 @@ +import { BaseCommand } from "../../../_/Command"; +import { BaseContext } from "../../_/Context"; +import { Middleware, MiddlewareFactory } from "../../../core/middleware/Middleware"; + +export interface SplitStringCtx { + commandName: string, + commandArguments: string, +} + +export const SplitString = () => (async (ctx: T): Promise => { + let { input } = ctx; + + let [commandName, ...args] = input.split(" "); + + return { + ...ctx, + commandName, + commandArguments: args.join(" "), + }; +}); diff --git a/src/builtin/middlewares/_/index.ts b/src/builtin/middlewares/_/index.ts new file mode 100644 index 0000000..6b88061 --- /dev/null +++ b/src/builtin/middlewares/_/index.ts @@ -0,0 +1,6 @@ +export * from "./_/CommandExecutor"; +export * from "./_/CommandReplier"; +export * from "./_/CommandResolver"; +export * from "./base/ContextStatic"; +export * from "./_/SplitString"; +export * from "./base/Inspect"; diff --git a/src/builtin/middlewares/base/Inspect.ts b/src/builtin/middlewares/base/Inspect.ts new file mode 100644 index 0000000..f175b83 --- /dev/null +++ b/src/builtin/middlewares/base/Inspect.ts @@ -0,0 +1,8 @@ +import { NOOPMiddleware } from "../../../core"; + +export type InspectCallback = (ctx: T) => any; + +export const Inspect = (fn: InspectCallback = console.log): NOOPMiddleware => ((ctx) => { + fn(ctx); + return ctx; +}) diff --git a/src/builtin/middlewares/base/Static.ts b/src/builtin/middlewares/base/Static.ts new file mode 100644 index 0000000..ed55be8 --- /dev/null +++ b/src/builtin/middlewares/base/Static.ts @@ -0,0 +1,10 @@ +import { PlainObject } from "simplytyped"; +import { Middleware } from "../../../core"; + +export const Static = < + T extends PlainObject, + Input extends object, +>(obj: T): Middleware => ((ctx) => ({ + ...ctx, + ...obj, +})); diff --git a/src/builtin/middlewares/base/index.ts b/src/builtin/middlewares/base/index.ts new file mode 100644 index 0000000..7d9a5f1 --- /dev/null +++ b/src/builtin/middlewares/base/index.ts @@ -0,0 +1,2 @@ +export * from "./Static"; +export * from "./Inspect"; diff --git a/src/builtin/middlewares/index.ts b/src/builtin/middlewares/index.ts new file mode 100644 index 0000000..955fdd1 --- /dev/null +++ b/src/builtin/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./base"; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..4e2bde6 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,2 @@ +export * from "./middleware" +export * from "./pipeline" diff --git a/src/core/middleware/IO.ts b/src/core/middleware/IO.ts new file mode 100644 index 0000000..0516e3e --- /dev/null +++ b/src/core/middleware/IO.ts @@ -0,0 +1,28 @@ +import { AnyMiddleware, Middleware } from "./Middleware"; +import { MiddlewareFactory } from "./MiddlewareFactory"; + +type Concat = T extends [infer A] ? A : ( + T extends [infer A, ...infer Rest] ? A & Concat : never +); + +export type MiddlewareOutput = M extends Middleware ? O : never; +export type MiddlewareInput = M extends Middleware ? I : never; + +export type MiddlewareOutputs = { + [Index in keyof Types]: MiddlewareOutput +}; + +export type MiddlewareInputs = { + [Index in keyof Types]: MiddlewareInput +}; + +export type MiddlewareResolvable = AnyMiddleware | MiddlewareFactory; +export type ResolveMiddleware = T extends MiddlewareFactory ? Middleware : ( + T extends Middleware ? Middleware : T +); +export type ResolveMiddlewares = { + [Index in keyof T]: ResolveMiddleware; +}; + +export type ExtractOutputs = Concat>> +export type ExtractInputs = Concat>> diff --git a/src/core/middleware/Middleware.ts b/src/core/middleware/Middleware.ts new file mode 100644 index 0000000..1c7c9fb --- /dev/null +++ b/src/core/middleware/Middleware.ts @@ -0,0 +1,12 @@ +import { PromiseOr } from "simplytyped" + +export type Middleware = ((ctx: T) => PromiseOr) & { displayName?: string }; +export type AnyMiddleware = Middleware; + +export const createMiddleware = (mw: Middleware, displayName?: string) => { + mw.displayName = displayName; + return mw; +}; + +export type NOOPMiddleware = Middleware; +export const NOOPMiddleware: NOOPMiddleware = createMiddleware((x: T) => x, "NOOPMiddleware"); diff --git a/src/core/middleware/MiddlewareFactory.ts b/src/core/middleware/MiddlewareFactory.ts new file mode 100644 index 0000000..dc13717 --- /dev/null +++ b/src/core/middleware/MiddlewareFactory.ts @@ -0,0 +1,9 @@ +import { Middleware } from "./Middleware"; + +export type MiddlewareFactory = + (options: Options) => Middleware; + + + + + diff --git a/src/core/middleware/MiddlewareList.ts b/src/core/middleware/MiddlewareList.ts new file mode 100644 index 0000000..2b561fd --- /dev/null +++ b/src/core/middleware/MiddlewareList.ts @@ -0,0 +1,22 @@ +import { Prev } from "simplytyped" +import { Middleware } from "./Middleware" + +type ParseInt = + T extends any + ? (T extends `${infer Digit extends number}` + ? Digit + : never) + : never + +export type MiddlewareList = { + [Index in keyof List]: ( + Middleware< + Index extends "0" ? ( + Input + ) : ( + List[Prev>] + ), + List[Index] + > + ) +} diff --git a/src/core/middleware/MiddlewareMixin.ts b/src/core/middleware/MiddlewareMixin.ts new file mode 100644 index 0000000..1d1bf82 --- /dev/null +++ b/src/core/middleware/MiddlewareMixin.ts @@ -0,0 +1,10 @@ +import { ExtractOutputs, MiddlewareResolvable } from "./IO"; +import { Middleware } from "./Middleware"; + +export type MiddlewareMixin = Middleware< + ExtractOutputs, + ExtractOutputs & Provides +>; + +export type Provider = Middleware; +export type Consumer = Middleware; diff --git a/src/core/middleware/index.ts b/src/core/middleware/index.ts new file mode 100644 index 0000000..62080d6 --- /dev/null +++ b/src/core/middleware/index.ts @@ -0,0 +1,5 @@ +export * from "./Middleware" +export * from "./MiddlewareList" +export * from "./MiddlewareFactory" +export * from "./IO" +export * from "./MiddlewareMixin" diff --git a/src/core/pipeline/Pipeline.ts b/src/core/pipeline/Pipeline.ts new file mode 100644 index 0000000..9eb1b6e --- /dev/null +++ b/src/core/pipeline/Pipeline.ts @@ -0,0 +1,75 @@ +import { Enum } from "@alan404/enum"; +import { AnyMiddleware, Middleware, MiddlewareList } from "../middleware"; + +type ArrayLast = T extends [...infer _, infer Last] ? Last : never; +type ArrayFirst = T extends [infer First, ...infer _] ? First : never; +type ArraySliceFirst = T extends [infer _, ...infer Tail] ? Tail : never; + +export type Pipeline = { + middlwares: MiddlewareList, ArraySliceFirst>; + pipe: (mw: Middleware, Next>) => + Pipeline<[...Types, Next]>; + execute: (initial: ArrayFirst) => Promise>>; + fire: (initial: ArrayFirst) => Pipeline; +} + +export type ExecutionResult = Enum<{ + success: T; + cancelled: { + middleware: AnyMiddleware; + }; + error: { + middleware: AnyMiddleware; + error: Error; + }; +}>; + +export const createPipeline = < + Input, + Output extends Input = Input, +>(mw: Middleware): Pipeline<[Input, Output]> => { + let pipeline = { + middlwares: [mw], + pipe: (next) => { + pipeline.middlwares.push(next); + return pipeline; + }, + execute: async (initial: Input) => { + let ctx = initial as any; + for(let middleware of pipeline.middlwares) { + let next: any; + try { + next = await middleware(ctx); + } catch(error) { + return ({ + type: "error", + data: { + middleware, + error, + }, + } as ExecutionResult); + } + + if(!next) return ({ + type: "cancelled", + data: { + middleware, + }, + } as ExecutionResult); + + ctx = next; + } + + return ({ + type: "success", + data: ctx, + } as ExecutionResult); + }, + fire: (initial: Input) => { + pipeline.execute(initial); + return pipeline; + }, + } + + return pipeline as Pipeline<[Input, Output]>; +}; diff --git a/src/core/pipeline/index.ts b/src/core/pipeline/index.ts new file mode 100644 index 0000000..3c19322 --- /dev/null +++ b/src/core/pipeline/index.ts @@ -0,0 +1 @@ +export * from "./Pipeline" diff --git a/src/extensions/discordjs-react.ts b/src/extensions/discordjs-react.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/extensions/discordjs-slash.ts b/src/extensions/discordjs-slash.ts new file mode 100644 index 0000000..cfc4cea --- /dev/null +++ b/src/extensions/discordjs-slash.ts @@ -0,0 +1,172 @@ +import { ButtonInteraction, CacheType, ChatInputCommandInteraction, Client, Events, Interaction, MessageComponentInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, REST, RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, SlashCommandBuilder, UserContextMenuCommandInteraction } from "discord.js"; +import { BaseContext, BaseCommand, CommandHandler, Middleware } from ".."; +import { CommandExecutor, CommandResolverCtx, ContextStatic } from "../builtin/middlewares"; +import { TypedEmitter } from "tiny-typed-emitter"; + +export interface DiscordClientCtx { + client: Client, +}; + +export interface InteractionCtx { + interaction: T, +} + +export type CommonContext = BaseContext & DiscordClientCtx; + +export class DiscordFramework extends TypedEmitter<{}> { + client: Client; + clientId: string; + rest: REST; + + slashCommands: CommandHandler>; + modals: CommandHandler>; + buttons: CommandHandler>; + userContextMenuCommands: CommandHandler>; + messageContextMenuCommands: CommandHandler>; + messageComponents: CommandHandler>; + messageCommands: CommandHandler; + + constructor({ + client, + token, + clientId, + }: { + client: Client, + token?: string, + clientId?: string, + }) { + super(); + + this.client = client; + this.client.token = token || process.env.DISCORD_TOKEN || process.env.TOKEN; + this.clientId = clientId || process.env.CLIENT_ID; + + this.rest = new REST() + .setToken(this.client.token); + + this.slashCommands = new CommandHandler() + .use(ContextStatic({ client })) + .use(async & DiscordClientCtx>(ctx: T): Promise => { + let cmd = ctx.handler.commands.get(ctx.interaction.commandName); + + let targetCommand = cmd; + + let subcommandGroup = ctx.interaction.options.getSubcommandGroup(); + let subcommand = ctx.interaction.options.getSubcommand(); + if(subcommandGroup) { + targetCommand = cmd.subcommands[subcommandGroup].subcommands[subcommand]; + } else if(subcommand) { + targetCommand = cmd.subcommands[subcommand]; + } + + return { + ...ctx, + rootCommand: cmd, + targetCommand, + }; + }); + + this.messageCommands = new CommandHandler() + .use(ContextStatic({ client })); + + } + + use(mw: Middleware): typeof this { + // @ts-ignore + this.slashCommands.use(mw); + // @ts-ignore + this.messageCommands.use(mw); + // @ts-ignore + return this; + } + + useExecutor() { + // @ts-ignore + this.slashCommands = this.slashCommands.use(CommandExecutor()); + // @ts-ignore + this.messageCommands = this.messageCommands.use(CommandExecutor()); + } + + getSlashCommandData() { + const toData = (cmd: BaseCommand) => { + let builder = new SlashCommandBuilder(); + builder.setName(cmd.name); + builder.setDescription(cmd.description || ""); + + if (cmd.subcommands) { + for (let [name, subcommand] of Object.entries(cmd.subcommands)) { + let isGroup = !!subcommand.subcommands; + if (isGroup) { + builder.addSubcommandGroup( + (group) => { + group + .setName(name) + .setDescription(subcommand.description || ""); + + for (let [name, sub] of Object.entries(subcommand.subcommands)) { + group.addSubcommand( + b => b.setName(name).setDescription(sub.description || "") + ) + } + + return group; + } + ); + } else { + builder.addSubcommand( + b => b.setName(name).setDescription(subcommand.description || "") + ) + } + } + } + + return builder.toJSON(); + } + + let arr: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; + + for(let [name, cmd] of this.slashCommands.commands.entries()) { + // @ts-ignore + arr.push(toData(cmd)); + } + + return arr; + } + + async publishSlashCommandsGuild(guildId: string) { + let data = this.getSlashCommandData(); + + return await this.rest.put( + Routes.applicationGuildCommands(this.clientId, guildId), + { body: data } + ); + } + + async publishCommandsGlobal() { + let data = this.getSlashCommandData(); + + return await this.rest.put( + Routes.applicationCommands(this.clientId), + { body: data } + ); + } + + registerEvents() { + this.client.on(Events.InteractionCreate, this.onInteractionCreate.bind(this)); + } + + async onInteractionCreate(interaction: Interaction) { + if(interaction.isChatInputCommand()) { + this.slashCommands.run({ + input: interaction.commandName, + interaction, + }) + } else if (interaction.isMessageComponent()) { + + } + } + + async login() { + return await this.client.login(); + } +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts new file mode 100644 index 0000000..e5a6e86 --- /dev/null +++ b/src/extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./discordjs-slash"; +export * from "./lowdb"; diff --git a/src/extensions/lowdb.ts b/src/extensions/lowdb.ts new file mode 100644 index 0000000..4b3fe64 --- /dev/null +++ b/src/extensions/lowdb.ts @@ -0,0 +1,20 @@ +import { Low } from "lowdb"; +import { BaseContext } from "../_/Context"; + +export interface LowDBCtx { + db: Low, +} + +export const LowDBExtension = (low: Low) => { + low.read(); + + return async (ctx: C): Promise> => ({ + ...ctx, + db: low, + }); +}; + +export const LowDBSave = () => (async >(ctx: C): Promise => { + await ctx.db.write(); + return ctx; +}) diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 0946e77..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,271 +0,0 @@ -/// - -import { EventEmitter } from "node:events"; - -type ValueOfMap = M extends Map ? V : never -type KeyOfMap = M extends Map ? K : never -type ConvertToMap = Map; -type AnyFunction = (...a: any) => any; -type ReturnTypePromise = ReturnType extends Promise ? U : ReturnType; -type KeyOfAsString = Extract; - -export as namespace stringcommands; -export as namespace strcmd; - -type BaseRunnerArgs = any[]; -type BaseUsageCollection = { [type: string]: UsageParser }; - -type ResolveUsageParser = V extends UsageParser ? V : - V extends Usage ? (V["type"] extends string ? TUsages[V["type"]] : never) : - V extends string ? - V extends `${"<" | "[" | ""}${infer NA}${"..." | ""}${">" | "]" | ""}` ? - NA extends `${infer N}:${infer RT}` ? TUsages[RT] : TUsages[NA] - : never - : never; -type _getUsageParserTOutput = TUsage extends UsageParser ? - TOutput : (TUsage extends { parse: (...any) => { parsed: infer T } } ? T : never); - -type StringToUsage = - { - rest: V extends `${string}...` ? true : false, - optional: V extends `[${string}]` ? true : false, - type: V extends `${"<" | "[" | ""}${infer Mid}${">" | "]" | ""}` ? - Mid extends `${infer Name}:${infer Type}` ? - Type - : Mid - : never, - name: V extends `${"<" | "[" | ""}${infer Name}:${infer Type}${">" | "]" | ""}` ? - Name - : "", - }; - -export type Command< - TName extends string, - TAliases extends string[] | null, - TRunnerArgs extends BaseRunnerArgs, - TUsages extends BaseUsageCollection, -> = { - name: TName, - run(...args: TRunnerArgs): Promise | void, - - aliases?: TAliases, - args?: UsageResolvableList, - checks: CommandCheck[], -}; - -export type CommandCheck = (...args: TRunnerArgs[]) => Promise; - -export interface CommandCheckPass { pass: true } -export interface CommandCheckFail { pass: false, message: string } - -export type CommandCheckResult = CommandCheckPass | CommandCheckFail; - -export interface CommandHandlerOptions { - prefix: string, - log: typeof console | { log: AnyFunction, info: AnyFunction, error: AnyFunction, } | false, -} - -/** - * The string-commands command handler - */ -export class CommandHandler< - Opts extends CommandHandlerOptions, - - CustomContext, - RunnerArgs extends BaseRunnerArgs, - - _commands extends { - [CName: string]: Command< - typeof CName, - any, - RunnerArgs, - _usages - > - }, - _aliases extends { [CAlias in _commands[keyof _commands]["aliases"][number]as string]: keyof _commands }, - _usages extends { - [P in {} as string]: UsageParser< - CustomContext, - _usages, - any, - any - > - }, - _middlewares extends CommandHandlerMiddleware<_middlewares>[], - - _execContext extends ExecutorContext, -> extends EventEmitter { - constructor(opts?: Opts) - - prefix: Opts["prefix"] | string; - Commands: ConvertToMap<_commands>; - Aliases: ConvertToMap<_aliases>; - middlewares: _middlewares; - - argumentParser: ArgumentParser; - - registerCommand(cmd: Command< - string, - string[], - RunnerArgs, - _usages - >): this; - registerCommands(path: string): Promise; - - addCommand(cmd: Command< - string, - string[], - RunnerArgs, - _usages - >): this; - addCommands(path: string): Promise; - - on(event: "invalidUsage", handler: (ctx: _execContext) => void): this; - on(event: "failedChecks", handler: (ctx: _execContext) => void): this; - - buildArguments(ctx: _execContext): RunnerArgs; - - use(mw: CommandHandlerMiddleware<_middlewares>): this; - - run(input: string, ctx: CustomContext): this; - - prettyPrint(cmd: _commands[string]): string; -} - -/** - * The command executor context, used in various parts of the command handler. - */ -type ExecutorContext< - CustomContext, - TCommand extends Command, - TRunnerArgs extends BaseRunnerArgs, - TUsages extends BaseUsageCollection, -> = { - /** The raw input string */ - input: string, - /** The custom context */ - ctx: CustomContext, - /** Name of the executing command */ - name?: TCommand["name"], - /** Unparsed arguments (raw) from the input string */ - rawArgs?: string[], - /** The command's information */ - command?: TCommand, - /** List of usage (argument parser) errors */ - errors?: UsageError[], - /** Parsed arguments/usages */ - args?: any[], // TODO: typescript voodoo to make it relationship >:3 - /** Built runner args */ - runArgs?: TRunnerArgs, - /** List of failed checks */ - checks?: CommandCheckFail[], - /** An error, if occured while executing the command */ - error?: Error, -}; - -// Usage System - -class ArgumentParser< - CustomContext, - _usages extends BaseUsageCollection, -> { - ArgumentParsers: ConvertToMap<_usages>; - - parseUsages[]>(text: string, - commandUsages: T, - ctx: CustomContext): { - args: { [I in keyof T]: _getUsageParserTOutput> }, - errors: [], - }; -} - -export type UsageResolvableList = UsageResolvable[]; - -export type UsageResolvable = - `${`${"<" | ""}${KeyOfAsString | `${string}:${KeyOfAsString}`}${">" | ""}` - | `[${KeyOfAsString | `${string}:${KeyOfAsString}`}]` - }${"..." | ""}` | { type: KeyOfAsString }; - -export interface Usage< - CustomContext, - TName extends KeyOfAsString, - TUsages extends BaseUsageCollection -> extends UsageParser< - CustomContext, - TUsages, - any, - any -> { - name: TName, - type: UsageResolvable, - optional?: boolean, -} - -export interface UsageParser< - CustomContext, - TUsages extends BaseUsageCollection, - TOutput, - TOpts, -> { - // The second in this union is for Usage compat. - type: string | { type: string }, - parse: (ctx: UsageParserContext< - CustomContext, - ReturnTypePromise, - TOutput, - TOpts - >) => Promise>; - optional?: boolean, - default?: TOutput | ((ctx: UsageParserContext< - CustomContext, - ReturnTypePromise, - TOutput, - TOpts - >) => TOutput), - rest?: boolean, -} - -export type UsageParserContext< - CustomContext, - TInput, - TOutput, - TOpts, -> = { - arg: TInput; - name: string; - opts: TOpts; - fail(message: string): Extract, { fail: true }>; - context: CustomContext; - style: {}; // TODO: ArgumentHandlerStylings -} - -export interface UsageParserSuccess { fail: false, parsed: TOutput } -export interface UsageParserFail { fail: true, message: string } - -export type UsageParserResult = UsageParserSuccess | UsageParserFail; - -export type UsageError< - TUsage extends UsageParser, -> = { - usage: TUsage, - message: UsageParserFail["message"], -}; - -// Middlewares - -export type ExecutorStage = "splitString" | "resolveCommand" | "parseUsages" | "checks" | "run"; -export type MiddlewareConstraint< - TMiddlewares extends CommandHandlerMiddleware[], -> = TMiddlewares[number]["id"] | ExecutorStage; - -export type CommandHandlerMiddleware< - TMiddlewares extends CommandHandlerMiddleware[], -> = { - id?: string, - run: (ctx: ExecutorContext, next: (() => void)) => void; - requires?: (TMiddlewares[number]["id"])[], -} & ({ - before?: MiddlewareConstraint, -} | { - after?: MiddlewareConstraint, -}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0a79d0c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +export * from "./core"; + + + + + diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..010240d --- /dev/null +++ b/src/test.ts @@ -0,0 +1,16 @@ +import { Inspect } from "./builtin/middlewares"; +import { createPipeline } from "./core"; + +let piper = createPipeline(x => x * 2) + .pipe(x => x + 1) + .pipe(x => x.toString()) + .pipe(x => `Output: ${x}`) + .pipe(Inspect()); + + +Promise.all([ + piper.execute(2), + piper.execute(3), +]).then(r => console.log(r)); + + diff --git a/src/utils/StringReader.ts b/src/utils/StringReader.ts new file mode 100644 index 0000000..d708990 --- /dev/null +++ b/src/utils/StringReader.ts @@ -0,0 +1,44 @@ +export class StringReader { + string: string = ""; + index: number = 0; + + constructor(str: string) { + this.string = str; + this.reset(); + } + + reset() { + this.index = 0; + } + + eof() { + return this.string.length < this.index; + } + + readOne() { + return this.string[this.index++]; + } + + read(count = 1) { + let buf = ""; + + while(count--) { + if(this.eof()) break; + buf += this.readOne(); + } + + return buf; + } + + skipOne() { + this.readOne(); + } + + peekOne() { + return this.string[this.index]; + } + + rest() { + return this.string.slice(this.index); + } +} diff --git a/src/utils/decorators.ts b/src/utils/decorators.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/forEachFile.ts b/src/utils/forEachFile.ts new file mode 100644 index 0000000..70890a0 --- /dev/null +++ b/src/utils/forEachFile.ts @@ -0,0 +1,14 @@ +import { Dirent } from "fs"; +import { readdir } from "fs/promises"; + +export const forEachFile = async (path: string, exts: string[], fn: (f: Dirent) => PromiseLike) => { + let files = await readdir(path, { withFileTypes: true }); + + for(let file of files) { + if(file.isDirectory()) { + await forEachFile(file.path, exts, fn); + } else if(exts.some(x => file.name.endsWith(x))) { + await fn(file); + } + } +} diff --git a/src/utils/recursiveImport.ts b/src/utils/recursiveImport.ts new file mode 100644 index 0000000..a310d70 --- /dev/null +++ b/src/utils/recursiveImport.ts @@ -0,0 +1,8 @@ +import { unloadModule } from "./unloadModule"; +import { forEachFile } from "./forEachFile"; + +export const recursiveImport = (path: string, exts: string[] = [".js"]) => forEachFile( + path, + exts, + async (file) => await import(file.path), +); diff --git a/src/utils/recursiveUnload.ts b/src/utils/recursiveUnload.ts new file mode 100644 index 0000000..26b4118 --- /dev/null +++ b/src/utils/recursiveUnload.ts @@ -0,0 +1,8 @@ +import { unloadModule } from "./unloadModule"; +import { forEachFile } from "./forEachFile"; + +export const recursiveUnload = (path: string, exts: string[] = [".js"]) => forEachFile( + path, + exts, + async (file) => unloadModule(file.path), +); diff --git a/src/utils/unloadModule.ts b/src/utils/unloadModule.ts new file mode 100644 index 0000000..a535294 --- /dev/null +++ b/src/utils/unloadModule.ts @@ -0,0 +1,7 @@ +export const unloadModule = (path: string) => { + let module = require.cache[path]; + if(module) { + for(let child of module.children) unloadModule(child.id); + } + delete require.cache[path]; +}; diff --git a/src/ArgumentParser.js b/srcold/ArgumentParser.js similarity index 73% rename from src/ArgumentParser.js rename to srcold/ArgumentParser.js index 3afbb8e..5064f70 100644 --- a/src/ArgumentParser.js +++ b/srcold/ArgumentParser.js @@ -1,4 +1,8 @@ +<<<<<<< Updated upstream:srcold/ArgumentParser.js import splitargs from "../utils/splitargs.js"; +======= +import splitargs from '../utils/splitargs.js'; +>>>>>>> Stashed changes:src/ArgumentParser.js // The Usage System @@ -13,7 +17,7 @@ import splitargs from "../utils/splitargs.js"; * @prop {ArgumentHandlerStylings} style */ -const fail = (m) => ({ fail: true, message: m }); +const fail = (m, code) => ({ fail: true, message: m, code }); /** * A collection of native/hardcoded argument parsers @@ -30,6 +34,7 @@ const NativeUsages = Object.entries({ )} cannot be longer than ${ctx.style.arg( opts.max, )} characters!`, + "TOO_LONG", ); } @@ -40,11 +45,17 @@ const NativeUsages = Object.entries({ )} cannot be shorter than ${ctx.style.arg( ctx.opts.min, )} characters!`, + "TOO_SHORT", ); } return { parsed: ctx.arg }; }, +<<<<<<< Updated upstream:srcold/ArgumentParser.js +======= + + _hideType: true, +>>>>>>> Stashed changes:src/ArgumentParser.js }, string: { type: "text" }, @@ -55,12 +66,13 @@ const NativeUsages = Object.entries({ let arg = Number(ctx.arg); if (isNaN(arg)) { - return fail(`${ctx.style.arg(ctx.name)} must be a number!`); + return fail(`${ctx.style.arg(ctx.name)} must be a number!`, "NAN"); } if (ctx.opts.isInt && arg % 1 !== 0) { return fail( `${ctx.style.arg(ctx.name)} must be a whole number!`, + "NOT_INT", ); } @@ -69,6 +81,7 @@ const NativeUsages = Object.entries({ `${ctx.style.arg( ctx.name, )} cannot be greater than ${ctx.style.arg(ctx.opts.max)}!`, + "TOO_BIG", ); } @@ -79,13 +92,40 @@ const NativeUsages = Object.entries({ )} cannot be smaller than ${ctx.style.arg( ctx.opts.min, )} characters!`, + "TOO_SMALL", ); } return { parsed: arg }; }, }, +<<<<<<< Updated upstream:srcold/ArgumentParser.js }); +======= + + bool: { + type: "native", + async parse(ctx) { + let arg = ctx.arg.toLowerCase(); + if(["0", "false", "f"].includes(arg)) { + return { parsed: false }; + } else if(["1", "true", "t"].includes(arg)) { + return { parsed: true }; + } else { + if(ctx.opts.acceptNull) { + if(ctx.opts.acceptNull == "strict") { + return fail(`${ctx.style.arg(ctx.name)} must be a boolean! (true, false or null)`); + } else { + return { parsed: null }; + }; + } else { + return fail(`${ctx.style.arg(ctx.name)} must be a boolean! (true or false)`); + }; + }; + }, + }, +}; +>>>>>>> Stashed changes:src/ArgumentParser.js /** * The stylings object for ArgumentHandler. @@ -105,7 +145,7 @@ class ArgumentParser { } /** - * Registers an usage + * Registers a usage parser * @param {string} id Usage Name * @param {UsageParser} usage The usage to register */ @@ -113,6 +153,15 @@ class ArgumentParser { this.ArgumentParsers.set(id, usage); } + /** + * Registers multiple usage parsers at once + * @param {Object} obj + */ + registerUsages(obj) { + for(let [k, v] of Object.entries(obj)) + this.registerUsage(k, v); + } + /** * Resolves an Usage Parser * @param {UsageResolvable} parser @@ -139,7 +188,7 @@ class ArgumentParser { parser = parser.slice(1).slice(0, -1); } - let sp = parser.split(":"); + let sp = parser.split(":").map(s => s.trim()); let type = sp.length === 2 ? sp[1] : sp[0]; let name = sp.length === 2 ? sp[0] : null; parser = this.ArgumentParsers.get(type); @@ -175,14 +224,19 @@ class ArgumentParser { let braceOpen = usage.optional ? "[" : "<"; let braceClose = usage.optional ? "]" : ">"; - let usageTypeName = usage.desc; + let usageTypeName = usage.type; + let typeStr = usage._hideType ? "" : (": " + usageTypeName); +<<<<<<< Updated upstream:srcold/ArgumentParser.js return ( braceOpen + usage.name + (usageTypeName ? ": " + usageTypeName : "") + braceClose ); +======= + return braceOpen + usage.name + typeStr + braceClose; +>>>>>>> Stashed changes:src/ArgumentParser.js } /** @@ -202,18 +256,28 @@ class ArgumentParser { // iterates over usages and parses them // adds to errors if it fails // adds to finalArgs if succeeds +<<<<<<< Updated upstream:srcold/ArgumentParser.js for (let i = 0; i < usages.length; i++) { let rawArg = rawArgs[i]; +======= + for(let i = 0; i < usages.length; i++) { + let rawArg = rawArgs[i] || ""; +>>>>>>> Stashed changes:src/ArgumentParser.js let currentUsage = usages[i]; if (currentUsage.rest) { rawArg = rawArgs.slice(i).join(" "); } - if (!rawArg.trim() && !currentUsage.optional) { + if (!(rawArg || "").trim() && !currentUsage.optional) { errors.push({ usage: currentUsage, - message: `${inlineCode(currentUsage.name)} is required!`, +<<<<<<< Updated upstream:srcold/ArgumentParser.js + message: `${(currentUsage.name)} is required!`, + code: "REQUIRED", +======= + message: `${this.styling.arg(currentUsage.name)} is required!`, +>>>>>>> Stashed changes:src/ArgumentParser.js }); continue; } @@ -222,7 +286,7 @@ class ArgumentParser { if (result.fail) { errors.push({ usage: currentUsage, - message: result.message, + ...result, }); } else { finalArgs.push(result.parsed); @@ -282,7 +346,10 @@ class ArgumentParser { parsed: defaultValue, }; } else { - return fail(`${this.styling.arg(usage.name)} is required!`); + return { + ...fail(`${this.styling.arg(usage.name)} is required!`), + code: "REQUIRED", + }; } } @@ -294,9 +361,10 @@ class ArgumentParser { name: usage.name, opts: usage, style: this.styling, - fail: (m) => ({ + fail: (m, extra = {}) => ({ fail: true, message: this.styling.arg(usage.name) + ": " + m, + ...extra, }), context, }); diff --git a/src/CommandHandler.js b/srcold/CommandHandler.js similarity index 96% rename from src/CommandHandler.js rename to srcold/CommandHandler.js index 3a713c4..b608554 100644 --- a/src/CommandHandler.js +++ b/srcold/CommandHandler.js @@ -8,7 +8,7 @@ import { stageify } from "./stageify.js"; * @typedef {Object} Command * @prop {string} name - Name of the command * @prop {string[]} [aliases] - aliases - * @prop {import("./usages").UsageResolvable[]} [args] - Arguments + * @prop {import("./usages.js").UsageResolvable[]} [args] - Arguments * @prop {CommandRun} run * @prop {CommandCheck[]} checks */ @@ -105,7 +105,7 @@ class CommandHandler extends EventEmitter { this.log.info("Registering folder: " + resolve(folderPath)); for (let entry of entries) { let fd = resolve(folderPath, entry.name); - if (entry.isDirectory()) registerCommands(fd); + if (entry.isDirectory()) await this.registerCommands(fd); else { let obj = {}; try { @@ -255,7 +255,7 @@ class CommandHandler extends EventEmitter { let failedChecks = []; for (let check of execCtx.command.checks) { /** @type {CommandCheckResult} */ - let result = await check(...execCtx.runArgs); + let result = await check(execCtx, execCtx.runArgs); if (!result.pass) { failedChecks.push(result); } @@ -264,7 +264,7 @@ class CommandHandler extends EventEmitter { if (failedChecks.length) { this.emit("failedChecks", { ...execCtx, - checks: failedChecks, + failedChecks, }); return; } @@ -297,6 +297,7 @@ class CommandHandler extends EventEmitter { execute({ input, ctx, + handler: this, }); return this; diff --git a/srcold/index.d.ts b/srcold/index.d.ts new file mode 100644 index 0000000..f4095e4 --- /dev/null +++ b/srcold/index.d.ts @@ -0,0 +1,175 @@ +/// + +import { EventEmitter } from "node:events"; + +export as namespace stringcommands; +export as namespace strcmd; + +type BaseRunnerArgs = any[]; +type BaseCustomContext = {}; + +type Command< + TRunnerArgs extends BaseRunnerArgs, +> = { + name: string, + run(...args: TRunnerArgs): Promise | void, + + aliases?: string[], + args?: UsageResolvableList, + checks: CommandCheck[], +}; + +type CommandCheck = (execCtx: TExecutorContext, ...args: TRunnerArgs) => Promise; + +interface CommandCheckPass { pass: true } +interface CommandCheckFail { pass: false, message: string } + +type CommandCheckResult = CommandCheckPass | CommandCheckFail; + +interface CommandHandlerOptions { + prefix: string, + log: typeof console | { log: AnyFunction, info: AnyFunction, error: AnyFunction, } | false, +} + +/** + * The string-commands command handler + */ +export class CommandHandler< + Opts extends CommandHandlerOptions, + + CustomContext extends BaseCustomContext, + RunnerArgs extends BaseRunnerArgs, + + _execContext extends ExecutorContext, +> extends EventEmitter { + constructor(opts?: Opts) + + prefix: Opts["prefix"] | string; + Commands: Map>; + Aliases: Map; + middlewares: CommandHandlerMiddleware[]; + + argumentParser: ArgumentParser; + + registerCommand(cmd: Command): this; + registerCommands(path: string): Promise; + + addCommand(cmd: Command): this; + addCommands(path: string): Promise; + + on(event: "invalidUsage", handler: (ctx: _execContext) => void): this; + on(event: "failedChecks", handler: (ctx: _execContext) => void): this; + + buildArguments(ctx: _execContext): RunnerArgs; + + use(mw: CommandHandlerMiddleware): this; + + run(input: string, ctx: CustomContext): this; + + prettyPrint(cmd: Command): string; +} + +/** + * The command executor context, used in various parts of the command handler. + */ +type ExecutorContext< + CustomContext, + TRunnerArgs extends BaseRunnerArgs, +> = { + /** The raw input string */ + input: string, + /** The custom context */ + ctx: CustomContext, + /** Name of the executing command */ + name?: Command["name"], + /** Unparsed arguments (raw) from the input string */ + rawArgs?: string[], + /** The command's information */ + command?: Command, + /** List of usage (argument parser) errors */ + errors?: UsageError[], + /** Parsed arguments/usages */ + args?: any[], // TODO: typescript voodoo to make it relationship >:3 + /** Built runner args */ + runArgs?: TRunnerArgs, + /** List of failed checks */ + failedChecks?: CommandCheckFail[], + /** An error, if occured while executing the command */ + error?: Error, +}; + +type BasicExecutorContext = ExecutorContext; + +// Usage System + +export class ArgumentParser { + ArgumentParsers: Map>; + + parseUsages(text: string, + commandUsages: T, + ctx: CustomContext): { + args: any[], + errors: UsageParserFail[], + }; +} + +type UsageResolvableList = UsageResolvable[]; + +type UsageResolvable = string | { type: UsageResolvable }; + +interface Usage extends UsageParser { + name: string, +} + +interface UsageParser< + TInput, + TOutput, +> { + // The second in this union is for Usage compat. + type: UsageResolvable, + parse: (ctx: UsageParserContext< + TInput + >) => Promise>; + optional?: boolean, + default?: TOutput | ((ctx: UsageParserContext< + TInput + >) => Promise), + rest?: boolean, +} + +type UsageParserContext< + TInput, +> = { + arg: TInput; + name: string; + opts: {}; + fail(message: string): UsageParserFail; + context: BaseCustomContext; + style: {}; // TODO: ArgumentHandlerStylings +}; + +interface UsageParserSuccess { fail: false, parsed: TOutput } +interface UsageParserFail { fail: true, message: string } + +type UsageParserResult = UsageParserSuccess | UsageParserFail; + +interface UsageError extends UsageParserFail { + usage: UsageParser, +} + +// Middlewares + +type ExecutorStage = "splitString" | "resolveCommand" | "parseUsages" | "checks" | "run"; +type MiddlewareConstraint< + TMiddlewares extends CommandHandlerMiddleware[], +> = TMiddlewares[number]["id"] | ExecutorStage; + +export type CommandHandlerMiddleware = { + id?: string, + run: (ctx: ExecutorContext, next: (() => void)) => void; + requires?: ExecutorStage[], +} & ({ + before?: ExecutorStage, +} | { + after?: ExecutorStage, +}); \ No newline at end of file diff --git a/src/index.js b/srcold/index.js similarity index 100% rename from src/index.js rename to srcold/index.js diff --git a/src/stageify.js b/srcold/stageify.js similarity index 100% rename from src/stageify.js rename to srcold/stageify.js diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9b41776 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "declaration": true, + "outDir": "./dist", + "moduleResolution": "Bundler" + }, + "include": [ + "src/**/*" + ] +}