Building Your Own NPX CLI Tool
Recently, I've been working on building a number of demos and PoCs covering a number of topics related to Headless Content Platforms, AI and nextjs. In so doing, I'm often working from a common set of base projects that implement the necessary basics like routing, components, and more. After the first few times of copying a base project, I decided it would be a good time to learn how to make a command line tool to bootstrap new projects. This post describes how to create such a tool.
This interface runs via npx
and uses the chalk
package for styling console output, and inquirer
for prompting the user for input. The language of choice is typescript.
"dependencies": {
"@types/node": "^22.13.9",
"chalk": "^5.4.1",
"inquirer": "^12.4.2",
"ts-node": "^10.9.2"
},
"devDependencies": {
"typescript": "^5.8.2"
}
To allow the tool to be executed by npx
, we'll need a few more package properties. First we'll configure the main method for execution to point to an index file that we'll build. Then we'll map a bin section with the command we want to bind to, create-myproject
. Finally, we'll update the build script to compile the typescript to javascript and pack the source as an installable tgz file.
{
// ...
"main": "dist/index.js",
"type": "module",
"bin": {
"create-myproject": "./dist/index.js"
},
"scripts": {
"build": "npx tsc && npm pack
}
}
Next we'll make a src/index.ts
file that starts with a shebang to allow the file to be executed.
#!/usr/bin/env node
At this point, you can go off and build the file as you need it. For instance, you can use inquirer
to ask the user to select a repository to clone.
First, we'll divert to a Git.ts
to declare a helper method
import fs from "fs";
import { execSync } from "child_process";
import path from "path";
import chalk from "chalk";
export function cloneRepoToFolder(repo, projectName) {
const fullPath = path.join(process.cwd(), projectName);
const gitFolder = path.join(process.cwd(), projectName, ".git");
console.log(chalk.blue("[INFO] ") + `Cloning ${repo} to ${fullPath}`);
execSync(`git clone ${repo} ${fullPath}`);
console.log(chalk.blue("[INFO] ") + "Cleaning git artifacts");
fs.rmSync(gitFolder, { recursive: true, force: true });
}
We'll use this back in index.ts
to help us clone the repository. We'll collect some info via inquirer
and use it to make folders and clone repos.
import { select } from "@inquirer/prompts";
const options = [
{ name: "Repository 1", value: "https://github.com/jsedlak/reach" },
{ name: "Quit", value: "Quit" },
];
const projectName = await input({
message: "What is the name of your project (e.g. site-name)?",
required: true,
});
select({
message: "What repository are you bootstrapping?",
choices: options,
})
.then((selectedOption) => {
cloneRepoToFolder(selectedOption, projectName);
})
.catch((reason) => {
/* no op */
});
Finally, we can pack this up, install it and use it!
npm run build
npm install -g package-name-1.0.0.tgz
npx create-myproject