mirror of
https://github.com/run-llama/create-llama.git
synced 2026-07-01 21:04:08 -04:00
2c20d30325
add license make contextchatengine the default change git commit message
403 lines
10 KiB
JavaScript
403 lines
10 KiB
JavaScript
#!/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 prompts from "prompts";
|
|
import checkForUpdate from "update-check";
|
|
import { createApp } from "./create-app";
|
|
import { getPkgManager } from "./helpers/get-pkg-manager";
|
|
import { isFolderEmpty } from "./helpers/is-folder-empty";
|
|
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
|
|
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>")
|
|
.usage(`${green("<project-directory>")} [options]`)
|
|
.action((name) => {
|
|
projectPath = name;
|
|
})
|
|
.option(
|
|
"--eslint",
|
|
`
|
|
|
|
Initialize with eslint config.
|
|
`,
|
|
)
|
|
.option(
|
|
"--use-npm",
|
|
`
|
|
|
|
Explicitly tell the CLI to bootstrap the application using npm
|
|
`,
|
|
)
|
|
.option(
|
|
"--use-pnpm",
|
|
`
|
|
|
|
Explicitly tell the CLI to bootstrap the application using pnpm
|
|
`,
|
|
)
|
|
.option(
|
|
"--use-yarn",
|
|
`
|
|
|
|
Explicitly tell the CLI to bootstrap the application using Yarn
|
|
`,
|
|
)
|
|
.option(
|
|
"--reset-preferences",
|
|
`
|
|
|
|
Explicitly tell the CLI to reset any stored preferences
|
|
`,
|
|
)
|
|
.allowUnknownOption()
|
|
.parse(process.argv);
|
|
|
|
const packageManager = !!program.useNpm
|
|
? "npm"
|
|
: !!program.usePnpm
|
|
? "pnpm"
|
|
: !!program.useYarn
|
|
? "yarn"
|
|
: getPkgManager();
|
|
|
|
async function run(): Promise<void> {
|
|
const conf = new Conf({ projectName: "create-llama" });
|
|
|
|
if (program.resetPreferences) {
|
|
conf.clear();
|
|
console.log(`Preferences reset successfully`);
|
|
return;
|
|
}
|
|
|
|
if (typeof projectPath === "string") {
|
|
projectPath = projectPath.trim();
|
|
}
|
|
|
|
if (!projectPath) {
|
|
const res = await prompts({
|
|
onState: onPromptState,
|
|
type: "text",
|
|
name: "path",
|
|
message: "What is your project named?",
|
|
initial: "my-app",
|
|
validate: (name) => {
|
|
const validation = validateNpmName(path.basename(path.resolve(name)));
|
|
if (validation.valid) {
|
|
return true;
|
|
}
|
|
return "Invalid project name: " + validation.problems![0];
|
|
},
|
|
});
|
|
|
|
if (typeof res.path === "string") {
|
|
projectPath = res.path.trim();
|
|
}
|
|
}
|
|
|
|
if (!projectPath) {
|
|
console.log(
|
|
"\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);
|
|
}
|
|
|
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
const projectName = path.basename(resolvedProjectPath);
|
|
|
|
const { valid, problems } = validateNpmName(projectName);
|
|
if (!valid) {
|
|
console.error(
|
|
`Could not create a project called ${red(
|
|
`"${projectName}"`,
|
|
)} because of npm naming restrictions:`,
|
|
);
|
|
|
|
problems!.forEach((p) => console.error(` ${red(bold("*"))} ${p}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Verify the project dir is empty or doesn't exist
|
|
*/
|
|
const root = path.resolve(resolvedProjectPath);
|
|
const appName = path.basename(root);
|
|
const folderExists = fs.existsSync(root);
|
|
|
|
if (folderExists && !isFolderEmpty(root, appName)) {
|
|
process.exit(1);
|
|
}
|
|
|
|
const preferences = (conf.get("preferences") || {}) as Record<
|
|
string,
|
|
boolean | string
|
|
>;
|
|
|
|
const defaults: typeof preferences = {
|
|
template: "simple",
|
|
framework: "nextjs",
|
|
engine: "simple",
|
|
ui: "html",
|
|
eslint: true,
|
|
frontend: false,
|
|
openAIKey: "",
|
|
};
|
|
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" },
|
|
],
|
|
initial: 1,
|
|
},
|
|
handlers,
|
|
);
|
|
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" },
|
|
],
|
|
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 === "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",
|
|
value: "simple (no data, just chat)",
|
|
},
|
|
],
|
|
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);
|
|
}
|
|
}
|
|
|
|
await createApp({
|
|
template: program.template,
|
|
framework: program.framework,
|
|
engine: program.engine,
|
|
ui: program.ui,
|
|
appPath: resolvedProjectPath,
|
|
packageManager,
|
|
eslint: program.eslint,
|
|
frontend: program.frontend,
|
|
openAIKey: program.openAIKey,
|
|
});
|
|
conf.set("preferences", preferences);
|
|
}
|
|
|
|
const update = checkForUpdate(packageJson).catch(() => null);
|
|
|
|
async function notifyUpdate(): Promise<void> {
|
|
try {
|
|
const res = await update;
|
|
if (res?.latest) {
|
|
const updateMessage =
|
|
packageManager === "yarn"
|
|
? "yarn global add create-llama@latest"
|
|
: packageManager === "pnpm"
|
|
? "pnpm add -g create-llama@latest"
|
|
: "npm i -g create-llama@latest";
|
|
|
|
console.log(
|
|
yellow(bold("A new version of `create-llama` is available!")) +
|
|
"\n" +
|
|
"You can update by running: " +
|
|
cyan(updateMessage) +
|
|
"\n",
|
|
);
|
|
}
|
|
process.exit();
|
|
} catch {
|
|
// ignore error
|
|
}
|
|
}
|
|
|
|
run()
|
|
.then(notifyUpdate)
|
|
.catch(async (reason) => {
|
|
console.log();
|
|
console.log("Aborting installation.");
|
|
if (reason.command) {
|
|
console.log(` ${cyan(reason.command)} has failed.`);
|
|
} else {
|
|
console.log(
|
|
red("Unexpected error. Please report it as a bug:") + "\n",
|
|
reason,
|
|
);
|
|
}
|
|
console.log();
|
|
|
|
await notifyUpdate();
|
|
|
|
process.exit(1);
|
|
});
|