Hello, I'm Emmanuel, a full-stack developer who loves solving real-world problems and making my life easier with tech.
CLI stands for Command Line Interface, which is a text-based interface used to communicate with computers. It is minimal and does not require extensive knowledge to be used. This is my first time building a CLI tool.
Kinde is an authentication service that aims to improve and simplify the process of authorization and authentication for startups and business owners. They allow you to ship fast without letting authentication slow you down.
Benefits of using Kinde
Focus on core business logic and operations, allowing you to ship features faster than the competition.
Provide resources to properly authenticate and authorize users with a focus on security.
Save money by offering the best tools and integrations possible.
Get 10,500 active users for free and a suite of available integrations.
I came across Kinde on YouTube and tried it out. To be honest, they are doing a great job.
There are a few reasons why I decided to create a CLI for Kinde, as listed below:
A CLI greatly improves the developer experience, especially if your user base or customers are mostly developers.
The CLI saves me time, eliminating the need to go to the browser to add a new user to my business or to modify permissions or roles.
Most importantly, I don't have to keep switching tabs between my browser and my IDE for minor changes on the Kinde Dashboard.
Creating a CLI in Node.js:
There are two things you will need:
Add this shebang line to the beginning of your entry file (e.g., index.ts):
#!/usr/bin/env node
Add the bin object to your package.json file. The property will be the name of your CLI, and the value will be the entry script of your program:
{ "bin": { "kinde-cli": "./dist/src/index.js" } }
Dependencies
The CLI uses npm packages like:
Commander JS (providing a convenient way for parsing command line arguments)
@clark/prompts (providing a way to prompt users for data)
picocolors (for colored text in the command line)
Installation
You can use the CLI via installing from NPM
npm install -g kinde-cli
or
npx kinde-cli --help
Exploring API Documentation
When creating a CLI tool for a startup, it is important to check out their API docs. By understanding the documentation, you will know in advance the features the CLI will be able to support. Reading the API docs, I realized some features might be harder to implement than others, e.g., properly displaying a list of data while providing support for pagination.
If I hadn't read the documentation, how would I know this? So, it's essential to read the docs. I also decided to implement simpler features first and worry about the complex ones later.
Handling Authentication
Since the Kinde API requires users to provide their access token, the CLI will also require users to be authenticated. This posed a challenge because, for a CLI, there are no cookies. If you intend to persist data for the authenticated user, you have no choice but to save it on the user's machine and hope for the best.
Overall Code Structure
I used the npm package called command
to parse arguments from the terminal. By splitting the code into classes, each representing a separate class of instruction, the code becomes easy to read, understand, and build upon.
#!/usr/bin/env node
import { Command } from "commander";
import packageJson from "../package.json";
import Authentication from "./questions/auth";
import Permission from "./questions/permissions";
import { createRootDirectory } from "./utils/storage";
createRootDirectory();
const program = new Command();
program
.name("kinde-cli")
.option("-v, --version", "version")
.description("A cli to manage your Kinde projects and dashboard workspace")
.version(packageJson.version);
new Authentication(program);
new Permission(program);
try {
program.parse(process.argv);
} catch (err) {
let error = err as Error;
console.log(`[Error]: ${error.message}`);
}
Absence of Middlewares in Commander JS
I couldn't find any articles or documentation on middleware support in Commander JS. Middleware support is essential, as I needed it to check the auth status of the user by reading the config. If the data is not valid, the middleware ends the node process.
export function errorHandler(
handler: "Auth" | "NoAuth",
fn: (...args: any[]) => Promise<any>
) {
return async (...args: any[]) => {
/**
* Some command action handlers do not require auth,
* But this auth check should run all at all times if handler value is Auth
*/
if (handler === "Auth") {
try {
await isAuthenticated();
} catch (e) {
handleMiddlewareError(e as Error);
}
}
/**
* This function below will never be executed if auth check fails,
* if auth fails handleMiddlwareError exits the process with a status of 1
*/
return await fn(...args).catch(handleMiddlewareError);
};
}
Here's how I used the middleware in code:
class Permission {
constructor(private program: Command) {
this.handlePermission();
}
private handlePermission() {
let program = this.program;
program
.command("permission")
.description(
colors.blue(
"Manage User Permissions. For more information, refer to: https://kinde.com/docs/user-management/user-permissions/"
)
)
.action(
errorHandler("Auth", async (str, options) => {
let context = ctx.getData() as ConfigData;
let prompt = (await select({
message: "Proceed with appropriate action",
options: [
{
label: "Create Permission",
value: "create",
},
{
label: "Update Permission",
value: "update",
},
],
})) as PermissionAction;
if (prompt === "create") {
let createPermissionData = await this.__createPermissionPrompts();
let response = await axiosRequest({
path: `${context.normalDomain}/api/v1/permissions`,
method: "POST",
data: createPermissionData,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${context.token.access_token}`,
},
});
return prettifyAxios(response);
}
if (prompt === "update") {
let updatePermission = await this.__updatePermissionPrompts();
let response = await axiosRequest({
path: `${context.normalDomain}/api/v1/permissions/${updatePermission.permissionId}`,
method: "PATCH",
data: omit("permissionId", updatePermission),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${context.token.access_token}`,
},
});
return prettifyAxios(response);
}
})
);
}
}
The errorHandler middleware serves as both an error handler and middleware, even though it might seem a bit hacky, it effectively works.
Contribution
If you wish to contribute or just give us a star the link to the github repo: Github Repo. Thank you for reading.