From af2a1687c9bbcc581f5a0677ca5bf5f7d7058657 Mon Sep 17 00:00:00 2001 From: thucpn <thucsh2@gmail.com> Date: Mon, 4 Dec 2023 13:51:53 +0700 Subject: [PATCH] feat: options to download community projects --- create-app.ts | 2 + helpers/constant.ts | 2 + helpers/repo.ts | 63 +++++++++ index.ts | 334 +++++++++++++++++++++++++------------------- templates/index.ts | 21 +++ templates/types.ts | 1 + 6 files changed, 277 insertions(+), 146 deletions(-) create mode 100644 helpers/constant.ts create mode 100644 helpers/repo.ts diff --git a/create-app.ts b/create-app.ts index 05186ed4..1e13f5ca 100644 --- a/create-app.ts +++ b/create-app.ts @@ -31,6 +31,7 @@ export async function createApp({ frontend, openAIKey, model, + communityProjectPath, }: InstallAppArgs): Promise<void> { const root = path.resolve(appPath); @@ -69,6 +70,7 @@ export async function createApp({ eslint, openAIKey, model, + communityProjectPath, }; if (frontend) { diff --git a/helpers/constant.ts b/helpers/constant.ts new file mode 100644 index 00000000..341fba2c --- /dev/null +++ b/helpers/constant.ts @@ -0,0 +1,2 @@ +export const COMMUNITY_OWNER = "run-llama"; +export const COMMUNITY_REPO = "create_llama_projects"; diff --git a/helpers/repo.ts b/helpers/repo.ts new file mode 100644 index 00000000..2471a91a --- /dev/null +++ b/helpers/repo.ts @@ -0,0 +1,63 @@ +import { createWriteStream, promises } from "fs"; +import got from "got"; +import { tmpdir } from "os"; +import { join } from "path"; +import { Stream } from "stream"; +import tar from "tar"; +import { promisify } from "util"; +import { makeDir } from "./make-dir"; + +export type RepoInfo = { + username: string; + name: string; + branch: string; + filePath: string; +}; + +const pipeline = promisify(Stream.pipeline); + +async function downloadTar(url: string) { + const tempFile = join(tmpdir(), `next.js-cna-example.temp-${Date.now()}`); + await pipeline(got.stream(url), createWriteStream(tempFile)); + return tempFile; +} + +export async function downloadAndExtractRepo( + root: string, + { username, name, branch, filePath }: RepoInfo, +) { + await makeDir(root); + + const tempFile = await downloadTar( + `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`, + ); + + await tar.x({ + file: tempFile, + cwd: root, + strip: filePath ? filePath.split("/").length + 1 : 1, + filter: (p) => + p.startsWith( + `${name}-${branch.replace(/\//g, "-")}${ + filePath ? `/${filePath}/` : "/" + }`, + ), + }); + + await promises.unlink(tempFile); +} + +export async function getRepoRootFolders( + owner: string, + repo: string, +): Promise<string[]> { + const url = `https://api.github.com/repos/${owner}/${repo}/contents`; + + const response = await got(url, { + responseType: "json", + }); + + const data = response.body as any[]; + const folders = data.filter((item) => item.type === "dir"); + return folders.map((item) => item.name); +} diff --git a/index.ts b/index.ts index 67a0f53c..3d5ff919 100644 --- a/index.ts +++ b/index.ts @@ -9,8 +9,10 @@ import { blue, 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 { 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"; @@ -169,6 +171,7 @@ async function run(): Promise<void> { frontend: false, openAIKey: "", model: "gpt-3.5-turbo", + communityProjectPath: "", }; const getPrefOrDefault = (field: keyof Args) => preferences[field] ?? defaults[field]; @@ -180,32 +183,6 @@ async function run(): Promise<void> { }, }; - 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 === "nextjs") { - program.template = "streaming"; - } if (!program.template) { if (ciInfo.isCI) { program.template = getPrefOrDefault("template"); @@ -218,6 +195,7 @@ async function run(): Promise<void> { choices: [ { title: "Chat without streaming", value: "simple" }, { title: "Chat with streaming", value: "streaming" }, + { title: "Community templates", value: "community" }, ], initial: 1, }, @@ -228,145 +206,208 @@ async function run(): Promise<void> { } } - 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.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); + }, + }, + ); - if (program.framework === "nextjs" || program.frontend) { - if (!program.ui) { + program.communityProjectPath = communityProjectPath; + preferences.communityProjectPath = communityProjectPath; + } else { + if (!program.framework) { if (ciInfo.isCI) { - program.ui = getPrefOrDefault("ui"); + program.framework = getPrefOrDefault("framework"); } else { - const { ui } = await prompts( + 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: "ui", - message: "Which UI would you like to use?", - choices: [ - { title: "Just HTML", value: "html" }, - { title: "Shadcn", value: "shadcn" }, - ], + name: "framework", + message: "Which framework would you like to use?", + choices, initial: 0, }, handlers, ); - program.ui = ui; - preferences.ui = ui; + program.framework = framework; + preferences.framework = framework; } } - } - 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 === "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 === "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.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.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 === "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 !== "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); + 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); + } } } @@ -381,6 +422,7 @@ async function run(): Promise<void> { frontend: program.frontend, openAIKey: program.openAIKey, model: program.model, + communityProjectPath: program.communityProjectPath, }); conf.set("preferences", preferences); } diff --git a/templates/index.ts b/templates/index.ts index fd5377d8..40102304 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -7,7 +7,9 @@ import path from "path"; import { bold, cyan } from "picocolors"; import { version } from "../../core/package.json"; +import { COMMUNITY_OWNER, COMMUNITY_REPO } from "../helpers/constant"; import { PackageManager } from "../helpers/get-pkg-manager"; +import { downloadAndExtractRepo } from "../helpers/repo"; import { InstallTemplateArgs, TemplateEngine, @@ -306,10 +308,29 @@ const installPythonTemplate = async ({ ); }; +const installCommunityProject = async ({ + root, + communityProjectPath, +}: Pick<InstallTemplateArgs, "root" | "communityProjectPath">) => { + console.log("\nInstalling community project:", communityProjectPath!); + await downloadAndExtractRepo(root, { + username: COMMUNITY_OWNER, + name: COMMUNITY_REPO, + branch: "main", + filePath: communityProjectPath!, + }); +}; + export const installTemplate = async ( props: InstallTemplateArgs & { backend: boolean }, ) => { process.chdir(props.root); + + if (props.communityProjectPath) { + await installCommunityProject(props); + return; + } + if (props.framework === "fastapi") { await installPythonTemplate(props); } else { diff --git a/templates/types.ts b/templates/types.ts index f6af4de0..bcdd37cd 100644 --- a/templates/types.ts +++ b/templates/types.ts @@ -19,4 +19,5 @@ export interface InstallTemplateArgs { openAIKey?: string; forBackend?: string; model: string; + communityProjectPath?: string; } -- GitLab