Started with CLI "Quick Setup" feature.

This commit is contained in:
DJj123dj
2025-04-16 21:44:23 +02:00
parent ad623f9650
commit e73c8e9b3b
4 changed files with 200 additions and 54 deletions
+75
View File
@@ -0,0 +1,75 @@
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. */
export 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. */
export 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. */
export function renderHeader(path:(string|number)[]|string){
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 (typeof path == "string") terminal.cyan(centerText(path+"\n\n",88))
else 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 renderCliModeSelector(backFn:(() => api.ODPromiseVoid)){
renderHeader([])
terminal(ansis.bold.green("Please select what CLI module you want to use.\n")+ansis.italic.gray("(use arrow keys to navigate, exit using escape)\n"))
const answer = await terminal.singleColumnMenu([
"✏️ Edit Config "+ansis.gray("=> Edit the current config, add/remove new tickets/questions/panels & more!"),
"⏱️ Quick Setup "+ansis.gray("=> A quick and easy way of setting up the bot in your Discord server."),
],{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
if (answer.canceled) return await backFn()
else if (answer.selectedIndex == 0) await (await import("./editConfig.js")).renderEditConfig(async () => {await renderCliModeSelector(backFn)})
else if (answer.selectedIndex == 1) await (await import("./quickSetup.js")).renderQuickSetup(async () => {await renderCliModeSelector(backFn)})
}
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 renderCliModeSelector(terminate)
}
+933
View File
@@ -0,0 +1,933 @@
import {opendiscord, api, utilities} from "../../index"
import {Terminal, terminal} from "terminal-kit"
import ansis from "ansis"
import {renderHeader} from "./cli"
export async function renderEditConfig(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 renderEditConfig(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 api.ODSystemError("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()
if (structure.options.cliDisplayName){
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName)+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
}
const list = structure.options.children.filter((child) => !child.cliHideInEditMode)
const nameList = list.map((child) => (child.checker.options.cliDisplayName ? child.checker.options.cliDisplayName : child.key))
const nameLength = utilities.getLongestLength(nameList)
const finalnameList = nameList.map((name,index) => name.padEnd(nameLength+5," ")+ansis.gray((!list[index].checker.options.cliHideDescriptionInParent && list[index].checker.options.cliDisplayDescription) ? "=> "+list[index].checker.options.cliDisplayDescription : ""))
const answer = await terminal.singleColumnMenu(finalnameList,{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold.defaultColor,
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 api.ODSystemError("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 api.ODSystemError("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 api.ODSystemError("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 api.ODSystemError("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()
if (structure.options.cliDisplayName || typeof parentIndex == "string" || !isNaN(parentIndex)){
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? parentIndex.toString())+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
}
const propertyName = structure.options.cliDisplayPropertyName ?? "index"
const answer = await terminal.singleColumnMenu(data.length < 1 ? ["Add "+propertyName] : [
"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
const backFnFunc = async () => {await renderConfigArrayStructureSelector(checker,backFn,structure,data,parent,parentIndex,path)}
if (answer.canceled) return await backFn()
if (answer.selectedIndex == 0) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => {
data[data.length] = newData
await checker.config.save()
await backFnFunc()
},structure.options.propertyChecker,data,data.length,path,[])
else if (answer.selectedIndex == 1) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path)
else if (answer.selectedIndex == 2) await renderconfigArrayStructureMoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path)
else if (answer.selectedIndex == 3) await renderconfigArrayStructureRemoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path)
else if (answer.selectedIndex == 4) await renderConfigArrayStructureDuplicateSelector(checker,backFnFunc,structure,structure.options.propertyChecker,data,parent,parentIndex,path)
}
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()
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)),{
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)
const subData = data[dataAnswer.selectedIndex]
const slicedData = [...data.slice(0,dataAnswer.selectedIndex),...data.slice(dataAnswer.selectedIndex+1)]
const insertedData = [...slicedData.slice(0,moveAnswer.selectedIndex),subData,...slicedData.slice(moveAnswer.selectedIndex)]
insertedData.forEach((d,i) => data[i] = d)
await checker.config.save()
terminal.bold.blue("\n\n✅ Property moved succesfully!")
await utilities.timer(400)
await backFn()
}
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()
data.splice(dataAnswer.selectedIndex,1)
await checker.config.save()
terminal.bold.blue("\n\n✅ Property deleted succesfully!")
await utilities.timer(400)
await backFn()
}
async function renderConfigArrayStructureDuplicateSelector(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 duplicate.\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()
data.push(JSON.parse(JSON.stringify(data[dataAnswer.selectedIndex])))
await checker.config.save()
terminal.bold.blue("\n\n✅ Property duplicated succesfully!")
await utilities.timer(400)
await backFn()
}
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 api.ODSystemError("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("#"+(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")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\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
await checker.config.save()
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)[],prefillValue?:string){
if (typeof data != "number") throw new api.ODSystemError("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("#"+(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")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const answer = await terminal.inputField({
default:prefillValue,
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
await checker.config.save()
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,answer)
}
}
async function renderConfigStringStructureEditor(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,data:string,parent:object,parentIndex:string|number,path:(string|number)[],prefillValue?:string){
if (typeof data != "string") throw new api.ODSystemError("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("#"+(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)+ansis.gray(!data.includes("\n") ? "\"\n" : "\n"))
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined
const customAutocompleteFunc = structure.options.cliAutocompleteFunc ? await structure.options.cliAutocompleteFunc() : null
const autocompleteList = ((customAutocompleteFunc ?? structure.options.cliAutocompleteList) ?? customExtraOptions) ?? structure.options.choices
const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = {
style:terminal.white,
selectedStyle:terminal.bgBlue.white
}
const input = terminal.inputField({
default:prefillValue,
style:terminal.cyan,
hintStyle:terminal.gray,
cancelable:false,
autoComplete:autocompleteList,
autoCompleteHint:(!!autocompleteList),
autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false
})
terminal.on("key",async (name:string,matches:string[],data:object) => {
if (name == "ESCAPE"){
terminal.removeListener("key","cli-render-string-structure-edit")
input.abort()
await backFn()
}
},({id:"cli-render-string-structure-edit"} as any))
const answer = await input.promise
terminal.removeListener("key","cli-render-string-structure-edit")
if (typeof answer != "string") return
//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
await checker.config.save()
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,answer)
}
}
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 api.ODSystemError("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("#"+(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")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\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
await checker.config.save()
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("#"+(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")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\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 "<unknown-property>".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 "<unknown-property>".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 "<unknown-property>".length
}else return "<unknown-property>".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 "<unknown-property>"
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 "<unknown-property>"
}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 "<unknown-property>"
}else return "<unknown-property>"
}
async function chooseAdditionConfigStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
if (structure instanceof api.ODCheckerObjectStructure) await renderAdditionConfigObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerBooleanStructure) await renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerNumberStructure) await renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerStringStructure) await renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerNullStructure) await renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerEnabledObjectStructure) await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerObjectSwitchStructure) await renderAdditionConfigObjectSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerArrayStructure) await renderAdditionConfigArrayStructureSelector(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else if (structure instanceof api.ODCheckerTypeSwitchStructure) await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
else await backFn()
}
async function renderAdditionConfigObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],localData:object={}){
const children = structure.options.children ?? []
const skipKeys = (structure.options.cliInitSkipKeys ?? [])
//add skipped properties
for (const key of skipKeys){
const childStructure = children.find((c) => c.key == key)
if (childStructure){
const defaultValue = childStructure.checker.options.cliInitDefaultValue
if (childStructure.checker instanceof api.ODCheckerBooleanStructure) localData[key] = (typeof defaultValue == "boolean" ? defaultValue : false)
else if (childStructure.checker instanceof api.ODCheckerNumberStructure) localData[key] = (typeof defaultValue == "number" ? defaultValue : 0)
else if (childStructure.checker instanceof api.ODCheckerStringStructure) localData[key] = (typeof defaultValue == "string" ? defaultValue : "")
else if (childStructure.checker instanceof api.ODCheckerNullStructure) localData[key] = (defaultValue === null ? defaultValue : null)
else if (childStructure.checker instanceof api.ODCheckerArrayStructure) localData[key] = (Array.isArray(defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : [])
else if (childStructure.checker instanceof api.ODCheckerObjectStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {})
else if (childStructure.checker instanceof api.ODCheckerObjectSwitchStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {})
else if (childStructure.checker instanceof api.ODCheckerEnabledObjectStructure) localData[key] = ((typeof defaultValue == "object" && !Array.isArray(defaultValue) && defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : {})
else if (childStructure.checker instanceof api.ODCheckerTypeSwitchStructure && typeof defaultValue != "undefined") localData[key] = JSON.parse(JSON.stringify(defaultValue))
else throw new api.ODSystemError("OT CLI => Object skip key has an invalid checker structure! key: "+key)
}
}
//add properties that need to be configured
const configChildren = children.filter((c) => !skipKeys.includes(c.key)).map((c) => {return {key:c.key,checker:c.checker}})
await configureAdditionObjectProperties(checker,configChildren,0,localData,[...path,parentIndex],(typeof parentIndex == "number") ? [...localPath] : [...localPath,parentIndex],async () => {
//go back to previous screen
await backFn()
},async () => {
//finish setup
terminal.bold.blue("\n\n✅ Variable saved succesfully!")
await utilities.timer(400)
await nextFn(localData)
})
}
async function configureAdditionObjectProperties(checker:api.ODChecker,children:{key:string,checker:api.ODCheckerStructure}[],currentIndex:number,localData:object,path:(string|number)[],localPath:(string|number)[],backFn:(() => api.ODPromiseVoid),nextFn:(() => api.ODPromiseVoid)){
if (children.length < 1) return await nextFn()
const child = children[currentIndex]
await chooseAdditionConfigStructure(checker,async () => {
if (children[currentIndex-1]) await configureAdditionObjectProperties(checker,children,currentIndex-1,localData,path,localPath,backFn,nextFn)
else await backFn()
},async (data) => {
localData[child.key] = data
if (children[currentIndex+1]) await configureAdditionObjectProperties(checker,children,currentIndex+1,localData,path,localPath,backFn,nextFn)
else await nextFn()
},child.checker,localData,child.key,path,localPath)
}
async function renderAdditionConfigEnabledObjectStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerEnabledObjectStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
const enabledProperty = structure.options.property
const enabledValue = structure.options.enabledValue
const subStructure = structure.options.checker
if (!enabledProperty || !subStructure || !subStructure.options.children) return await backFn()
let propertyStructure: api.ODCheckerBooleanStructure|api.ODCheckerNumberStructure|api.ODCheckerStringStructure
if (typeof enabledValue == "string") propertyStructure = new api.ODCheckerStringStructure("opendiscord:CLI-checker-enabled-object-structure",{})
else if (typeof enabledValue == "number") propertyStructure = new api.ODCheckerNumberStructure("opendiscord:CLI-checker-enabled-object-structure",{})
else if (typeof enabledValue == "boolean") propertyStructure = new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-enabled-object-structure",{})
else throw new Error("OT CLI => enabled object structure has an invalid type of enabledProperty. It must be a primitive boolean/number/string.")
const localData = {}
await chooseAdditionConfigStructure(checker,backFn,async (data) => {
if (data === enabledValue) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,subStructure,parent,parentIndex,path,localPath,localData)
else{
localData[enabledProperty] = data
//copy old object checker to new object checker => all options get de-referenced (this is needed for the new object skip keys are temporary)
const newStructure = new api.ODCheckerObjectStructure(subStructure.id,{children:[]})
//copy all options over to the new checker
newStructure.options.children = [...subStructure.options.children]
newStructure.options.cliInitSkipKeys = subStructure.options.children.map((child) => child.key)
for (const key of Object.keys(subStructure.options)){
if (key != "children" && key != "cliInitSkipKeys") newStructure.options[key] = subStructure.options[key]
}
//adds all properties to object as "skipKeys", then continues to next function
await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigEnabledObjectStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,newStructure,parent,parentIndex,path,localPath,localData)
await nextFn(localData)
}
},propertyStructure,localData,enabledProperty,[...path,parentIndex],[...localPath,parentIndex])
}
async function renderAdditionConfigObjectSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerObjectSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("What type of object would you like to add?\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n"))
const answer = await terminal.singleColumnMenu(structure.options.objects.map((obj) => obj.name),{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
if (answer.canceled) return await backFn()
const objectTemplate = structure.options.objects[answer.selectedIndex]
//copy old object checker to new object checker => all options get de-referenced (this is needed for the new object switch properties which are temporary)
const oldStructure = objectTemplate.checker
const newStructure = new api.ODCheckerObjectStructure(oldStructure.id,{children:[]})
//copy all options over to the new checker
newStructure.options.children = [...oldStructure.options.children]
newStructure.options.cliInitSkipKeys = [...(oldStructure.options.cliInitSkipKeys ?? [])]
for (const key of Object.keys(oldStructure.options)){
if (key != "children" && key != "cliInitSkipKeys") newStructure.options[key] = oldStructure.options[key]
}
//add the keys of the object switch properties to the 'cliInitSkipKeys' because they need to be skipped.
objectTemplate.properties.map((p) => p.key).forEach((p) => {
if (!newStructure.options.cliInitSkipKeys) newStructure.options.cliInitSkipKeys = [p]
else if (!newStructure.options.cliInitSkipKeys.includes(p)) newStructure.options.cliInitSkipKeys.push(p)
})
//add structure checkers for all properties
for (const prop of objectTemplate.properties){
if (!newStructure.options.children.find((child) => child.key === prop.key)){
if (typeof prop.value == "string") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerStringStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})})
else if (typeof prop.value == "number") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerNumberStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})})
else if (typeof prop.value == "boolean") newStructure.options.children.unshift({key:prop.key,optional:false,priority:1,checker:new api.ODCheckerBooleanStructure("opendiscord:CLI-checker-object-switch-structure",{cliInitDefaultValue:prop.value})})
}
}
await chooseAdditionConfigStructure(checker,backFn,nextFn,newStructure,parent,parentIndex,path,localPath)
}
async function renderAdditionConfigBooleanStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerBooleanStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the boolean property "+ansis.blue("\""+parentIndex+"\"") : "boolean property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n"))
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\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){
terminal.bold.blue("\n\n✅ Variable saved succesfully!")
await utilities.timer(400)
await nextFn(newValue)
}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 renderAdditionConfigBooleanStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
}
}
async function renderAdditionConfigNumberStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNumberStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],prefillValue?:string){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the number property "+ansis.blue("\""+parentIndex+"\"") : "number property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n"))
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const answer = await terminal.inputField({
default:prefillValue,
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){
terminal.bold.blue("\n\n✅ Variable saved succesfully!")
await utilities.timer(400)
await nextFn(newValue)
}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 renderAdditionConfigNumberStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,answer)
}
}
async function renderAdditionConfigStringStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerStringStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],prefillValue?:string){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the string property "+ansis.blue("\""+parentIndex+"\"") : "string property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(insert a new value and press enter, go back using escape)\n"))
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const customExtraOptions = (structure instanceof api.ODCheckerCustomStructure_DiscordId) ? structure.extraOptions : undefined
const customAutocompleteFunc = structure.options.cliAutocompleteFunc ? await structure.options.cliAutocompleteFunc() : null
const autocompleteList = ((customAutocompleteFunc ?? structure.options.cliAutocompleteList) ?? customExtraOptions) ?? structure.options.choices
const autoCompleteMenuOpts: Terminal.SingleLineMenuOptions = {
style:terminal.white,
selectedStyle:terminal.bgBlue.white
}
const input = terminal.inputField({
default:prefillValue,
style:terminal.cyan,
hintStyle:terminal.gray,
cancelable:false,
autoComplete:autocompleteList,
autoCompleteHint:(!!autocompleteList),
autoCompleteMenu:(autocompleteList) ? autoCompleteMenuOpts as Terminal.Autocompletion : false
})
terminal.on("key",async (name:string,matches:string[],data:object) => {
if (name == "ESCAPE"){
terminal.removeListener("key","cli-render-string-structure-add")
input.abort()
await backFn()
}
},({id:"cli-render-string-structure-add"} as any))
const answer = await input.promise
terminal.removeListener("key","cli-render-string-structure-add")
if (typeof answer != "string") return
//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){
terminal.bold.blue("\n\n✅ Variable saved succesfully!")
await utilities.timer(400)
await nextFn(newValue)
}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 renderAdditionConfigStringStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,answer)
}
}
async function renderAdditionConfigNullStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerNullStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the null property "+ansis.blue("\""+parentIndex+"\"") : "null property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n"))
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\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){
terminal.bold.blue("\n\n✅ Variable saved succesfully!")
await utilities.timer(400)
await nextFn(newValue)
}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 renderAdditionConfigNullStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)
}
}
async function renderAdditionConfigArrayStructureSelector(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerArrayStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[],localData:any[]=[]){
renderHeader([...path,parentIndex])
terminal(ansis.bold.green("Please select what you would like to do with the new array.\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n"))
if (!structure.options.propertyChecker) return await backFn()
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const propertyName = structure.options.cliDisplayPropertyName ?? "index"
const answer = await terminal.singleColumnMenu(localData.length < 1 ? [ansis.magenta("-> Continue to next variable"),"Add "+propertyName] : [
ansis.magenta("-> Continue to next variable"),
"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
const backFnFunc = async () => {await renderAdditionConfigArrayStructureSelector(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath,localData)}
if (answer.canceled) return await backFn()
if (answer.selectedIndex == 0) await nextFn(localData)
else if (answer.selectedIndex == 1) await chooseAdditionConfigStructure(checker,backFnFunc,async (newData) => {
localData[localData.length] = newData
await backFnFunc()
},structure.options.propertyChecker,localData,localData.length,path,[])
else if (answer.selectedIndex == 2) await renderConfigArrayStructureEditSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path)
else if (answer.selectedIndex == 3) await renderconfigArrayStructureMoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path)
else if (answer.selectedIndex == 4) await renderconfigArrayStructureRemoveSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path)
else if (answer.selectedIndex == 5) await renderConfigArrayStructureDuplicateSelector(checker,backFnFunc,structure,structure.options.propertyChecker,localData,parent,parentIndex,path)
}
async function renderAdditionConfigTypeSwitchStructure(checker:api.ODChecker,backFn:(() => api.ODPromiseVoid),nextFn:((data:any) => api.ODPromiseVoid),structure:api.ODCheckerTypeSwitchStructure,parent:object|any[],parentIndex:string|number,path:(string|number)[],localPath:(string|number)[]){
renderHeader(path)
terminal(ansis.bold.green("You are now creating "+(typeof parentIndex == "string" ? "the property "+ansis.blue("\""+parentIndex+"\"") : "property "+ansis.blue("#"+(parentIndex+1)))+".\n")+ansis.italic.gray("(use arrow keys to navigate, go back using escape)\n"))
terminal.gray("\nProperty: "+ansis.bold.blue(structure.options.cliDisplayName ?? [...localPath,parentIndex].join("."))+"\n")
terminal.gray("Description: "+ansis.bold(structure.options.cliDisplayDescription ?? "/")+"\n")
const actionsList: string[] = []
if (structure.options.boolean) actionsList.push("Create as boolean")
if (structure.options.string) actionsList.push("Create as string")
if (structure.options.number) actionsList.push("Create as number")
if (structure.options.object) actionsList.push("Create as object")
if (structure.options.array) actionsList.push("Create as array/list")
if (structure.options.null) actionsList.push("Create 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("Create as boolean") && structure.options.boolean) await renderAdditionConfigBooleanStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.boolean,parent,parentIndex,path,localPath)
else if (answer.selectedText.startsWith("Create as string") && structure.options.string) await renderAdditionConfigStringStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.string,parent,parentIndex,path,localPath)
else if (answer.selectedText.startsWith("Create as number") && structure.options.number) await renderAdditionConfigNumberStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.number,parent,parentIndex,path,localPath)
else if (answer.selectedText.startsWith("Create as object") && structure.options.object) await renderAdditionConfigObjectStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.object,parent,parentIndex,path,localPath)
else if (answer.selectedText.startsWith("Create as array/list") && structure.options.array) await renderAdditionConfigArrayStructureSelector(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.array,parent,parentIndex,path,localPath)
else if (answer.selectedText.startsWith("Create as null") && structure.options.null) await renderAdditionConfigNullStructure(checker,async () => {await renderAdditionConfigTypeSwitchStructure(checker,backFn,nextFn,structure,parent,parentIndex,path,localPath)},nextFn,structure.options.null,parent,parentIndex,path,localPath)
}
+121
View File
@@ -0,0 +1,121 @@
import {opendiscord, api, utilities} from "../../index"
import {Terminal, terminal} from "terminal-kit"
import ansis from "ansis"
import {renderHeader} from "./cli"
export async function renderQuickSetup(backFn:() => api.ODPromiseVoid){
if (quickSetupRequiresReset()) await renderQuickSetupWarning(backFn)
else await renderQuickSetupWelcome(backFn)
}
function quickSetupRequiresReset(): boolean {
const generalConfig = opendiscord.configs.get("opendiscord:general")
if (generalConfig.data.token != "your bot token here! (or leave empty when using 'tokenFromENV')") return true
if (generalConfig.data.mainColor != "#f8ba00") return true
if (generalConfig.data.language != "english") return true
if (generalConfig.data.prefix != "!ticket ") return true
if (generalConfig.data.serverId != "discord server id") return true
return false
}
async function renderQuickSetupWarning(backFn:() => api.ODPromiseVoid) {
renderHeader("⏱️ Open Ticket Quick Setup: Warning")
terminal.bold(ansis.yellow("WARNING! ")+ansis.red("By using the 'Quick Setup' feature, your current config will be completely resetted!"))
terminal.gray("\n\nAre you sure you want to continue?")
const answer = await terminal.singleColumnMenu([
ansis.green("✅ No, take me back."),
ansis.red("🚨 Yes, continue and reset the config.")
],{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
if (answer.canceled || answer.selectedIndex == 0) return await backFn()
if (answer.selectedIndex == 1) await renderQuickSetupWelcome(async () => {await renderQuickSetupWarning(backFn)})
}
async function renderQuickSetupWelcome(backFn:() => api.ODPromiseVoid){
renderHeader("⏱️ Open Ticket Quick Setup: Introduction")
terminal.bold.underline.blue("Open Ticket: Quick Setup\n")
terminal.gray([
"Hi there! Thank you for downloading and installing Open Ticket.",
"You have chosen to configure the bot using the 'Quick Setup CLI'.",
"",
"This program will help you with configuring Open Ticket using a step-by-step method.",
"If you've ever used Google Forms, then this will probably be very easy for you 😉.",
"",
ansis.magenta("The configuration should normally only take around 5 minutes."),
ansis.magenta("Once you've completed the form, the bot is technically ready for usage!")
].join("\n")+"\n\n")
const answer = await terminal.singleColumnMenu([
ansis.green("Press 'Enter' to start!")
],{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
if (answer.canceled) return await backFn()
else if (answer.selectedIndex == 0) await renderQuickSetupDevPortalInfo(async () => {await renderQuickSetupWelcome(backFn)})
}
async function renderQuickSetupDevPortalInfo(backFn:() => api.ODPromiseVoid){
renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience")
terminal.bold.blue("(Step 1) Have you already created a Discord bot to use for Open Ticket?\n")
const answer = await terminal.singleColumnMenu([
"✅ Yes I have, and it has been invited to the server.",
"❓ No not yet, I don't know how to create one.",
"👶 I've never seen a Discord bot before.",
],{
leftPadding:"> ",
style:terminal.gray,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
if (answer.canceled) return await backFn()
else if (answer.selectedIndex == 0) await backFn() //TODO NEXT QUESTION
else if (answer.selectedIndex == 1) await renderQuickSetupDevPortalGuide(0,async () => {await renderQuickSetupDevPortalInfo(backFn)})
else if (answer.selectedIndex == 2) await renderQuickSetupDevPortalGuide(1,async () => {await renderQuickSetupDevPortalInfo(backFn)})
}
async function renderQuickSetupDevPortalGuide(variation:0|1,backFn:() => api.ODPromiseVoid){
renderHeader("⏱️ Open Ticket Quick Setup: Dev Portal & Experience")
if (variation == 0){
terminal.bold.blue("(Step 1.1) You've mentioned that you don't know how to create a Discord bot.\n\n")
terminal.gray("Please visit the following URL for a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n"))
}else{
terminal.bold.blue("(Step 1.2) You've mentioned that you've never seen Discord bot before.\n\n")
terminal.gray("How did you even download Open Ticket 🤪? But still, we have a step-by-step guide on how to create a Discord bot.\nIf it still doesn't work, join our Discord server and we will help you further!\n"+ansis.magenta("=> https://otdocs.dj-dj.be/docs/guides/get-started#bot\n\n"))
}
const answer = await terminal.singleColumnMenu([
ansis.green("✅ Alright, I've made the bot. Take me back please!")
],{
leftPadding:"> ",
style:terminal.cyan,
selectedStyle:terminal.bgDefaultColor.bold,
submittedStyle:terminal.bgBlue,
extraLines:2,
cancelable:true
}).promise
return await backFn()
}