I wanted to learn how to make pleasant and interactive CLI like the creat-vite project scaffolding CLI, so yesterday I took a quick dive into the code of create-vite (note: not vite itself) to study what magical sauces they have and hopefully learn some cool new techniques along the way.
At a glance
Everything starts in the init function of the src/index.ts file
At a glance, we can see that the CLI progresses through 6 stages:
- Get (or ask for) project name and target directory
- Handle directory if exist and not empty
- Get package name (if the project name at step 1 is an invalid NPM package name)
- ASk users to choose a framework and variant
- Ask user if immediate install is desired
- Finally starts scaffolding folders and files
The main "stars" of this elegant experience includes:
-
mrilibrary for working with CLI arguments -
@clack/promptslibrary for displaying pretty interactive prompts - And
picocolorsfor adding colors to the console log
Overall, the create-vite CLI is a pretty straightforward and simple tool. Diving into the code, I learned some interesting details.
Cool thing 1 - All the different CLI flags
On README, you might see that create-vite CLI supports the --template flag, but that's not the only one. Here are some more:
-
--overwrite/--no-overwrite: Do you want to overwrite if a non-empty directory already exists at your target location -
--immediate/--no-immediate: Marks your preference for step (5) - i.e. do you want immediate install and starting the Dev server after scaffolding -
--interactive(-i) /--no-interactive: Should Vite prompt you for answer, or assume default values? By default, the template isvanilla-tsif none is provided, and overwrite and immediate is false. The No-Interactive mode is useful when runningcreate-viteas part of some unmonitored CI/CD pipeline, or if an AI agent is running some command.
Additionally, here are the full list of template names that you can pass into the --template argument:
// vanilla
"vanilla-ts",
"vanilla",
// vue
"vue-ts",
"vue",
"custom-create-vue",
"custom-nuxt",
"custom-vike-vue",
// react
"react-ts",
"react-compiler-ts",
"react-swc-ts",
"react",
"react-compiler",
"react-swc",
"rsc",
"custom-react-router",
"custom-tanstack-router-react",
"redwoodsdk-standard",
"custom-vike-react",
// preact
"preact-ts",
"preact",
"custom-create-preact",
// lit
"lit-ts",
"lit",
// svelte
"svelte-ts",
"svelte",
"custom-svelte-kit",
// solid
"solid-ts",
"solid",
"custom-tanstack-router-solid",
"custom-vike-solid",
// ember
"ember-app-ts",
"ember-app",
// qwik
"qwik-ts",
"qwik",
"custom-qwik-city",
// angular
"custom-angular",
"custom-analog",
// marko
"marko-run",
// others
"create-vite-extra",
"create-electron-vite"
Cool thing 2 - "Support" for AI agent
This is a fun little detail, but create-vite uses @vercel/detect-agent to determine if an agent is running the CLI. And if isAgent and interactive mode is enabled, the CLI will log a helpful message
To create in one go, run: create-vite --no-interactive --template
Cool thing 3 - Some coding techniques
Here are some cool programming techniques I though were very interesting:
- Determining the package mananger used via
npm_config_user_agentENV
Ever wondered how CLI can determine what package manager you used, so they can continue using that in subsequent commands? It's all thanks to the npm_config_user_agent environment variable. Each package manager sets the variable accordingly (like how pnpm does here).
Example: You can run pnpm config get user-agent to get the full agent string:
pnpm/10.20.0 npm/? node/v20.11.1 linux x64
Then you can split by space and then by slash to get the package manager name.
- Detect whether standard input is connected to a terminal or not via
process.stdin.isTTY.
The terminal input can also be piped in (like cat data.txt | xargs pnpm create-vite), in which case interactivity won't be possible. As a result, the CLI only enables interactive mode if isTTY is true
- Handling Control-C gracefully
After every prompt, I noticed that there is always a check to see if user has cancelled the command so that we can gracefully display the message "Operation Cancelled".
if (prompts.isCancel(projectName)) return cancel()
This technique feels so obvious in retrospect (and the @clack/prompts creator also recommends so), but seeing how it is employed in production-ready code base somehow really cements it for me.
In summary - lessons I learned
-
mri,@clack/prompts, andpicocolorsare a quick combo to create a very pleasant CLI - Use
process.env.npm_config_user_agentto detect what package manager was used - If you're using
@clack/prompts: Checkprompts.isCancelto handle control-C gracefully
Top comments (0)