Welcome to the Voice project! In this tutorial series, we are going to build a robust Command Line Interface (CLI) feature.
Imagine you are building a Swiss Army Knife. It has a knife, a screwdriver, a saw, and a pair of scissors.
If you tried to open all the tools at once every time you picked it up, it would be heavy, dangerous, and hard to handle. Instead, you keep them folded (hidden) and only pull out the specific tool you need, when you need it.
In software, this is the Command Definition Pattern.
We want to add a voice command to our CLI. The logic for voice processing is heavy (it needs audio libraries). If we load that heavy logic every time the user just wants to check the help menu, our CLI will be slow.
The Solution: We create a lightweight "Registration Card" (or Menu Item) that tells the CLI about the command without actually loading the heavy code until the user specifically asks for it.
Think of this pattern like a Menu in a Restaurant:
In this chapter, we are writing the Menu.
We will create a file called index.ts. This file acts as the registration card for our command.
First, we define the basics: what is this command called, and what does it do?
// index.ts
const voice = {
type: 'local',
name: 'voice',
description: 'Toggle voice mode',
supportsNonInteractive: false,
// ... more properties later
}
> voice).false because voice mode requires a human to speak.Next, we determine who can order this item. Sometimes, a feature shouldn't be visible to everyone yet.
// ... inside the object
availability: ['claude-ai'],
isEnabled: () => isVoiceGrowthBookEnabled(),
get isHidden() {
return !isVoiceModeEnabled()
},
Note: These checks rely on concepts we will cover in Feature Availability Gating. For now, just know these act as the "bouncers" deciding if the command is available.
Finally, we tell the CLI where to find the "Kitchen" (the actual code), but we wrap it in a function so it doesn't run immediately.
// ... inside the object
load: () => import('./voice.js'),
} satisfies Command
export default voice
import() to fetch the heavy ./voice.js file only when called.Command structure (TypeScript helper).Let's visualize the flow. When you run the CLI, it doesn't load the voice libraries immediately. It only reads the definition we just wrote.
index.ts. It sees "Okay, there is a command named voice." It does not read voice.js yet.voice.isEnabled. If it's true, it proceeds..load() function defined in our object.voice.js file and runs it.Here is the complete file combining all the pieces we discussed. It's clean, simple, and efficient.
import type { Command } from '../../commands.js'
import {
isVoiceGrowthBookEnabled,
isVoiceModeEnabled,
} from '../../voice/voiceModeEnabled.js'
const voice = {
type: 'local',
name: 'voice',
description: 'Toggle voice mode',
availability: ['claude-ai'],
isEnabled: () => isVoiceGrowthBookEnabled(),
get isHidden() {
return !isVoiceModeEnabled()
},
supportsNonInteractive: false,
load: () => import('./voice.js'),
} satisfies Command
export default voice
In this chapter, you learned the Command Definition Pattern.
Now that our command is defined and registered, we need to handle how the application remembers the user's choices (like which voice they want to use).
See you in the next chapter!
Next: Settings Persistence & Change Detection
Generated by Code IQ