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 "ingredients" for 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: Should Vite 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, andoverwriteandimmediateis false. The No-Interactive mode is useful when runningcreate-viteas part of some unmonitored CI/CD pipeline, or if an AI agent is running commands.
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 for the agent:
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 manager via
npm_config_user_agentENVEver wondered how a CLI can determine what package manager you used, so it can continue using that in subsequent commands? It's all thanks to the
npm_config_user_agentenvironment variable. Each package manager sets the variable accordingly (like howpnpmdoes here).Example: You can run
pnpm config get user-agentto get the full agent string:
pnpm/10.20.0 npm/? node/v20.11.1 linux x64Then 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.isTTYThe 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 ifisTTYis true. -
Handling Control-C gracefully
After every prompt, I noticed that there is always a check to see if the 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/promptscreator 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)