diff --git a/package.json b/package.json index 996a9ea..430ef08 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "scripts": { "build": "node index.js --compile-only", "start": "node index.js", + "setup": "node index.js --cli", "startnc": "node index.js --no-compile", - "test": "node index.js --dev-config --dev-database", - "testnc": "node index.js --no-compile --dev-config --dev-database", + "test": "node index.js --dev-config --dev-database --soft-plugins", + "testsetup": "node index.js --cli --dev-config --dev-database --soft-plugins", + "testnc": "node index.js --no-compile --dev-config --dev-database --soft-plugins", "docs": "npx typedoc --options .docs/typedoc-config.json && node .docs/createDocs.js" }, "type": "commonjs", @@ -24,9 +26,11 @@ "dependencies": { "@discordjs/rest": "^2.4.2", "@types/node": "^22.5.0", + "@types/terminal-kit": "^2.5.7", "ansis": "^2.3.0", "discord.js": "^14.17.2", "formatted-json-stringify": "^1.1.0", + "terminal-kit": "^3.1.2", "typescript": "^5.5.4" }, "repository": { diff --git a/src/core/api/defaults/checker.ts b/src/core/api/defaults/checker.ts index db3bbe1..62a15fb 100644 --- a/src/core/api/defaults/checker.ts +++ b/src/core/api/defaults/checker.ts @@ -183,14 +183,8 @@ export class ODCheckerRenderer_Default extends ODCheckerRenderer { return finalComponents } /**Get the length of the longest string in the array. */ - #getLongestLength(text:string[]): number { - let finalLength = 0 - text.forEach((t) => { - const l = ansis.strip(t).length - if (l > finalLength) finalLength = l - }) - - return finalLength + #getLongestLength(texts:string[]): number { + return Math.max(...texts.map((t) => ansis.strip(t).length)) } /**Get a horizontal divider used between different parts of the config checker result. */ #getHorizontalDivider(width:number): string { diff --git a/src/core/api/defaults/flag.ts b/src/core/api/defaults/flag.ts index 218fae4..8830cc2 100644 --- a/src/core/api/defaults/flag.ts +++ b/src/core/api/defaults/flag.ts @@ -24,6 +24,7 @@ export interface ODFlagManagerIds_Default { "opendiscord:no-compile":ODFlag, "opendiscord:compile-only":ODFlag, "opendiscord:silent":ODFlag, + "opendiscord:cli":ODFlag, } /**## ODFlagManager_Default `default_class` diff --git a/src/core/api/defaults/stat.ts b/src/core/api/defaults/stat.ts index 35226c6..dd4c016 100644 --- a/src/core/api/defaults/stat.ts +++ b/src/core/api/defaults/stat.ts @@ -1,7 +1,6 @@ /////////////////////////////////////// //DEFAULT SESSION MODULE /////////////////////////////////////// -import { Guild, TextBasedChannel, User } from "discord.js" import { ODValidId } from "../modules/base" import { ODStatScope, ODStatGlobalScope, ODStatsManager, ODStat, ODBasicStat, ODDynamicStat, ODValidStatValue, ODStatScopeSetMode } from "../modules/stat" diff --git a/src/core/api/modules/checker.ts b/src/core/api/modules/checker.ts index 14b397b..cccc4e2 100644 --- a/src/core/api/modules/checker.ts +++ b/src/core/api/modules/checker.ts @@ -289,6 +289,16 @@ export class ODCheckerFunctionManager extends ODManager { */ export type ODCheckerLocationTrace = (string|number)[] +/**## ODCheckerOptions `interface` + * This interface contains all optional properties to customise in the `ODChecker` class. + */ +export interface ODCheckerOptions { + /**The name of this config in the Interactive Setup CLI. */ + cliDisplayName?:string + /**The description of this config in the Interactive Setup CLI. */ + cliDisplayDescription?:string +} + /**## ODChecker `class` * This is an Open Ticket config checker. * @@ -310,13 +320,16 @@ export class ODChecker extends ODManagerData { messages: ODCheckerMessage[] = [] /**Temporary storage for the quit status from the check() method (not recommended to use) */ quit: boolean = false + /**All additional properties of this config checker. */ + options: ODCheckerOptions - constructor(id:ODValidId, storage: ODCheckerStorage, priority:number, config:ODConfig, structure: ODCheckerStructure){ + constructor(id:ODValidId, storage: ODCheckerStorage, priority:number, config:ODConfig, structure:ODCheckerStructure, options?:ODCheckerOptions){ super(id) this.storage = storage this.priority = priority this.config = config this.structure = structure + this.options = options ?? {} } /**Run this checker. Returns all errors*/ @@ -391,7 +404,13 @@ export interface ODCheckerStructureOptions { /**Add a custom checker function. Returns `true` when valid. */ custom?:(checker:ODChecker, value:ODValidJsonType, locationTrace:ODCheckerLocationTrace, locationId:ODId, locationDocs:string|null) => boolean, /**Set the url to the documentation of this variable. */ - docs?:string + docs?:string, + /**The name of this config in the Interactive Setup CLI. */ + cliDisplayName?:string + /**The description of this config in the Interactive Setup CLI. */ + cliDisplayDescription?:string + /**The default value of this variable when creating it in the Interactive Setup CLI. When not specified, the user will be asked to insert a value. */ + cliInitDefaultValue?:string } /**## ODCheckerStructure `class` @@ -426,7 +445,13 @@ export class ODCheckerStructure { */ export interface ODCheckerObjectStructureOptions extends ODCheckerStructureOptions { /**Add a checker for a property in an object (can also be optional) */ - children?:{key:string, checker:ODCheckerStructure, priority:number, optional:boolean}[] + children?:{key:string, priority:number, optional:boolean, cliHideInEditMode?:boolean, checker:ODCheckerStructure}[], + /**A list of keys to skip when creating this object with the Interactive Setup CLI. The default value of these properties will be used instead. */ + cliInitSkipKeys?:string[], + /**The key of a (primitive) property in this object to show the value of in the Interactive Setup CLI when listed in an array. */ + cliDisplayKeyInParentArray?:string, + /**A list of additional (primitive) property keys in this object to show the value of in the Interactive Setup CLI when listed in an array. */ + cliDisplayAdditionalKeysInParentArray?:string[] } /**## ODCheckerObjectStructure `class` @@ -500,7 +525,9 @@ export interface ODCheckerStringStructureOptions extends ODCheckerStructureOptio /**You need to choose between ... */ choices?:string[], /**The string needs to match this regex */ - regex?:RegExp + regex?:RegExp, + /**Provide an optional list for autocomplete when using the Interactive Setup CLI. Defaults to the `choices` option. */ + cliAutocompleteList?:string[] } /**## ODCheckerStringStructure `class` @@ -718,7 +745,9 @@ export interface ODCheckerArrayStructureOptions extends ODCheckerStructureOption /**Allow double values (only for `string`, `number` & `boolean`) */ allowDoubles?:boolean /**Only allow these types in the array (for multi-type propertyCheckers) */ - allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[] + allowedTypes?:("string"|"number"|"boolean"|"null"|"array"|"object"|"other")[], + /**The name of the properties inside this array. Used in the GUI of the Interactive Setup CLI. */ + cliDisplayPropertyName?:string } /**## ODCheckerArrayStructure `class` @@ -863,9 +892,9 @@ export interface ODCheckerTypeSwitchStructureOptions extends ODCheckerStructureO /**A checker when the property is a boolean */ boolean?:ODCheckerBooleanStructure, /**A checker when the property is null */ - null?:ODCheckerStructure, + null?:ODCheckerNullStructure, /**A checker when the property is an array */ - array?:ODCheckerStructure, + array?:ODCheckerArrayStructure, /**A checker when the property is an object */ object?:ODCheckerObjectStructure, /**A checker when the property is something else */ @@ -1390,32 +1419,26 @@ export class ODCheckerCustomStructure_UniqueIdArray extends ODCheckerArrayStruct constructor(id:ODValidId, source:string, scope:string, usedScope?:string, options?:ODCheckerArrayStructureOptions){ //add premade custom structure checker const newOptions = options ?? {} - newOptions.custom = (checker,value,locationTrace,locationId,locationDocs) => { - const lt = checker.locationTraceDeref(locationTrace) + newOptions.propertyChecker = new ODCheckerStringStructure("opendiscord:unique-id",{minLength:1,custom:(checker,value,locationTrace,locationId,locationDocs) => { + if (typeof value != "string") return false + const localLt = checker.locationTraceDeref(locationTrace) + localLt.pop() - if (!Array.isArray(value)) return false - const uniqueArray: string[] = (checker.storage.get(source,scope) === null) ? [] : checker.storage.get(source,scope) - - let localQuit = false - value.forEach((id,index) => { - if (typeof id != "string") return - const localLt = checker.locationTraceDeref(lt) - localLt.push(index) - if (uniqueArray.includes(id)){ - //exists - if (usedScope){ - const current: string[] = checker.storage.get(source,usedScope) ?? [] - current.push(id) - checker.storage.set(source,usedScope,current) - } - }else{ - //doesn't exist - checker.createMessage("opendiscord:id-non-existent","error",`The id "${id}" doesn't exist!`,localLt,null,[`"${id}"`],this.id,(this.options.docs ?? null)) - localQuit = true + const uniqueArray: string[] = checker.storage.get(source,scope) ?? [] + if (uniqueArray.includes(value)){ + //exists + if (usedScope){ + const current: string[] = checker.storage.get(source,usedScope) ?? [] + current.push(value) + checker.storage.set(source,usedScope,current) } - }) - return !localQuit - } + return true + }else{ + //doesn't exist + checker.createMessage("opendiscord:id-non-existent","error",`The id "${value}" doesn't exist!`,localLt,null,[`"${value}"`],locationId,locationDocs) + return false + } + }}) super(id,newOptions) this.source = source this.scope = scope diff --git a/src/core/startup/cli.ts b/src/core/startup/cli.ts new file mode 100644 index 0000000..acbd8f4 --- /dev/null +++ b/src/core/startup/cli.ts @@ -0,0 +1,524 @@ +import {opendiscord, api, utilities} from "../../index" +import {Terminal, terminal} from "terminal-kit" +import ansis from "ansis" + +const logo = [ + " ██████╗ ██████╗ ███████╗███╗ ██╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ", + " ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ", + " ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ", + " ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ", + " ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ", + " ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ " +] + +/**A utility function to center text to a certain width. */ +function centerText(text:string,width:number){ + if (width < text.length) return text + let newWidth = width-ansis.strip(text).length+1 + let final = " ".repeat(newWidth/2)+text + return final +} + +/**A utility function to terminate the interactive CLI. */ +async function terminate(){ + terminal.grabInput(false) + terminal.clear() + terminal.green("👋 Exited the Open Ticket Interactive Setup CLI.\n") + process.exit(0) +} +terminal.on("key",(name,matches,data) => { + if (name == "CTRL_C") terminate() +}) + +/**Render the header of the interactive CLI. */ +function renderHeader(path:(string|number)[]){ + terminal.grabInput(true) + terminal.clear().moveTo(1,1) + terminal(ansis.hex("#f8ba00")(logo.join("\n")+"\n")) + terminal.bold(centerText("Interactive Setup CLI - Version: "+opendiscord.versions.get("opendiscord:version").toString()+" - Support: https://discord.dj-dj.be\n",88)) + if (path.length < 1) terminal.cyan(centerText("👋 Hi! Welcome to the Open Ticket Interactive Setup CLI! 👋\n\n",88)) + else terminal.cyan(centerText("🌐 Current Location: "+path.map((v,i) => { + if (i == 0) return v.toString() + else if (typeof v == "string") return ".\""+v+"\"" + else if (typeof v == "number") return "."+v + }).join("")+"\n\n",88)) +} + +async function renderConfigSelector(backFn:(() => api.ODPromiseVoid)){ + renderHeader([]) + terminal(ansis.bold.green("Please select which config you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const checkerList = opendiscord.checkers.getAll() + const checkerNameList = checkerList.map((checker) => (checker.options.cliDisplayName ? checker.options.cliDisplayName+" ("+checker.config.file+")" : checker.config.file)) + const checkerNameLength = utilities.getLongestLength(checkerNameList) + const finalCheckerNameList = checkerNameList.map((name,index) => name.padEnd(checkerNameLength+5," ")+ansis.gray(checkerList[index].options.cliDisplayDescription ? "=> "+checkerList[index].options.cliDisplayDescription : "")) + + const answer = await terminal.singleColumnMenu(finalCheckerNameList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + const checker = checkerList[answer.selectedIndex] + const configData = checker.config.data as api.ODValidJsonType + await chooseConfigStructure(checker,async () => {await renderConfigSelector(backFn)},checker.structure,configData,{},NaN,["("+checker.config.path+")"]) +} + +async function chooseConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object|any[],parentIndex:string|number,path:(string|number)[]){ + if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigEnabledObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data) await renderConfigObjectSwitchStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") await renderConfigBooleanStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerNullStructure && data === null) await renderConfigNullStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else if (structure instanceof api.ODCheckerTypeSwitchStructure) await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + else terminal.red.bold("❌ Unable to detect type of variable! Please try to edit this property in the JSON file itself!") +} + +async function renderConfigObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("Please select which variable you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + if (!structure.options.children) return await backFn() + + const list = structure.options.children.filter((child) => !child.cliHideInEditMode) + const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName+" ("+child.key+")" : child.key)) + const nameLength = utilities.getLongestLength(nameList) + const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray(list[index].checker.options.cliDisplayDescription ? "=> "+list[index].checker.options.cliDisplayDescription : "")) + + + const answer = await terminal.singleColumnMenu(finalnameList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + const subStructure = list[answer.selectedIndex] + const subData = data[subStructure.key] + await chooseConfigStructure(checker,async () => {await renderConfigObjectStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},subStructure.checker,subData,data,subStructure.key,[...path,subStructure.key]) +} + +async function renderConfigEnabledObjectStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + const enabledProperty = structure.options.property + const subStructure = structure.options.checker + if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn() + + if (!subStructure.options.children.find((child) => child.key === structure.options.property)){ + if (typeof structure.options.enabledValue == "string") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "number") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + else if (typeof structure.options.enabledValue == "boolean") subStructure.options.children.unshift({key:enabledProperty,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{})}) + } + + await chooseConfigStructure(checker,backFn,subStructure,data,parent,parentIndex,path) +} + +async function renderConfigObjectSwitchStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,data:object,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "object" || Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'object'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + if (!structure.options.objects) return await backFn() + + let didMatch: boolean = false + for (const objectTemplate of structure.options.objects){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + didMatch = true + await chooseConfigStructure(checker,backFn,subStructure,data,parent,parentIndex,path) + } + } + if (!didMatch) throw new Error("OT CLI => Unable to detect type of object in the object switch. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") +} + +async function renderConfigArrayStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerArrayStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + if (!Array.isArray(data)) throw new Error("OT CLI => Property is not of the type 'array'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("Please select what you would like to do.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + if (!structure.options.propertyChecker) return await backFn() + + const propertyName = structure.options.cliDisplayPropertyName ?? "index" + const answer = await terminal.singleColumnMenu([ + "Add "+propertyName, + "Edit "+propertyName, + "Move "+propertyName, + "Remove "+propertyName, + "Duplicate "+propertyName + ],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + if (answer.selectedIndex == 0){ + //TODO => add => trigger chooseConfigStructure() function but for "addition instead of editing" + }else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)},structure,structure.options.propertyChecker,data,parent,parentIndex,path) + else if (answer.selectedIndex == 4){ + //TODO => duplicate trigger chooseConfigStructure() function but for "addition instead of editing" + } + +} + +async function renderConfigArrayStructureEditSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to edit.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + await chooseConfigStructure(checker,async () => {await renderConfigArrayStructureEditSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path)},structure,subData,data,dataAnswer.selectedIndex,[...path,dataAnswer.selectedIndex]) +} + +async function renderconfigArrayStructureMoveSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to move.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + + renderHeader([...path,dataAnswer.selectedIndex]) + terminal(ansis.bold.green("Please select the position you would like to move to.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const moveAnswer = await terminal.singleColumnMenu([...data.map((d,i) => "Position "+(i+1)),"Last Position"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (moveAnswer.canceled) return await renderconfigArrayStructureMoveSelector(checker,backFn,arrayStructure,structure,data,parent,parentIndex,path) + console.log("move from:",dataAnswer.selectedIndex,"to:",moveAnswer.selectedIndex) +} + +async function renderconfigArrayStructureRemoveSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),arrayStructure:api.ODCheckerArrayStructure,structure:api.ODCheckerStructure,data:any[],parent:object,parentIndex:string|number,path:(string|number)[]){ + const propertyName = arrayStructure.options.cliDisplayPropertyName ?? "index" + renderHeader(path) + terminal(ansis.bold.green("Please select the "+propertyName+" you would like to delete.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + const longestDataListName = Math.max(...data.map((d,i) => getArrayPreviewStructureNameLength(structure,d,data,i))) + const dataList = data.map((d,i) => (i+1)+". "+getArrayPreviewFromStructure(structure,d,data,i,longestDataListName)) + const dataAnswer = await terminal.singleColumnMenu(dataList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (dataAnswer.canceled) return await backFn() + const subData = data[dataAnswer.selectedIndex] + console.log("delete position:",dataAnswer.selectedIndex) +} + +async function renderConfigBooleanStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,data:boolean,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "boolean") throw new Error("OT CLI => Property is not of the type 'boolean'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold[data ? "green" : "red"](data.toString())+"\n") + + const answer = await terminal.singleColumnMenu(["false (Disabled)","true (Enabled)"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = (answer.selectedIndex == 0) ? false : true + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.gray("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigBooleanStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigNumberStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,data:number,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "number") throw new Error("OT CLI => Property is not of the type 'number'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + + const answer = await terminal.inputField({ + style:terminal.cyan, + cancelable:true + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = Number(answer.replaceAll(",",".")) + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigNumberStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,data:string,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (typeof data != "string") throw new Error("OT CLI => Property is not of the type 'string'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n")) + + terminal.gray("\nCurrent value:"+(data.includes("\n") ? "\n" : " ")+ansis.bold.blue(data)+"\n") + + const autocompleteList = structure.options.cliAutocompleteList ?? structure.options.choices + const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = { + style:terminal.white, + selectedStyle:terminal.bgBlue.white + } + + const answer = await terminal.inputField({ + style:terminal.cyan, + hintStyle:terminal.gray, + cancelable:true, + autoComplete:autocompleteList, + autoCompleteHint:(!!autocompleteList), + autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false + }).promise + + if (typeof answer != "string") return await backFn() + + //run config checker + const newValue = answer.replaceAll("\\n","\n") + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigStringStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigNullStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,data:null,parent:object,parentIndex:string|number,path:(string|number)[]){ + if (data !== null) throw new Error("OT CLI => Property is not of the type 'null'. Please check your config for possible errors. (index: "+parentIndex+", path: "+path.join(".")+")") + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value:"+ansis.bold.blue("null")+"\n") + + const answer = await terminal.singleColumnMenu(["null"],{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run config checker + const newValue = null + const newPath = [...path] + newPath.shift() + checker.messages = [] //manually clear previous messages + const isDataValid = structure.check(checker,newValue,newPath) + + if (isDataValid){ + parent[parentIndex] = newValue + terminal.bold.blue("\n\n✅ Variable saved succesfully!") + await utilities.timer(400) + await backFn() + }else{ + const messages = checker.messages.map((msg) => "=> ["+msg.type.toUpperCase()+"] "+msg.message).join("\n") + terminal.bold.blue("\n\n❌ Variable is invalid! Please try again!") + terminal.red("\n"+messages) + await utilities.timer(1000+(2000*checker.messages.length)) + await renderConfigNullStructureEditor(checker,backFn,structure,data,parent,parentIndex,path) + } +} + +async function renderConfigTypeSwitchStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,data:any,parent:object,parentIndex:string|number,path:(string|number)[]){ + renderHeader(path) + terminal(ansis.bold.green("You are now editing "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("nr."+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n")) + + terminal.gray("\nCurrent value: "+ansis.bold.blue(data.toString())+"\n") + + const actionsList: string[] = [] + if (structure.options.boolean) actionsList.push("Edit as boolean") + if (structure.options.string) actionsList.push("Edit as string") + if (structure.options.number) actionsList.push("Edit as number") + if (structure.options.object) actionsList.push("Edit as object") + if (structure.options.array) actionsList.push("Edit as array/list") + if (structure.options.null) actionsList.push("Edit as null") + + const answer = await terminal.singleColumnMenu(actionsList,{ + leftPadding:"> ", + style:terminal.cyan, + selectedStyle:terminal.bgDefaultColor.bold, + submittedStyle:terminal.bgBlue, + extraLines:2, + cancelable:true + }).promise + + if (answer.canceled) return await backFn() + + //run selected structure editor (untested) + if (answer.selectedText.startsWith("Edit as boolean") && structure.options.boolean) await renderConfigBooleanStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.boolean,false,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as string") && structure.options.string) await renderConfigStringStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.string,data.toString(),parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as number") && structure.options.number) await renderConfigNumberStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.number,0,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as object") && structure.options.object) await renderConfigObjectStructureSelector(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.object,data,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as array/list") && structure.options.array) await renderConfigArrayStructureSelector(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.array,data,parent,parentIndex,path) + else if (answer.selectedText.startsWith("Edit as null") && structure.options.null) await renderConfigNullStructureEditor(checker,async () => {await renderConfigTypeSwitchStructureEditor(checker,backFn,structure,data,parent,parentIndex,path)},structure.options.null,null,parent,parentIndex,path) +} + +function getArrayPreviewStructureNameLength(structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object,parentIndex:string|number): number { + if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") return data.toString().length + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") return data.toString().length + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") return data.length + else if (structure instanceof api.ODCheckerNullStructure && data === null) return "Null".length + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) return "Array".length + else if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + if (!structure.options.cliDisplayKeyInParentArray) return "Object".length + else return data[structure.options.cliDisplayKeyInParentArray].toString().length + + }else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const subStructure = structure.options.checker + if (!subStructure) return "".length + return getArrayPreviewStructureNameLength(subStructure,data,parent,parentIndex) + + }else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data){ + for (const objectTemplate of (structure.options.objects ?? [])){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + return getArrayPreviewStructureNameLength(subStructure,data,parent,parentIndex) + } + } + return "".length + + }else if (structure instanceof api.ODCheckerTypeSwitchStructure){ + if (typeof data == "boolean" && structure.options.boolean) return getArrayPreviewStructureNameLength(structure.options.boolean,data,parent,parentIndex) + else if (typeof data == "number" && structure.options.number) return getArrayPreviewStructureNameLength(structure.options.number,data,parent,parentIndex) + else if (typeof data == "string" && structure.options.string) return getArrayPreviewStructureNameLength(structure.options.string,data,parent,parentIndex) + else if (typeof data == "object" && !Array.isArray(data) && data && structure.options.object) return getArrayPreviewStructureNameLength(structure.options.object,data,parent,parentIndex) + else if (Array.isArray(data) && structure.options.array) return getArrayPreviewStructureNameLength(structure.options.array,data,parent,parentIndex) + else if (data === null && structure.options.null) return getArrayPreviewStructureNameLength(structure.options.null,data,parent,parentIndex) + else return "".length + }else return "".length +} + +function getArrayPreviewFromStructure(structure:api.ODCheckerStructure,data:api.ODValidJsonType,parent:object,parentIndex:string|number,nameLength:number): string { + if (structure instanceof api.ODCheckerBooleanStructure && typeof data == "boolean") return data.toString() + else if (structure instanceof api.ODCheckerNumberStructure && typeof data == "number") return data.toString() + else if (structure instanceof api.ODCheckerStringStructure && typeof data == "string") return data + else if (structure instanceof api.ODCheckerNullStructure && data === null) return "Null" + else if (structure instanceof api.ODCheckerArrayStructure && Array.isArray(data)) return "Array" + else if (structure instanceof api.ODCheckerObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const additionalKeys = (structure.options.cliDisplayAdditionalKeysInParentArray ?? []).map((key) => key+": "+data[key].toString()).join(", ") + if (!structure.options.cliDisplayKeyInParentArray) return "Object" + else return data[structure.options.cliDisplayKeyInParentArray].toString().padEnd(nameLength+5," ")+ansis.gray(additionalKeys.length > 0 ? "("+additionalKeys+")" : "") + + }else if (structure instanceof api.ODCheckerEnabledObjectStructure && typeof data == "object" && !Array.isArray(data) && data){ + const subStructure = structure.options.checker + if (!subStructure) return "" + return getArrayPreviewFromStructure(subStructure,data,parent,parentIndex,nameLength) + + }else if (structure instanceof api.ODCheckerObjectSwitchStructure && typeof data == "object" && !Array.isArray(data) && data){ + for (const objectTemplate of (structure.options.objects ?? [])){ + if (objectTemplate.properties.every((prop) => data[prop.key] === prop.value)){ + //object template matches data + const subStructure = objectTemplate.checker + return getArrayPreviewFromStructure(subStructure,data,parent,parentIndex,nameLength) + } + } + return "" + + }else if (structure instanceof api.ODCheckerTypeSwitchStructure){ + if (typeof data == "boolean" && structure.options.boolean) return getArrayPreviewFromStructure(structure.options.boolean,data,parent,parentIndex,nameLength) + else if (typeof data == "number" && structure.options.number) return getArrayPreviewFromStructure(structure.options.number,data,parent,parentIndex,nameLength) + else if (typeof data == "string" && structure.options.string) return getArrayPreviewFromStructure(structure.options.string,data,parent,parentIndex,nameLength) + else if (typeof data == "object" && !Array.isArray(data) && data && structure.options.object) return getArrayPreviewFromStructure(structure.options.object,data,parent,parentIndex,nameLength) + else if (Array.isArray(data) && structure.options.array) return getArrayPreviewFromStructure(structure.options.array,data,parent,parentIndex,nameLength) + else if (data === null && structure.options.null) return getArrayPreviewFromStructure(structure.options.null,data,parent,parentIndex,nameLength) + else return "" + }else return "" +} + +export async function execute(){ + if (terminal.width < 100 || terminal.height < 35){ + terminal(ansis.red.bold("\n\nMake sure your console or cmd window has a "+ansis.cyan("minimum width & height")+" of "+ansis.cyan("100x35")+" characters.")) + terminal(ansis.red.bold("\nOtherwise the Open Ticket Interactive Setup CLI will be rendered incorrectly.")) + terminal(ansis.red.bold("\nThe current terminal dimensions are: "+ansis.cyan(terminal.width+"x"+terminal.height)+".")) + }else await renderConfigSelector(terminate) +} \ No newline at end of file diff --git a/src/core/startup/init.ts b/src/core/startup/init.ts index 0ae357e..66279de 100644 --- a/src/core/startup/init.ts +++ b/src/core/startup/init.ts @@ -31,12 +31,13 @@ moduleInstalled("discord.js",true) moduleInstalled("ansis",true) moduleInstalled("formatted-json-stringify",true) moduleInstalled("typescript",true) +moduleInstalled("terminal-kit",true) tempError() //init API import * as api from "../api/api" //import for local use export * as api from "../api/api" //export to other parts of bot - +import ansis from "ansis" //import ansis for usage in initialization export const opendiscord = new api.ODMain() console.log("\n--------------------------- OPEN TICKET STARTUP ---------------------------") @@ -103,6 +104,10 @@ export interface ODUtilities { * Same as `string.replace(search, value)` but with async compatibility */ asyncReplace(text:string, regex:RegExp, func:(value:string,...args:any[]) => Promise): Promise + /**## getLongestLength `utility function` + * Get the length of the longest string in the array. + */ + getLongestLength(text:string[]): number /**## easterEggs `utility object` * Object containing data for Open Ticket easter eggs. */ @@ -208,6 +213,9 @@ export const utilities: ODUtilities = { }) return result }, + getLongestLength(texts:string[]): number { + return Math.max(...texts.map((t) => ansis.strip(t).length)) + }, easterEggs:{ /* THANK YOU TO ALL OUR CONTRIBUTORS!!! */ creator:"779742674932072469", //DJj123dj diff --git a/src/data/framework/checkerLoader.ts b/src/data/framework/checkerLoader.ts index 8955ae9..44d94ba 100644 --- a/src/data/framework/checkerLoader.ts +++ b/src/data/framework/checkerLoader.ts @@ -3,11 +3,11 @@ import {opendiscord, api, utilities} from "../../index" const generalConfig = opendiscord.configs.get("opendiscord:general") export const loadAllConfigCheckers = async () => { - opendiscord.checkers.add(new api.ODChecker("opendiscord:general",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:general"),defaultGeneralStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:options",opendiscord.checkers.storage,1,opendiscord.configs.get("opendiscord:options"),defaultOptionsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:panels",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:panels"),defaultPanelsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:questions",opendiscord.checkers.storage,2,opendiscord.configs.get("opendiscord:questions"),defaultQuestionsStructure)) - opendiscord.checkers.add(new api.ODChecker("opendiscord:transcripts",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:transcripts"),defaultTranscriptsStructure)) + opendiscord.checkers.add(new api.ODChecker("opendiscord:general",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:general"),defaultGeneralStructure,{cliDisplayName:"General Config",cliDisplayDescription:"Configure the bot token, status, colors, permissions & more."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:questions",opendiscord.checkers.storage,2,opendiscord.configs.get("opendiscord:questions"),defaultQuestionsStructure,{cliDisplayName:"Questions Config",cliDisplayDescription:"Create, modify & delete questions which are used in options."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:options",opendiscord.checkers.storage,1,opendiscord.configs.get("opendiscord:options"),defaultOptionsStructure,{cliDisplayName:"Options Config",cliDisplayDescription:"Create, modify & delete options which are used in panels."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:panels",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:panels"),defaultPanelsStructure,{cliDisplayName:"Panels Config",cliDisplayDescription:"Create, modify & delete panels which can be spawned in discord."})) + opendiscord.checkers.add(new api.ODChecker("opendiscord:transcripts",opendiscord.checkers.storage,0,opendiscord.configs.get("opendiscord:transcripts"),defaultTranscriptsStructure,{cliDisplayName:"Transcript Config",cliDisplayDescription:"Configure everything related to transcripts."})) } export const loadAllConfigCheckerFunctions = async () => { @@ -116,7 +116,7 @@ export const registerDefaultCheckerCustomTranslations = (tm:api.ODCheckerTransla tm.quickTranslate(lm,"checker.messages.dropdownOption","message","opendiscord:dropdown-option") // A panel with dropdown enabled can only contain options of the 'ticket' type! //TODO TRANSLATION!!! - //tm.quickTranslate(lm,"checker.messages.TODO","message","opendiscord:invalid-version") // The version specified in your config is invalid! Make sure you have updated it to the latest version! + //tm.quickTranslate(lm,"checker.messages.TODO","message","opendiscord:invalid-version") // The version specified in your config does not match! Make sure you have updated the config to the latest version! } //UTILITY FUNCTIONS @@ -134,7 +134,7 @@ const createTicketEmbedStructure = (id:api.ODValidId) => { {key:"image",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-image",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:ticket-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-embed-fields",{children:[ {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-name",{minLength:1,maxLength:256})}, {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-embed-field-value",{minLength:1,maxLength:1024})}, {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-embed-field-inline",{})} @@ -146,7 +146,7 @@ const createTicketPingStructure = (id:api.ODValidId) => { return new api.ODCheckerObjectStructure(id,{children:[ {key:"@here",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-here",{})}, {key:"@everyone",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-ping-everyone",{})}, - {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false})}, + {key:"custom",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ping-custom","role",[],{allowDoubles:false,cliDisplayPropertyName:"custom role id"})}, ]}) } const createPanelEmbedStructure = (id:api.ODValidId) => { @@ -160,7 +160,7 @@ const createPanelEmbedStructure = (id:api.ODValidId) => { {key:"thumbnail",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UrlString("opendiscord:panel-embed-thumbnail",true,{allowHttp:false,allowedExtensions:[".png",".jpg",".jpeg",".webp",".gif"]})}, {key:"footer",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-footer",{maxLength:2048})}, - {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ + {key:"fields",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:panel-embed-fields",{allowedTypes:["object"],cliDisplayPropertyName:"embed field",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panel-embed-fields",{children:[ {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-name",{minLength:1,maxLength:256})}, {key:"value",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-embed-field-value",{minLength:1,maxLength:1024})}, {key:"inline",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-embed-field-inline",{})} @@ -178,7 +178,7 @@ function loadFromEnv(){ //STRUCTURES export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendiscord:general",{children:[ //STATUS - {key:"_INFO",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ + {key:"_INFO",optional:false,priority:0,cliHideInEditMode:true,checker:new api.ODCheckerObjectStructure("opendiscord:info",{children:[ {key:"support",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-support",{choices:["https://otdocs.dj-dj.be"]})}, {key:"discord",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-discord",{choices:["https://discord.dj-dj.be"]})}, {key:"version",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:info-version",{custom(checker,value,locationTrace,locationId,locationDocs) { @@ -186,14 +186,14 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis if (typeof value != "string") return false else if (value != "open-ticket-"+opendiscord.versions.get("opendiscord:version").toString()){ - checker.createMessage("opendiscord:invalid-version","warning","The version specified in your config is invalid! Make sure you have updated it to the latest version!",lt,null,[],locationId,locationDocs) + checker.createMessage("opendiscord:invalid-version","warning","The version specified in your config does not match! Make sure you have updated the config to the latest version!",lt,null,[],locationId,locationDocs) return false }else return true },})}, ]})}, //BASIC - {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token")}, + {key:"token",optional:false,priority:0,checker:(loadFromEnv()) ? new api.ODCheckerStringStructure("opendiscord:token-disabled",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."}) : new api.ODCheckerCustomStructure_DiscordToken("opendiscord:token",{cliDisplayName:"Token",cliDisplayDescription:"The token of your discord bot."})}, {key:"tokenFromENV",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:token-env",{})}, {key:"mainColor",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_HexColor("opendiscord:main-color",true,false)}, {key:"language",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:language",{ @@ -206,10 +206,11 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis return false }else return true }, + cliAutocompleteList:opendiscord.defaults.getDefault("languageList") })}, {key:"prefix",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:prefix",{minLength:1})}, {key:"serverId",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:server-id","server",false,[])}, - {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false})}, + {key:"globalAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:global-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"global admin role"})}, {key:"slashCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:slash-commands",{})}, {key:"textCommands",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:text-commands",{})}, @@ -244,13 +245,9 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis {key:"enableTicketActionWithReason",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-ticket-action-with-reason",{})}, {key:"enableDeleteWithoutTranscript",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:enable-delete-without-transcript",{})}, - {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{ - property:"enabled", - enabledValue:true, - checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ - {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[])}, - ]}) - })}, + {key:"logs",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:system-logs",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:system-logs",{children:[ + {key:"channel",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:log-channel","channel",false,[])}, + ]})})}, {key:"limits",optional:false,priority:0,checker:new api.ODCheckerEnabledObjectStructure("opendiscord:limits",{property:"enabled",enabledValue:true,checker:new api.ODCheckerObjectStructure("opendiscord:limits",{children:[ {key:"globalMaximum",optional:false,priority:0,checker:new api.ODCheckerNumberStructure("opendiscord:limits-global",{zeroAllowed:false,negativeAllowed:false,floatAllowed:false,min:1})}, @@ -297,9 +294,9 @@ export const defaultGeneralStructure = new api.ODCheckerObjectStructure("opendis ]})} ]}) -export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ +export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendiscord:options",{allowedTypes:["object"],cliDisplayPropertyName:"option",propertyChecker:new api.ODCheckerObjectSwitchStructure("opendiscord:options",{objects:[ //TICKET - {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{children:[ + {name:"ticket",priority:0,properties:[{key:"type",value:"ticket"}],checker:new api.ODCheckerObjectStructure("opendiscord:ticket",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:ticket-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:ticket-description",{maxLength:256})}, @@ -321,10 +318,10 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc }})}, //TICKET ADMINS - {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false})}, - {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false})}, + {key:"ticketAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-ticket-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"ticket admin role"})}, + {key:"readonlyAdmins",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:ticket-readonly-admins","role",[],{allowDoubles:false,cliDisplayPropertyName:"read-only ticket admin role"})}, {key:"allowCreationByBlacklistedUsers",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:ticket-allow-blacklisted-users",{})}, - {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5})}, + {key:"questions",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:option-questions","openticket","question-ids","question-ids-used",{allowDoubles:false,maxLength:5,cliDisplayPropertyName:"question"})}, //TICKET CHANNEL {key:"channel",optional:false,priority:0,checker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel",{children:[ @@ -334,7 +331,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-category","category",true,[])}, {key:"closedCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-closed-category","category",true,[])}, {key:"backupCategory",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-backup-category","category",true,[])}, - {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ + {key:"claimedCategory",optional:false,priority:0,checker:new api.ODCheckerArrayStructure("opendiscord:ticket-channel-claimed-category",{allowDoubles:false,allowedTypes:["object"],cliDisplayPropertyName:"claim category",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:ticket-channel-claimed-category",{children:[ {key:"user",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-user","user",false,[])}, {key:"category",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordId("opendiscord:ticket-channel-claimed-category","category",false,[])} ]})})}, @@ -383,7 +380,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc ]})}, //WEBSITE - {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-website",{children:[ + {name:"website",priority:0,properties:[{key:"type",value:"website"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-website",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:website-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:website-description",{maxLength:256})}, @@ -408,7 +405,7 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc ]})}, //REACTION ROLES - {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-role",{children:[ + {name:"role",priority:0,properties:[{key:"type",value:"role"}],checker:new api.ODCheckerObjectStructure("opendiscord:options-role",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:role-id","openticket","option-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-name",{minLength:2,maxLength:50})}, {key:"description",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-description",{maxLength:256})}, @@ -430,18 +427,18 @@ export const defaultOptionsStructure = new api.ODCheckerArrayStructure("opendisc }})}, //ROLE SETTINGS - {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1})}, + {key:"roles",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-roles","role",[],{allowDoubles:false,minLength:1,cliDisplayPropertyName:"role"})}, {key:"mode",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:role-mode",{choices:["add","remove","add&remove"]})}, - {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false})}, + {key:"removeRolesOnAdd",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_DiscordIdArray("opendiscord:role-remove-roles","role",[],{allowDoubles:false,cliDisplayPropertyName:"role"})}, {key:"addOnMemberJoin",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:role-add-on-join",{})}, ]})}, ]})}) -export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{children:[ +export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendiscord:panels",{allowedTypes:["object"],cliDisplayPropertyName:"panel",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:panels",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","dropdown"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:panel-id","openticket","panel-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-name",{minLength:3,maxLength:50})}, {key:"dropdown",optional:false,priority:0,checker:new api.ODCheckerBooleanStructure("opendiscord:panel-dropdown",{})}, - {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25})}, + {key:"options",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueIdArray("opendiscord:panel-options","openticket","option-ids","option-ids-used",{allowDoubles:false,maxLength:25,cliDisplayPropertyName:"option"})}, //EMBED & TEXT {key:"text",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:panel-text",{maxLength:4096})}, @@ -461,7 +458,7 @@ export const defaultPanelsStructure = new api.ODCheckerArrayStructure("opendisco ]})}, ]})}) -export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{children:[ +export const defaultQuestionsStructure = new api.ODCheckerArrayStructure("opendiscord:questions",{allowedTypes:["object"],cliDisplayPropertyName:"question",propertyChecker:new api.ODCheckerObjectStructure("opendiscord:questions",{cliDisplayKeyInParentArray:"name",cliDisplayAdditionalKeysInParentArray:["id","type"],children:[ {key:"id",optional:false,priority:0,checker:new api.ODCheckerCustomStructure_UniqueId("opendiscord:question-id","openticket","question-ids",{regex:/^[A-Za-z0-9-éèçàêâôûî]+$/,minLength:3,maxLength:40})}, {key:"name",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-name",{minLength:3,maxLength:45})}, {key:"type",optional:false,priority:0,checker:new api.ODCheckerStringStructure("opendiscord:question-type",{choices:["short","paragraph"]})}, @@ -604,4 +601,14 @@ export const defaultDropdownOptionsFunction = (manager:api.ODCheckerManager, fun }) return {valid:(final.length < 1),messages:final} -} \ No newline at end of file +} + +/* TODO + * - add & duplicate functionality for arrays (complex) + * - move & remove functionality for arrays (simple) + * - add/duplicate generators + forms for each structure type + * - implement default value & skip values in all structures + * - when sub-arrays are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. + * - when sub-objects are required to be configured in an add/duplicate generator, it will have an extra "next" option in the menu to go back to the next property to configure. + * - and more + */ \ No newline at end of file diff --git a/src/data/framework/flagLoader.ts b/src/data/framework/flagLoader.ts index 6a70f40..9c80a99 100644 --- a/src/data/framework/flagLoader.ts +++ b/src/data/framework/flagLoader.ts @@ -16,4 +16,5 @@ export const loadAllFlags = async () => { opendiscord.flags.add(new api.ODFlag("opendiscord:no-compile","No Compile","Disable compilation of plugins & bot before starting.","--no-compile",[])) opendiscord.flags.add(new api.ODFlag("opendiscord:compile-only","Compile Only","This description will never be shown because the bot wouldn't run when this flag is enabled :)","--compile-only",[])) opendiscord.flags.add(new api.ODFlag("opendiscord:silent","Silent Mode","Run the bot without displaying logs. The startscreen will still be displayed.","--silent",[])) + opendiscord.flags.add(new api.ODFlag("opendiscord:cli","Run CLI","This description will never be shown because the bot wouldn't run when this flag is enabled :)","--cli",[])) } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 33ed912..d2c0ca9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -269,9 +269,10 @@ const main = async () => { //render config checker const advancedCheckerFlag = opendiscord.flags.get("opendiscord:checker") const disableCheckerFlag = opendiscord.flags.get("opendiscord:no-checker") + const useCliFlag = opendiscord.flags.get("opendiscord:cli") await opendiscord.events.get("onCheckerRender").emit([opendiscord.checkers.renderer,opendiscord.checkers]) - if (opendiscord.defaults.getDefault("checkerRendering") && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + if (opendiscord.defaults.getDefault("checkerRendering") && !(disableCheckerFlag ? disableCheckerFlag.value : false) && !(useCliFlag ? useCliFlag.value : false)){ //check if there is a result (otherwise throw minor error) const result = opendiscord.checkers.lastResult if (!result) return opendiscord.log("Failed to render Config Checker! (couldn't fetch result)","error") @@ -290,7 +291,7 @@ const main = async () => { } //quit config checker (when required) - if (opendiscord.checkers.lastResult && !opendiscord.checkers.lastResult.valid && !(disableCheckerFlag ? disableCheckerFlag.value : false)){ + if (opendiscord.checkers.lastResult && !opendiscord.checkers.lastResult.valid && !(disableCheckerFlag ? disableCheckerFlag.value : false) && !(useCliFlag ? useCliFlag.value : false)){ await opendiscord.events.get("onCheckerQuit").emit([opendiscord.checkers]) if (opendiscord.defaults.getDefault("checkerQuit")){ process.exit(1) @@ -298,6 +299,14 @@ const main = async () => { } } + //switch to CLI context instead of running the bot + if (useCliFlag && useCliFlag.value){ + await (await (import("./core/startup/cli.js"))).execute() + await utilities.timer(1000) + console.log("\n\n"+ansis.red("❌ Something went wrong in the Interactive Setup CLI. Please try again or report a bug in our discord server.")) + process.exit(0) + } + //plugin loading before client await opendiscord.events.get("onPluginBeforeClientLoad").emit([]) await opendiscord.events.get("afterPluginBeforeClientLoaded").emit([])