feat: Allow for passing flags instead of interactive prompts

This commit is contained in:
bracesproul
2025-03-20 12:00:48 -07:00
parent ee7e714c9f
commit 4dd1b9d7a4
6 changed files with 297 additions and 88 deletions
+42 -1
View File
@@ -8,7 +8,48 @@ This will clone a frontend chat application (Next.js or Vite), along with up to
## Usage
Clone code:
### Quickstart
The quickest way to get started is to pass flags to the CLI, instead of going through the prompts:
```bash
# Pass `-y` to accept all default values
npx create-agent-chat-app@latest -y
```
You can also pass individual flags. Here are all of the options the CLI accepts, and their default values:
```bash
npx create-agent-chat-app@latest --help
```
```
Usage: create-agent-chat-app [options]
Create an agent chat app with one command
Options:
-V, --version output the version number
-y, --yes Skip all prompts and use default values
--project-name <name> Name of the project (default: "agent-chat-app")
--package-manager <manager> Package manager to use (npm, pnpm, yarn) (default: "yarn")
--install-deps <boolean> Automatically install dependencies (default: "true")
--framework <framework> Framework to use (nextjs, vite) (default: "nextjs")
--include-agent <agent...> Pre-built agents to include (react, memory, research, retrieval)
-h, --help display help for command
```
If you want to pass some flags, and use the defaults for the rest, simply add `-y`/`--yes`, in addition to the flags you want to pass:
```bash
npx create-agent-chat-app@latest -y --package-manager pnpm
```
This will accept all default values, except for the package manager, which will be set to `pnpm`.
### Interactive
If you prefer to go through the prompts, you can run the following:
```bash
# Using npx
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-agent-chat-app",
"version": "0.1.2",
"version": "0.1.3-rc.2",
"description": "Create a LangGraph chat app with one command",
"repository": {
"type": "git",
@@ -35,6 +35,7 @@
"dependencies": {
"@clack/prompts": "^0.10.0",
"chalk": "^5.3.0",
"commander": "^13.1.0",
"fs-extra": "^11.2.0"
},
"devDependencies": {
+243 -83
View File
@@ -5,6 +5,7 @@ import fs from "fs-extra";
import chalk, { ChalkInstance } from "chalk";
import { fileURLToPath } from "url";
import { execSync } from "child_process";
import { Command } from "commander";
import {
BASE_GITIGNORE,
NEXTJS_GITIGNORE,
@@ -24,6 +25,8 @@ import {
const __filename: string = fileURLToPath(import.meta.url);
const __dirname: string = path.dirname(__filename);
const VERSION = "0.1.3";
type PackageManager = "npm" | "pnpm" | "yarn";
type Framework = "nextjs" | "vite";
@@ -129,6 +132,9 @@ async function setPackageJsonFields(
pkgJson[overridesPkgManagerMap[packageManager]] = {
"@langchain/core": "^0.3.42",
};
if (packageManager === "npm") {
delete pkgJson["resolutions"];
}
await fs.promises.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
} catch (_) {
console.log(
@@ -281,9 +287,13 @@ const AGENT_DEPENDENCIES_MAP = {
*/
async function setAgentPackageJsonFields(
baseDir: string,
args: IncludeAgents,
chalk: ChalkInstance,
inputs: {
agentArgs: IncludeAgents;
packageManager: PackageManager;
chalk: ChalkInstance;
},
): Promise<void> {
const { agentArgs, packageManager, chalk } = inputs;
try {
const agentsPkgJsonPath = path.join(
baseDir,
@@ -295,16 +305,16 @@ async function setAgentPackageJsonFields(
await fs.promises.readFile(agentsPkgJsonPath, "utf8"),
);
const requiredPackages: Record<string, string> = {};
if (args.includeReactAgent) {
if (agentArgs.includeReactAgent) {
Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["react-agent"]);
}
if (args.includeMemoryAgent) {
if (agentArgs.includeMemoryAgent) {
Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["memory-agent"]);
}
if (args.includeResearchAgent) {
if (agentArgs.includeResearchAgent) {
Object.assign(requiredPackages, AGENT_DEPENDENCIES_MAP["research-agent"]);
}
if (args.includeRetrievalAgent) {
if (agentArgs.includeRetrievalAgent) {
Object.assign(
requiredPackages,
AGENT_DEPENDENCIES_MAP["retrieval-agent"],
@@ -314,6 +324,11 @@ async function setAgentPackageJsonFields(
...pkgJson.dependencies,
...requiredPackages,
};
// Update the scripts to call the correct package manager
pkgJson.scripts["build:internal"] = pkgJson.scripts[
"build:internal"
].replace("{PACKAGE_MANAGER}", packageManager);
await fs.promises.writeFile(
agentsPkgJsonPath,
JSON.stringify(pkgJson, null, 2),
@@ -484,86 +499,211 @@ async function addPnpmDirectDependencyWorkaround(
}
}
async function promptUser(): Promise<ProjectAnswers> {
/**
* Parse command-line arguments and return project configuration.
* If all required arguments are provided, this will bypass the interactive prompts.
* If only some arguments are provided, the user will be prompted for the remaining ones.
*/
async function parseCommandLineArgs(): Promise<Partial<ProjectAnswers>> {
const program = new Command();
program
.name("create-agent-chat-app")
.description("Create an agent chat app with one command")
.version(VERSION)
.option("-y, --yes", "Skip all prompts and use default values")
.option("--project-name <name>", "Name of the project", "agent-chat-app")
.option(
"--package-manager <manager>",
"Package manager to use (npm, pnpm, yarn)",
"yarn",
)
.option(
"--install-deps <boolean>",
"Automatically install dependencies",
"true",
)
.option(
"--framework <framework>",
"Framework to use (nextjs, vite)",
"nextjs",
)
.option(
"--include-agent <agent...>",
"Pre-built agents to include (react, memory, research, retrieval)",
)
.allowUnknownOption();
program.parse();
const options = program.opts();
const result: Partial<ProjectAnswers> = {};
// If -y or --yes flag is provided, use all defaults
if (options.yes) {
return {
projectName: options.projectName ?? "agent-chat-app",
packageManager: options.packageManager ?? "yarn",
autoInstallDeps: options.autoInstallDeps ?? true,
framework: options.framework ?? "nextjs",
includeReactAgent: options.includeAgent?.includes("react") ?? true,
includeMemoryAgent: options.includeAgent?.includes("memory") ?? true,
includeResearchAgent: options.includeAgent?.includes("research") ?? true,
includeRetrievalAgent:
options.includeAgent?.includes("retrieval") ?? true,
} as ProjectAnswers;
}
if (options.projectName) {
result.projectName = options.projectName;
}
if (
options.packageManager &&
["npm", "pnpm", "yarn"].includes(options.packageManager)
) {
result.packageManager = options.packageManager as PackageManager;
}
if (options.installDeps !== undefined) {
result.autoInstallDeps = options.installDeps.toLowerCase() === "true";
}
if (options.framework && ["nextjs", "vite"].includes(options.framework)) {
result.framework = options.framework as Framework;
}
if (options.includeAgent) {
const selectedAgents = Array.isArray(options.includeAgent)
? options.includeAgent
: [options.includeAgent];
result.includeReactAgent = selectedAgents.includes("react");
result.includeMemoryAgent = selectedAgents.includes("memory");
result.includeResearchAgent = selectedAgents.includes("research");
result.includeRetrievalAgent = selectedAgents.includes("retrieval");
}
return result;
}
/**
* Prompt the user for any missing configuration options.
* If a value is already provided in partialAnswers, the user won't be prompted for it.
*/
async function promptUser(
partialAnswers: Partial<ProjectAnswers> = {},
): Promise<ProjectAnswers> {
intro(chalk.green(" create-agent-chat-app "));
const projectNameResponse = await text({
message: "What is the name of your project?",
placeholder: "agent-chat-app",
defaultValue: "agent-chat-app",
});
// Project name prompt
let projectName = partialAnswers.projectName;
if (!projectName) {
const projectNameResponse = await text({
message: "What is the name of your project?",
placeholder: "agent-chat-app",
defaultValue: "agent-chat-app",
});
if (isCancel(projectNameResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
const projectName = projectNameResponse as string;
const packageManagerResponse = await select({
message: "Which package manager would you like to use?",
options: [
{ value: "npm", label: "npm" },
{ value: "pnpm", label: "pnpm" },
{ value: "yarn", label: "yarn" },
],
});
if (isCancel(packageManagerResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
const packageManager = packageManagerResponse as PackageManager;
const autoInstallDepsResponse = await confirm({
message: "Would you like to automatically install dependencies?",
initialValue: true,
});
if (isCancel(autoInstallDepsResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
const autoInstallDeps = autoInstallDepsResponse as boolean;
const frameworkResponse = await select({
message: "Which framework would you like to use?",
options: [
{ value: "nextjs", label: "Next.js" },
{ value: "vite", label: "Vite" },
],
});
if (isCancel(frameworkResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
const framework = frameworkResponse as Framework;
const selectedAgentsResponse = await multiselect({
message:
'Which pre-built agents would you like to include? (Press "space" to select/unselect)',
options: [
{ value: "react", label: "ReAct Agent" },
{ value: "memory", label: "Memory Agent" },
{ value: "research", label: "Research Agent" },
{ value: "retrieval", label: "Retrieval Agent" },
],
initialValues: ["react", "memory", "research", "retrieval"],
required: false,
});
if (isCancel(selectedAgentsResponse)) {
cancel("Operation cancelled");
process.exit(0);
if (isCancel(projectNameResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
projectName = projectNameResponse as string;
}
const selectedAgents = selectedAgentsResponse as string[];
// Package manager prompt
let packageManager = partialAnswers.packageManager;
if (!packageManager) {
const packageManagerResponse = await select({
message: "Which package manager would you like to use?",
options: [
{ value: "npm", label: "npm" },
{ value: "pnpm", label: "pnpm" },
{ value: "yarn", label: "yarn" },
],
});
// Determine which agents are included
const includeReactAgent = selectedAgents.includes("react");
const includeMemoryAgent = selectedAgents.includes("memory");
const includeResearchAgent = selectedAgents.includes("research");
const includeRetrievalAgent = selectedAgents.includes("retrieval");
if (isCancel(packageManagerResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
packageManager = packageManagerResponse as PackageManager;
}
// Auto install dependencies prompt
let autoInstallDeps = partialAnswers.autoInstallDeps;
if (autoInstallDeps === undefined) {
const autoInstallDepsResponse = await confirm({
message: "Would you like to automatically install dependencies?",
initialValue: true,
});
if (isCancel(autoInstallDepsResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
autoInstallDeps = autoInstallDepsResponse as boolean;
}
// Framework prompt
let framework = partialAnswers.framework;
if (!framework) {
const frameworkResponse = await select({
message: "Which framework would you like to use?",
options: [
{ value: "nextjs", label: "Next.js" },
{ value: "vite", label: "Vite" },
],
});
if (isCancel(frameworkResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
framework = frameworkResponse as Framework;
}
// Check if all agent selections are already provided
const allAgentSelectionsProvided =
partialAnswers.includeReactAgent !== undefined &&
partialAnswers.includeMemoryAgent !== undefined &&
partialAnswers.includeResearchAgent !== undefined &&
partialAnswers.includeRetrievalAgent !== undefined;
// Agent selection prompt if not all provided
let includeReactAgent = partialAnswers.includeReactAgent ?? false;
let includeMemoryAgent = partialAnswers.includeMemoryAgent ?? false;
let includeResearchAgent = partialAnswers.includeResearchAgent ?? false;
let includeRetrievalAgent = partialAnswers.includeRetrievalAgent ?? false;
if (!allAgentSelectionsProvided) {
const selectedAgentsResponse = await multiselect({
message:
'Which pre-built agents would you like to include? (Press "space" to select/unselect)',
options: [
{ value: "react", label: "ReAct Agent" },
{ value: "memory", label: "Memory Agent" },
{ value: "research", label: "Research Agent" },
{ value: "retrieval", label: "Retrieval Agent" },
],
initialValues: ["react", "memory", "research", "retrieval"],
required: false,
});
if (isCancel(selectedAgentsResponse)) {
cancel("Operation cancelled");
process.exit(0);
}
const selectedAgents = selectedAgentsResponse as string[];
// Determine which agents are included
includeReactAgent = selectedAgents.includes("react");
includeMemoryAgent = selectedAgents.includes("memory");
includeResearchAgent = selectedAgents.includes("research");
includeRetrievalAgent = selectedAgents.includes("retrieval");
}
// Combine all answers
return {
@@ -579,8 +719,24 @@ async function promptUser(): Promise<ProjectAnswers> {
}
async function init(): Promise<void> {
// Get user input using our new promptUser function
const answers = await promptUser();
// Parse command-line arguments first
const cliOptions = await parseCommandLineArgs();
const allRequiredOptionsProvided =
cliOptions.autoInstallDeps !== undefined &&
cliOptions.projectName !== undefined &&
cliOptions.packageManager !== undefined &&
cliOptions.framework !== undefined &&
cliOptions.includeReactAgent !== undefined &&
cliOptions.includeMemoryAgent !== undefined &&
cliOptions.includeResearchAgent !== undefined &&
cliOptions.includeRetrievalAgent !== undefined;
// If all options are provided via CLI, use them directly
// Otherwise, prompt for the missing options
const answers = allRequiredOptionsProvided
? (cliOptions as ProjectAnswers)
: await promptUser(cliOptions);
const { projectName, packageManager, autoInstallDeps, framework } = answers;
@@ -640,7 +796,11 @@ async function init(): Promise<void> {
await Promise.all([
updateLangGraphConfig(targetDir, chalk, includesAgentSelectionsMap),
setAgentPackageJsonFields(targetDir, includesAgentSelectionsMap, chalk),
setAgentPackageJsonFields(targetDir, {
agentArgs: includesAgentSelectionsMap,
packageManager,
chalk,
}),
setEnvExampleFile(targetDir, includesAgentSelectionsMap, chalk),
]);
+2 -2
View File
@@ -6,8 +6,8 @@
"type": "module",
"scripts": {
"dev": "npx @langchain/langgraph-cli dev --port 2024 --config ../../langgraph.json",
"build": "yarn turbo build:internal --filter=agents",
"build:internal": "yarn clean && tsc",
"build": "turbo build:internal --filter=agents",
"build:internal": "{PACKAGE_MANAGER} run clean && tsc",
"clean": "rm -rf ./dist .turbo || true",
"format": "prettier --config .prettierrc --write \"src\"",
"lint": "eslint src",
+3 -1
View File
@@ -5,7 +5,9 @@ import { initChatModel } from "langchain/chat_models/universal";
* @param fullySpecifiedName - String in the format 'provider/model' or 'provider/account/provider/model'.
* @returns A Promise that resolves to a BaseChatModel instance.
*/
export async function loadChatModel(fullySpecifiedName: string) {
export async function loadChatModel(
fullySpecifiedName: string,
): Promise<ReturnType<typeof initChatModel>> {
const index = fullySpecifiedName.indexOf("/");
if (index === -1) {
// If there's no "/", assume it's just the model
+5
View File
@@ -1392,6 +1392,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commander@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46"
integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"