Skip to content
Snippets Groups Projects
index.ts 9.77 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env node
    /* eslint-disable import/no-extraneous-dependencies */
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    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 prompts from "prompts";
    import checkForUpdate from "update-check";
    
    import { createApp } from "./create-app";
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    import { getPkgManager } from "./helpers/get-pkg-manager";
    import { isFolderEmpty } from "./helpers/is-folder-empty";
    
    import { isUrl } from "./helpers/is-url";
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    import { validateNpmName } from "./helpers/validate-pkg";
    import packageJson from "./package.json";
    
    let projectPath: string = "";
    
    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
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        process.stdout.write("\x1B[?25h");
        process.stdout.write("\n");
        process.exit(1);
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    };
    
    
    const program = new Commander.Command(packageJson.name)
      .version(packageJson.version)
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      .arguments("<project-directory>")
      .usage(`${green("<project-directory>")} [options]`)
    
      .action((name) => {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        projectPath = name;
    
      })
      .option(
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        "--eslint",
    
        `
    
      Initialize with eslint config.
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    `,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        "--use-npm",
    
        `
    
      Explicitly tell the CLI to bootstrap the application using npm
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    `,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        "--use-pnpm",
    
        `
    
      Explicitly tell the CLI to bootstrap the application using pnpm
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    `,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        "--use-yarn",
    
        `
    
      Explicitly tell the CLI to bootstrap the application using Yarn
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    `,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        "--reset-preferences",
    
        `
    
      Explicitly tell the CLI to reset any stored preferences
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    `,
    
      )
      .allowUnknownOption()
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      .parse(process.argv);
    
    
    const packageManager = !!program.useNpm
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      ? "npm"
    
      : !!program.usePnpm
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      ? "pnpm"
    
      : !!program.useYarn
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      ? "yarn"
      : getPkgManager();
    
    
    async function run(): Promise<void> {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      const conf = new Conf({ projectName: "create-llama" });
    
    
      if (program.resetPreferences) {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        conf.clear();
        console.log(`Preferences reset successfully`);
        return;
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      if (typeof projectPath === "string") {
        projectPath = projectPath.trim();
    
      }
    
      if (!projectPath) {
        const res = await prompts({
          onState: onPromptState,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
          type: "text",
          name: "path",
          message: "What is your project named?",
          initial: "my-app",
    
          validate: (name) => {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            const validation = validateNpmName(path.basename(path.resolve(name)));
    
            if (validation.valid) {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
              return true;
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            return "Invalid project name: " + validation.problems![0];
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        });
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        if (typeof res.path === "string") {
          projectPath = res.path.trim();
    
        }
      }
    
      if (!projectPath) {
        console.log(
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
          "\nPlease specify the project directory:\n" +
            `  ${cyan(program.name())} ${green("<project-directory>")}\n` +
            "For example:\n" +
            `  ${cyan(program.name())} ${green("my-next-app")}\n\n` +
            `Run ${cyan(`${program.name()} --help`)} to see all options.`,
        );
        process.exit(1);
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      const resolvedProjectPath = path.resolve(projectPath);
      const projectName = path.basename(resolvedProjectPath);
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      const { valid, problems } = validateNpmName(projectName);
    
      if (!valid) {
        console.error(
          `Could not create a project called ${red(
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            `"${projectName}"`,
          )} because of npm naming restrictions:`,
        );
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        problems!.forEach((p) => console.error(`    ${red(bold("*"))} ${p}`));
        process.exit(1);
    
      }
    
      /**
       * Verify the project dir is empty or doesn't exist
       */
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      const root = path.resolve(resolvedProjectPath);
      const appName = path.basename(root);
      const folderExists = fs.existsSync(root);
    
    
      if (folderExists && !isFolderEmpty(root, appName)) {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        process.exit(1);
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      const preferences = (conf.get("preferences") || {}) as Record<
    
        string,
        boolean | string
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      >;
    
    
      const defaults: typeof preferences = {
    
        eslint: true,
    
        customApiPath: "http://localhost:8000/api/chat",
    
      };
      const getPrefOrDefault = (field: string) =>
        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" },
    
          );
          program.template = template;
          preferences.template = template;
        }
      }
    
    
      if (!program.framework) {
        if (ciInfo.isCI) {
          program.framework = getPrefOrDefault("framework");
        } else {
          const { framework } = await prompts(
            {
              type: "select",
              name: "framework",
              message: "Which framework would you like to use?",
              choices: [
                { title: "NextJS", value: "nextjs" },
                { title: "Express", value: "express" },
    
                { title: "FastAPI (Python)", value: "fastapi" },
    
          );
          program.framework = framework;
          preferences.framework = framework;
        }
      }
    
    
      if (program.framework === "nextjs") {
        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,
              },
    
      if (program.framework === "express" || program.framework === "nextjs") {
        if (!program.engine) {
          if (ciInfo.isCI) {
            program.engine = getPrefOrDefault("engine");
          } else {
    
            const external =
              program.framework === "nextjs"
                ? [
                    {
                      title: "External chat engine (e.g. FastAPI)",
                      value: "external",
                    },
                  ]
                : [];
    
            const { engine } = await prompts(
              {
                type: "select",
                name: "engine",
                message: "Which chat engine would you like to use?",
                choices: [
                  { title: "SimpleChatEngine", value: "simple" },
                  { title: "ContextChatEngine", value: "context" },
    
            );
            program.engine = engine;
            preferences.engine = engine;
          }
    
        if (
          program.framework === "nextjs" &&
          program.engine === "external" &&
          !program.customApiPath
        ) {
          if (ciInfo.isCI) {
            program.customApiPath = getPrefOrDefault("customApiPath");
          } else {
            const { customApiPath } = await prompts(
              {
                type: "text",
                name: "customApiPath",
                message:
                  "URL path of your external chat engine (used for development)?",
                validate: (url) => (isUrl(url) ? true : "Please enter a valid URL"),
                initial: getPrefOrDefault("customApiPath"),
              },
              handlers,
            );
            program.customApiPath = customApiPath;
            preferences.customApiPath = customApiPath;
          }
        }
    
      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);
    
      await createApp({
    
        template: program.template,
    
        appPath: resolvedProjectPath,
        packageManager,
        eslint: program.eslint,
    
        customApiPath: program.customApiPath,
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
      conf.set("preferences", preferences);
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
    const update = checkForUpdate(packageJson).catch(() => null);
    
    
    async function notifyUpdate(): Promise<void> {
      try {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        const res = await update;
    
        if (res?.latest) {
          const updateMessage =
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            packageManager === "yarn"
              ? "yarn global add create-llama"
              : packageManager === "pnpm"
              ? "pnpm add -g create-llama"
              : "npm i -g create-llama";
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            yellow(bold("A new version of `create-llama` is available!")) +
              "\n" +
              "You can update by running: " +
    
              cyan(updateMessage) +
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
              "\n",
          );
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        process.exit();
    
      } catch {
        // ignore error
      }
    }
    
    run()
      .then(notifyUpdate)
      .catch(async (reason) => {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        console.log();
        console.log("Aborting installation.");
    
        if (reason.command) {
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
          console.log(`  ${cyan(reason.command)} has failed.`);
    
        } else {
          console.log(
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
            red("Unexpected error. Please report it as a bug:") + "\n",
            reason,
          );
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        console.log();
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        await notifyUpdate();
    
    Marcus Schiesser's avatar
    Marcus Schiesser committed
        process.exit(1);
      });