From fdf48dd459591d5c6bf98d2fbd15f27ea83db549 Mon Sep 17 00:00:00 2001
From: "Huu Le (Lee)" <39040748+leehuwuj@users.noreply.github.com>
Date: Thu, 7 Mar 2024 17:19:08 +0700
Subject: [PATCH] feat: Add start in VSCode option and support python for dev
 container (#619)

---
 .changeset/few-chicken-exercise.md            |  5 ++
 .changeset/plenty-onions-relate.md            |  5 ++
 packages/create-llama/create-app.ts           |  3 +
 packages/create-llama/helpers/devcontainer.ts | 61 +++++++++++++++++++
 packages/create-llama/helpers/llama-pack.ts   |  2 +-
 packages/create-llama/helpers/python.ts       |  2 +-
 packages/create-llama/helpers/types.ts        |  6 +-
 packages/create-llama/helpers/typescript.ts   |  2 +-
 packages/create-llama/index.ts                | 28 ++++++++-
 packages/create-llama/questions.ts            |  4 ++
 .../create-llama/templates/devcontainer.json  | 35 +++++++++++
 11 files changed, 148 insertions(+), 5 deletions(-)
 create mode 100644 .changeset/few-chicken-exercise.md
 create mode 100644 .changeset/plenty-onions-relate.md
 create mode 100644 packages/create-llama/helpers/devcontainer.ts
 create mode 100644 packages/create-llama/templates/devcontainer.json

diff --git a/.changeset/few-chicken-exercise.md b/.changeset/few-chicken-exercise.md
new file mode 100644
index 000000000..9ddded96a
--- /dev/null
+++ b/.changeset/few-chicken-exercise.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Add "Start in VSCode" option to postInstallAction
diff --git a/.changeset/plenty-onions-relate.md b/.changeset/plenty-onions-relate.md
new file mode 100644
index 000000000..8b940e8a2
--- /dev/null
+++ b/.changeset/plenty-onions-relate.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Add devcontainers to generated code
diff --git a/packages/create-llama/create-app.ts b/packages/create-llama/create-app.ts
index 1b5ed303d..2f7db13a7 100644
--- a/packages/create-llama/create-app.ts
+++ b/packages/create-llama/create-app.ts
@@ -11,6 +11,7 @@ import fs from "fs";
 import terminalLink from "terminal-link";
 import type { InstallTemplateArgs } from "./helpers";
 import { installTemplate } from "./helpers";
+import { writeDevcontainer } from "./helpers/devcontainer";
 import { templatesDir } from "./helpers/dir";
 import { toolsRequireConfig } from "./helpers/tools";
 
@@ -121,6 +122,8 @@ export async function createApp({
     console.log();
   }
 
+  await writeDevcontainer(root, templatesDir, framework, frontend);
+
   if (toolsRequireConfig(tools)) {
     console.log(
       yellow(
diff --git a/packages/create-llama/helpers/devcontainer.ts b/packages/create-llama/helpers/devcontainer.ts
new file mode 100644
index 000000000..cb008b977
--- /dev/null
+++ b/packages/create-llama/helpers/devcontainer.ts
@@ -0,0 +1,61 @@
+import fs from "fs";
+import path from "path";
+import { TemplateFramework } from "./types";
+
+function renderDevcontainerContent(
+  templatesDir: string,
+  framework: TemplateFramework,
+  frontend: boolean,
+) {
+  const devcontainerJson: any = JSON.parse(
+    fs.readFileSync(path.join(templatesDir, "devcontainer.json"), "utf8"),
+  );
+
+  // Modify postCreateCommand
+  if (frontend) {
+    devcontainerJson.postCreateCommand =
+      framework === "fastapi"
+        ? "cd backend && poetry install && cd ../frontend && npm install"
+        : "cd backend && npm install && cd ../frontend && npm install";
+  } else {
+    devcontainerJson.postCreateCommand =
+      framework === "fastapi" ? "poetry install" : "npm install";
+  }
+
+  // Modify containerEnv
+  if (framework === "fastapi") {
+    if (frontend) {
+      devcontainerJson.containerEnv = {
+        ...devcontainerJson.containerEnv,
+        PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}/backend",
+      };
+    } else {
+      devcontainerJson.containerEnv = {
+        ...devcontainerJson.containerEnv,
+        PYTHONPATH: "${PYTHONPATH}:${workspaceFolder}",
+      };
+    }
+  }
+
+  return JSON.stringify(devcontainerJson, null, 2);
+}
+
+export const writeDevcontainer = async (
+  root: string,
+  templatesDir: string,
+  framework: TemplateFramework,
+  frontend: boolean,
+) => {
+  console.log("Adding .devcontainer");
+  const devcontainerContent = renderDevcontainerContent(
+    templatesDir,
+    framework,
+    frontend,
+  );
+  const devcontainerDir = path.join(root, ".devcontainer");
+  fs.mkdirSync(devcontainerDir);
+  await fs.promises.writeFile(
+    path.join(devcontainerDir, "devcontainer.json"),
+    devcontainerContent,
+  );
+};
diff --git a/packages/create-llama/helpers/llama-pack.ts b/packages/create-llama/helpers/llama-pack.ts
index 16cd801fa..887201d9f 100644
--- a/packages/create-llama/helpers/llama-pack.ts
+++ b/packages/create-llama/helpers/llama-pack.ts
@@ -142,7 +142,7 @@ export const installLlamapackProject = async ({
   await copyLlamapackEmptyProject({ root });
   await copyData({ root });
   await installLlamapackExample({ root, llamapack });
-  if (postInstallAction !== "none") {
+  if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
     installPythonDependencies({ noRoot: true });
   }
 };
diff --git a/packages/create-llama/helpers/python.ts b/packages/create-llama/helpers/python.ts
index 3ebde783b..285beb9a8 100644
--- a/packages/create-llama/helpers/python.ts
+++ b/packages/create-llama/helpers/python.ts
@@ -266,7 +266,7 @@ export const installPythonTemplate = async ({
   );
   await addDependencies(root, addOnDependencies);
 
-  if (postInstallAction !== "none") {
+  if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
     installPythonDependencies();
   }
 };
diff --git a/packages/create-llama/helpers/types.ts b/packages/create-llama/helpers/types.ts
index 52ef94b91..742277373 100644
--- a/packages/create-llama/helpers/types.ts
+++ b/packages/create-llama/helpers/types.ts
@@ -6,7 +6,11 @@ export type TemplateFramework = "nextjs" | "express" | "fastapi";
 export type TemplateEngine = "simple" | "context";
 export type TemplateUI = "html" | "shadcn";
 export type TemplateVectorDB = "none" | "mongo" | "pg" | "pinecone";
-export type TemplatePostInstallAction = "none" | "dependencies" | "runApp";
+export type TemplatePostInstallAction =
+  | "none"
+  | "VSCode"
+  | "dependencies"
+  | "runApp";
 export type TemplateDataSource = {
   type: TemplateDataSourceType;
   config: TemplateDataSourceConfig;
diff --git a/packages/create-llama/helpers/typescript.ts b/packages/create-llama/helpers/typescript.ts
index cfadc67b1..bab6812f7 100644
--- a/packages/create-llama/helpers/typescript.ts
+++ b/packages/create-llama/helpers/typescript.ts
@@ -228,7 +228,7 @@ export const installTSTemplate = async ({
     JSON.stringify(packageJson, null, 2) + os.EOL,
   );
 
-  if (postInstallAction !== "none") {
+  if (postInstallAction === "runApp" || postInstallAction === "dependencies") {
     await installTSDependencies(packageJson, packageManager, isOnline);
   }
 };
diff --git a/packages/create-llama/index.ts b/packages/create-llama/index.ts
index 804eae6cc..47e78c26a 100644
--- a/packages/create-llama/index.ts
+++ b/packages/create-llama/index.ts
@@ -1,11 +1,13 @@
 #!/usr/bin/env node
 /* eslint-disable import/no-extraneous-dependencies */
+import { execSync } from "child_process";
 import Commander from "commander";
 import Conf from "conf";
 import fs from "fs";
 import path from "path";
 import { bold, cyan, green, red, yellow } from "picocolors";
 import prompts from "prompts";
+import terminalLink from "terminal-link";
 import checkForUpdate from "update-check";
 import { createApp } from "./create-app";
 import { getPkgManager } from "./helpers/get-pkg-manager";
@@ -298,7 +300,31 @@ async function run(): Promise<void> {
   });
   conf.set("preferences", preferences);
 
-  if (program.postInstallAction === "runApp") {
+  if (program.postInstallAction === "VSCode") {
+    console.log(`Starting VSCode in ${root}...`);
+    try {
+      execSync(`code . --new-window --goto README.md`, {
+        stdio: "inherit",
+        cwd: root,
+      });
+    } catch (error) {
+      console.log(
+        red(
+          `Failed to start VSCode in ${root}. 
+Got error: ${(error as Error).message}.\n`,
+        ),
+      );
+      console.log(
+        `Make sure you have VSCode installed and added to your PATH. 
+Please check ${cyan(
+          terminalLink(
+            "This documentation",
+            `https://code.visualstudio.com/docs/setup/setup-overview`,
+          ),
+        )} for more information.`,
+      );
+    }
+  } else if (program.postInstallAction === "runApp") {
     console.log(`Running app in ${root}...`);
     await runApp(
       root,
diff --git a/packages/create-llama/questions.ts b/packages/create-llama/questions.ts
index 6835bb363..af5f17d69 100644
--- a/packages/create-llama/questions.ts
+++ b/packages/create-llama/questions.ts
@@ -215,6 +215,10 @@ export const askQuestions = async (
             title: "Just generate code (~1 sec)",
             value: "none",
           },
+          {
+            title: "Start in VSCode (~1 sec)",
+            value: "VSCode",
+          },
           {
             title: "Generate code and install dependencies (~2 min)",
             value: "dependencies",
diff --git a/packages/create-llama/templates/devcontainer.json b/packages/create-llama/templates/devcontainer.json
new file mode 100644
index 000000000..f87545ffb
--- /dev/null
+++ b/packages/create-llama/templates/devcontainer.json
@@ -0,0 +1,35 @@
+{
+  "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:dev-20-bullseye",
+  "features": {
+    "ghcr.io/devcontainers-contrib/features/turborepo-npm:1": {},
+    "ghcr.io/devcontainers-contrib/features/typescript:2": {},
+    "ghcr.io/devcontainers/features/python:1": {
+      "version": "3.11",
+      "toolsToInstall": ["flake8", "black", "mypy", "poetry"]
+    }
+  },
+  "customizations": {
+    "codespaces": {
+      "openFiles": ["README.md"]
+    },
+    "vscode": {
+      "extensions": [
+        "ms-vscode.typescript-language-features",
+        "esbenp.prettier-vscode",
+        "ms-python.python",
+        "ms-python.black-formatter",
+        "ms-python.vscode-flake8",
+        "ms-python.vscode-pylance"
+      ],
+      "settings": {
+        "python.formatting.provider": "black",
+        "python.languageServer": "Pylance",
+        "python.analysis.typeCheckingMode": "basic"
+      }
+    }
+  },
+  "containerEnv": {
+    "POETRY_VIRTUALENVS_CREATE": "false"
+  },
+  "forwardPorts": [3000, 8000]
+}
-- 
GitLab