Skip to content
Snippets Groups Projects
Commit 6f48831d authored by Marcus Schiesser's avatar Marcus Schiesser
Browse files

refactor: factor out questions and use strong-typing for question args

parent 58a852c0
No related branches found
No related tags found
No related merge requests found
#!/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,
......
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);
}
}
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment