diff --git a/index.ts b/index.ts index 3d5ff9190db85f8c6cff82d37ce3b2d7967ba5e8..72ee5cd8ba00eef6c6e61c3b326743f47df0d5c3 100644 --- a/index.ts +++ b/index.ts @@ -1,20 +1,18 @@ #!/usr/bin/env node /* eslint-disable import/no-extraneous-dependencies */ -import ciInfo from "ci-info"; import Commander from "commander"; import Conf from "conf"; import fs from "fs"; import path from "path"; -import { blue, bold, cyan, green, red, yellow } from "picocolors"; +import { bold, cyan, green, red, yellow } from "picocolors"; import prompts from "prompts"; import checkForUpdate from "update-check"; -import { InstallAppArgs, createApp } from "./create-app"; -import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant"; +import { createApp } from "./create-app"; import { getPkgManager } from "./helpers/get-pkg-manager"; import { isFolderEmpty } from "./helpers/is-folder-empty"; -import { getRepoRootFolders } from "./helpers/repo"; import { validateNpmName } from "./helpers/validate-pkg"; import packageJson from "./package.json"; +import { QuestionArgs, askQuestions, onPromptState } from "./questions"; let projectPath: string = ""; @@ -23,16 +21,6 @@ const handleSigTerm = () => process.exit(0); process.on("SIGINT", handleSigTerm); process.on("SIGTERM", handleSigTerm); -const onPromptState = (state: any) => { - if (state.aborted) { - // If we don't re-enable the terminal cursor before exiting - // the program, the cursor will remain hidden - process.stdout.write("\x1B[?25h"); - process.stdout.write("\n"); - process.exit(1); - } -}; - const program = new Commander.Command(packageJson.name) .version(packageJson.version) .arguments("<project-directory>") @@ -157,259 +145,8 @@ async function run(): Promise<void> { process.exit(1); } - // TODO: use Args also for program - type Args = Omit<InstallAppArgs, "appPath" | "packageManager">; - - const preferences = (conf.get("preferences") || {}) as Args; - - const defaults: Args = { - template: "streaming", - framework: "nextjs", - engine: "simple", - ui: "html", - eslint: true, - frontend: false, - openAIKey: "", - model: "gpt-3.5-turbo", - communityProjectPath: "", - }; - const getPrefOrDefault = (field: keyof Args) => - preferences[field] ?? defaults[field]; - - const handlers = { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }; - - if (!program.template) { - if (ciInfo.isCI) { - program.template = getPrefOrDefault("template"); - } else { - const { template } = await prompts( - { - type: "select", - name: "template", - message: "Which template would you like to use?", - choices: [ - { title: "Chat without streaming", value: "simple" }, - { title: "Chat with streaming", value: "streaming" }, - { title: "Community templates", value: "community" }, - ], - initial: 1, - }, - handlers, - ); - program.template = template; - preferences.template = template; - } - } - - if (program.template === "community") { - const rootFolderNames = await getRepoRootFolders( - COMMUNITY_OWNER, - COMMUNITY_REPO, - ); - const { communityProjectPath } = await prompts( - { - type: "select", - name: "communityProjectPath", - message: "Select community templates?", - choices: rootFolderNames.map((name) => ({ - title: name, - value: name, - })), - initial: 0, - }, - { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }, - ); - - program.communityProjectPath = communityProjectPath; - preferences.communityProjectPath = communityProjectPath; - } else { - if (!program.framework) { - if (ciInfo.isCI) { - program.framework = getPrefOrDefault("framework"); - } else { - const allChoices = [ - { title: "NextJS", value: "nextjs" }, - { title: "Express", value: "express" }, - { title: "FastAPI (Python)", value: "fastapi" }, - ]; - const choiceIndexes = - program.template === "simple" ? [1, 2] : [0, 1, 2]; - const choices = allChoices.filter((_, i) => choiceIndexes.includes(i)); - - const { framework } = await prompts( - { - type: "select", - name: "framework", - message: "Which framework would you like to use?", - choices, - initial: 0, - }, - handlers, - ); - program.framework = framework; - preferences.framework = framework; - } - } - - if (program.framework === "nextjs") { - program.template = "streaming"; - } - - if (program.framework === "express" || program.framework === "fastapi") { - // if a backend-only framework is selected, ask whether we should create a frontend - if (!program.frontend) { - if (ciInfo.isCI) { - program.frontend = getPrefOrDefault("frontend"); - } else { - const styledNextJS = blue("NextJS"); - const styledBackend = green( - program.framework === "express" - ? "Express " - : program.framework === "fastapi" - ? "FastAPI (Python) " - : "", - ); - const { frontend } = await prompts({ - onState: onPromptState, - type: "toggle", - name: "frontend", - message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`, - initial: getPrefOrDefault("frontend"), - active: "Yes", - inactive: "No", - }); - program.frontend = Boolean(frontend); - preferences.frontend = Boolean(frontend); - } - } - } - - if (program.framework === "nextjs" || program.frontend) { - if (!program.ui) { - if (ciInfo.isCI) { - program.ui = getPrefOrDefault("ui"); - } else { - const { ui } = await prompts( - { - type: "select", - name: "ui", - message: "Which UI would you like to use?", - choices: [ - { title: "Just HTML", value: "html" }, - { title: "Shadcn", value: "shadcn" }, - ], - initial: 0, - }, - handlers, - ); - program.ui = ui; - preferences.ui = ui; - } - } - } - - if (program.framework === "nextjs") { - if (!program.model) { - if (ciInfo.isCI) { - program.model = getPrefOrDefault("model"); - } else { - const { model } = await prompts( - { - type: "select", - name: "model", - message: "Which model would you like to use?", - choices: [ - { title: "gpt-3.5-turbo", value: "gpt-3.5-turbo" }, - { title: "gpt-4", value: "gpt-4" }, - { title: "gpt-4-1106-preview", value: "gpt-4-1106-preview" }, - { - title: "gpt-4-vision-preview", - value: "gpt-4-vision-preview", - }, - ], - initial: 0, - }, - handlers, - ); - program.model = model; - preferences.model = model; - } - } - } - - if (program.framework === "express" || program.framework === "nextjs") { - if (!program.engine) { - if (ciInfo.isCI) { - program.engine = getPrefOrDefault("engine"); - } else { - const { engine } = await prompts( - { - type: "select", - name: "engine", - message: "Which chat engine would you like to use?", - choices: [ - { title: "ContextChatEngine", value: "context" }, - { - title: "SimpleChatEngine (no data, just chat)", - value: "simple", - }, - ], - initial: 0, - }, - handlers, - ); - program.engine = engine; - preferences.engine = engine; - } - } - } - - if (!program.openAIKey) { - const { key } = await prompts( - { - type: "text", - name: "key", - message: "Please provide your OpenAI API key (leave blank to skip):", - }, - handlers, - ); - program.openAIKey = key; - preferences.openAIKey = key; - } - - if ( - program.framework !== "fastapi" && - !process.argv.includes("--eslint") && - !process.argv.includes("--no-eslint") - ) { - if (ciInfo.isCI) { - program.eslint = getPrefOrDefault("eslint"); - } else { - const styledEslint = blue("ESLint"); - const { eslint } = await prompts({ - onState: onPromptState, - type: "toggle", - name: "eslint", - message: `Would you like to use ${styledEslint}?`, - initial: getPrefOrDefault("eslint"), - active: "Yes", - inactive: "No", - }); - program.eslint = Boolean(eslint); - preferences.eslint = Boolean(eslint); - } - } - } + const preferences = (conf.get("preferences") || {}) as QuestionArgs; + await askQuestions(program as unknown as QuestionArgs, preferences); await createApp({ template: program.template, diff --git a/questions.ts b/questions.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f0786acb0197988b8423025c09fe796c98f6085 --- /dev/null +++ b/questions.ts @@ -0,0 +1,277 @@ +import ciInfo from "ci-info"; +import { blue, green } from "picocolors"; +import prompts from "prompts"; +import { InstallAppArgs } from "./create-app"; +import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant"; +import { getRepoRootFolders } from "./helpers/repo"; + +export type QuestionArgs = Omit<InstallAppArgs, "appPath" | "packageManager">; + +const defaults: QuestionArgs = { + template: "streaming", + framework: "nextjs", + engine: "simple", + ui: "html", + eslint: true, + frontend: false, + openAIKey: "", + model: "gpt-3.5-turbo", + communityProjectPath: "", +}; + +const handlers = { + onCancel: () => { + console.error("Exiting."); + process.exit(1); + }, +}; + +export const onPromptState = (state: any) => { + if (state.aborted) { + // If we don't re-enable the terminal cursor before exiting + // the program, the cursor will remain hidden + process.stdout.write("\x1B[?25h"); + process.stdout.write("\n"); + process.exit(1); + } +}; + +export const askQuestions = async ( + program: QuestionArgs, + preferences: QuestionArgs, +) => { + const getPrefOrDefault = <K extends keyof QuestionArgs>( + field: K, + ): QuestionArgs[K] => preferences[field] ?? defaults[field]; + + if (!program.template) { + if (ciInfo.isCI) { + program.template = getPrefOrDefault("template"); + } else { + const styledRepo = blue( + `https://github.com/${COMMUNITY_OWNER}/${COMMUNITY_REPO}`, + ); + const { template } = await prompts( + { + type: "select", + name: "template", + message: "Which template would you like to use?", + choices: [ + { title: "Chat without streaming", value: "simple" }, + { title: "Chat with streaming", value: "streaming" }, + { + title: `Community template from ${styledRepo}`, + value: "community", + }, + ], + initial: 1, + }, + handlers, + ); + program.template = template; + preferences.template = template; + } + } + + if (program.template === "community") { + const rootFolderNames = await getRepoRootFolders( + COMMUNITY_OWNER, + COMMUNITY_REPO, + ); + const { communityProjectPath } = await prompts( + { + type: "select", + name: "communityProjectPath", + message: "Select community template", + choices: rootFolderNames.map((name) => ({ + title: name, + value: name, + })), + initial: 0, + }, + { + onCancel: () => { + console.error("Exiting."); + process.exit(1); + }, + }, + ); + + program.communityProjectPath = communityProjectPath; + preferences.communityProjectPath = communityProjectPath; + return; // early return - no further questions needed for community projects + } + + if (!program.framework) { + if (ciInfo.isCI) { + program.framework = getPrefOrDefault("framework"); + } else { + const choices = [ + { title: "Express", value: "express" }, + { title: "FastAPI (Python)", value: "fastapi" }, + ]; + if (program.template === "streaming") { + // allow NextJS only for streaming template + choices.unshift({ title: "NextJS", value: "nextjs" }); + } + + const { framework } = await prompts( + { + type: "select", + name: "framework", + message: "Which framework would you like to use?", + choices, + initial: 0, + }, + handlers, + ); + program.framework = framework; + preferences.framework = framework; + } + } + + if (program.framework === "express" || program.framework === "fastapi") { + // if a backend-only framework is selected, ask whether we should create a frontend + if (!program.frontend) { + if (ciInfo.isCI) { + program.frontend = getPrefOrDefault("frontend"); + } else { + const styledNextJS = blue("NextJS"); + const styledBackend = green( + program.framework === "express" + ? "Express " + : program.framework === "fastapi" + ? "FastAPI (Python) " + : "", + ); + const { frontend } = await prompts({ + onState: onPromptState, + type: "toggle", + name: "frontend", + message: `Would you like to generate a ${styledNextJS} frontend for your ${styledBackend}backend?`, + initial: getPrefOrDefault("frontend"), + active: "Yes", + inactive: "No", + }); + program.frontend = Boolean(frontend); + preferences.frontend = Boolean(frontend); + } + } + } + + if (program.framework === "nextjs" || program.frontend) { + if (!program.ui) { + if (ciInfo.isCI) { + program.ui = getPrefOrDefault("ui"); + } else { + const { ui } = await prompts( + { + type: "select", + name: "ui", + message: "Which UI would you like to use?", + choices: [ + { title: "Just HTML", value: "html" }, + { title: "Shadcn", value: "shadcn" }, + ], + initial: 0, + }, + handlers, + ); + program.ui = ui; + preferences.ui = ui; + } + } + } + + if (program.framework === "nextjs") { + if (!program.model) { + if (ciInfo.isCI) { + program.model = getPrefOrDefault("model"); + } else { + const { model } = await prompts( + { + type: "select", + name: "model", + message: "Which model would you like to use?", + choices: [ + { title: "gpt-3.5-turbo", value: "gpt-3.5-turbo" }, + { title: "gpt-4", value: "gpt-4" }, + { title: "gpt-4-1106-preview", value: "gpt-4-1106-preview" }, + { + title: "gpt-4-vision-preview", + value: "gpt-4-vision-preview", + }, + ], + initial: 0, + }, + handlers, + ); + program.model = model; + preferences.model = model; + } + } + } + + if (program.framework === "express" || program.framework === "nextjs") { + if (!program.engine) { + if (ciInfo.isCI) { + program.engine = getPrefOrDefault("engine"); + } else { + const { engine } = await prompts( + { + type: "select", + name: "engine", + message: "Which chat engine would you like to use?", + choices: [ + { title: "ContextChatEngine", value: "context" }, + { + title: "SimpleChatEngine (no data, just chat)", + value: "simple", + }, + ], + initial: 0, + }, + handlers, + ); + program.engine = engine; + preferences.engine = engine; + } + } + } + + if (!program.openAIKey) { + const { key } = await prompts( + { + type: "text", + name: "key", + message: "Please provide your OpenAI API key (leave blank to skip):", + }, + handlers, + ); + program.openAIKey = key; + preferences.openAIKey = key; + } + + if ( + program.framework !== "fastapi" && + !process.argv.includes("--eslint") && + !process.argv.includes("--no-eslint") + ) { + if (ciInfo.isCI) { + program.eslint = getPrefOrDefault("eslint"); + } else { + const styledEslint = blue("ESLint"); + const { eslint } = await prompts({ + onState: onPromptState, + type: "toggle", + name: "eslint", + message: `Would you like to use ${styledEslint}?`, + initial: getPrefOrDefault("eslint"), + active: "Yes", + inactive: "No", + }); + program.eslint = Boolean(eslint); + preferences.eslint = Boolean(eslint); + } + } +};