/* eslint-disable no-console */ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import { getExtensions } from "@factor/cli/extension-loader" import { getPath } from "@factor/api/paths" import { highlight } from "cli-highlight" import { log, sortPriority, deepMerge, applyFilters, addCallback, pushToFilter, slugify, } from "@factor/api" import chalk from "chalk" import envfile from "envfile" import fs from "fs-extra" import inquirer, { Answers } from "inquirer" import json2yaml from "json2yaml" import { FactorPackageJson } from "@factor/cli/types" export interface SetupCliConfig { name: string value: string callback: () => {} | void priority?: number } const configFile = getPath("config-file-public") const secretsFile = getPath("config-file-private") /** * Batch notices during CLI initialization * If they've already been logged, then just log them */ let __noticesLogged = false /** * Add a CLI notice * This allows for a consistent and 'all at once' output */ export const addNotice = (text: string): void => { if (__noticesLogged) { log.warn(text) } else { pushToFilter({ key: slugify(text.slice(1, 30)) ?? "", hook: "cli-notices", item: text, }) } } /** * Log the notices that have been added */ export const logNotices = (): void => { const notices = applyFilters("cli-notices", []) if (notices.length > 0) { log.log() notices.forEach((_: string) => { log.info(typeof _ == "string" ? chalk.bold(_) : _) }) log.log() } __noticesLogged = true } /** * Gets the names of a specific type of extension * @param type - type of extension * @param format - the format to return */ const extensionNames = (type: "plugin" | "theme" | "app", format = "join"): string => { const extensions = getExtensions().filter((_) => _.extend == type) if (extensions && extensions.length > 0) { const names = extensions.map((_) => _.name) return format == "count" ? names.length.toString() : names.join(", ") } else return "none" } /** * Gets existing configuration settings * Also returns packageJson for writing later */ const existingSettings = (): { packageJson: FactorPackageJson publicConfig: Record privateConfig: Record } => { const packageJson = require(configFile) const { factor: publicConfig = {} } = packageJson fs.ensureFileSync(secretsFile) const privateConfig = envfile.parseFileSync(secretsFile) return { publicConfig, privateConfig, packageJson } } /** * Runs the CLI setup utility */ export const runSetup = async (): Promise => { let answers: Answers log.formatted({ title: "Welcome to Factor Setup!", lines: [ { title: "Theme", value: extensionNames("theme"), indent: true }, { title: "Modules", value: extensionNames("plugin", "count"), indent: true, }, ], }) let setups: SetupCliConfig[] = applyFilters( "cli-add-setup", [ { name: "Exit Setup", value: "exit", callback: (): void => { // eslint-disable-next-line unicorn/no-process-exit process.exit() }, priority: 1000, }, ], existingSettings() ) setups = sortPriority(setups) // Escapes the endless loop const askAgain = true const ask = async (): Promise => { answers = await inquirer.prompt({ type: "list", name: `setupItem`, message: `What would you like to do?`, choices: setups, }) console.log() const setupRunner = setups.find((_: SetupCliConfig) => _.value == answers.setupItem) if (setupRunner) { await setupRunner.callback() } if (askAgain) await ask() } await ask() } /** * Reports to the user which configuration is missing */ export const logSetupNeeded = (command = ""): void => { const setupNeeded = applyFilters("setup-needed", []) if (setupNeeded.length > 0) { const lines = setupNeeded.map((_: { title: string; value: string }) => { return { title: _.title, value: _.value, indent: true } }) log.formatted({ title: "Setup Needed", lines, color: "cyan" }) } log.diagnostic({ event: "factorCommand", action: `${command}-${setupNeeded.length}` }) } addCallback({ key: "notices", hook: "dev-server-built", callback: () => logNotices() }) /** * Hook into the CLI command filter */ addCallback({ key: "setup", hook: "cli-setup", callback: () => runSetup() }) /** * Output JSON nicely to the CLI * @param data - data to output */ export const prettyJson = (data: object): string => { return highlight(json2yaml.stringify(data, null, " ")) } /** * Writes configuration to the private or public config files * @param file - private or public config * @param values - object map of values */ export const writeFiles = ( file: "public" | "private" | "package", values: object ): void => { const { publicConfig, privateConfig } = existingSettings() let { packageJson } = existingSettings() if (file == "public" || file == "package") { if (file == "public") { packageJson.factor = deepMerge([publicConfig, values]) } else { packageJson = deepMerge([packageJson, values]) as FactorPackageJson } fs.writeFileSync(configFile, JSON.stringify(packageJson, null, " ")) // In case the built file is used later in process delete require.cache[configFile] } if (file == "private") { const sec = deepMerge([privateConfig, values]) fs.writeFileSync(secretsFile, envfile.stringifySync(sec)) // In case the built file is used later in process delete require.cache[secretsFile] } } /** * Display to the user the values that will be written and confirm with them * @param file - private or public config * @param values - object map of values */ export const writeConfig = async ( file: "public" | "private", values: object ): Promise => { if (!file || !values) { return } const fileName = file == "public" ? "package.json" : ".env" const outFile = chalk.cyan(fileName) const answers = await inquirer.prompt({ type: "confirm", name: `writeFiles`, message: `Write the following ${file} config to the ${outFile} file? \n\n ${prettyJson( values )} \n`, default: true, }) console.log() if (answers.writeFiles) { writeFiles(file, values) log.success(`Wrote to ${fileName}...\n\n`) } else { log.log(`Writing skipped.`) } console.log() return }