Welcome to the Keybindings project! In this tutorial series, we are going to explore how to build robust Command Line Interface (CLI) tools.
We are starting with the most fundamental concept: Command Module Structure.
Imagine a busy restaurant. To run it smoothly, you separate the Menu from the Kitchen.
index.ts): This is what the customer sees. It has the name of the dish ("Steak") and a description ("Grilled to order"). It tells the waiter (the CLI framework) what is available to order.keybindings.ts): This is where the work happens. It contains the recipe and the chef. The customer doesn't see this part; they just see the result.In our project, every command is split into these two files. This keeps our code organized and easy to read.
We want to build a command called keybindings.
Let's look at how we split this into our two files.
index.ts)
The index.ts file acts as the definition. It contains metadataβdata about the commandβbut no heavy logic.
Here is how we define the command's identity:
// --- File: index.ts ---
const keybindings = {
name: 'keybindings',
description: 'Open or create your keybindings configuration file',
type: 'local',
// ... more settings below
} satisfies Command
Explanation:
name: This is what the user types in the terminal (e.g., my-cli keybindings).description: This shows up when the user runs the --help command.How does the Menu tell the Kitchen what to do?
// --- File: index.ts ---
const keybindings = {
// ... previous settings
isEnabled: () => isKeybindingCustomizationEnabled(),
supportsNonInteractive: false,
load: () => import('./keybindings.js'),
} satisfies Command
export default keybindings
Explanation:
isEnabled: Checks if this command is allowed to run. We will cover this in Feature Gating.load: This is the magic link. It points to the actual execution file (keybindings.js).keybindings.ts)
The keybindings.ts file contains the actual "recipe." It exports a function called call that does the heavy lifting.
First, we define the main function and perform safety checks.
// --- File: keybindings.ts ---
export async function call(): Promise<{ type: 'text'; value: string }> {
// Check if we are allowed to be here
if (!isKeybindingCustomizationEnabled()) {
return {
type: 'text',
value: 'Keybinding customization is not enabled.',
}
}
// ...
Explanation:
export async function call(): This is the standard entry point. The CLI framework looks for this exact function name to start execution.Next, the code attempts to create the configuration file safely.
// ... inside call() ...
const keybindingsPath = getKeybindingsPath()
let fileExists = false
// Ensure the folder exists first
await mkdir(dirname(keybindingsPath), { recursive: true })
try {
// Try to write the file (fails if it already exists)
await writeFile(keybindingsPath, generateKeybindingsTemplate(), {
flag: 'wx',
})
} catch (e: unknown) { /* handle error */ }
Explanation:
Finally, once the file is ready, we open it for the user.
// ... inside call() ...
// Open the file in the user's default editor (vim, code, nano, etc.)
const result = await editFileInEditor(keybindingsPath)
if (result.error) {
return { type: 'text', value: `Error: ${result.error}` }
}
return {
type: 'text',
value: `Opened ${keybindingsPath} in your editor.`,
}
}
Explanation:
When a user runs a command, the system does not load every single file immediately. It follows a specific sequence to save memory and time.
Here is a simple flow of what happens when you type keybindings:
The separation relies on the load property in index.ts.
index.ts. This file is tiny. It allows the CLI to build its help menu instantly without reading the heavy code in keybindings.ts. load: () => import('./keybindings.js')
This line uses a JavaScript concept called a "Dynamic Import." It tells the computer: "Don't read the keybindings.js file yet. Only read it when the user actually asks for it." This is the core of Lazy Module Loading, which makes your application start much faster.
In this chapter, we learned that a Command Module is split into two parts:
index.ts): The menu definition.keybindings.ts): The logic recipe.This structure allows us to keep our application organized and performant.
Now that we have the structure, how do we prevent users from running commands that aren't ready yet?
Generated by Code IQ