From 5f79ae909b67fa05f741ba94be7bed544530cddf Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Sun, 6 Apr 2025 19:04:09 +0200 Subject: [PATCH] Added Interactive Setup CLI (Unfinished) Added the first part of the interactive setup CLI. You can explore the interface but some parts will not work yet. It will also not save to the config file yet. The "Quick Setup" & "Manage Plugins" parts of the CLI will arrive later this week. --- package.json | 8 +- src/core/api/defaults/checker.ts | 10 +- src/core/api/defaults/flag.ts | 1 + src/core/api/defaults/stat.ts | 1 - src/core/api/modules/checker.ts | 85 +++-- src/core/startup/cli.ts | 524 ++++++++++++++++++++++++++++ src/core/startup/init.ts | 10 +- src/data/framework/checkerLoader.ts | 75 ++-- src/data/framework/flagLoader.ts | 1 + src/index.ts | 13 +- 10 files changed, 649 insertions(+), 79 deletions(-) create mode 100644 src/core/startup/cli.ts 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([])