Welcome to the final chapter of our tutorial series!
In the previous chapter, Context Dependency Injection, we learned how the system acts like a "Site Manager," handing our command a toolbox (the context) filled with the data it needs.
We now have everything we need:
There is just one piece missing: How does the command end?
In this chapter, we explore Lifecycle Flow Control. This is how a command tells the system, "I am finished, you can take over now."
Think of your application as a Relay Race.
skills, the System passes the Baton (control) to your command.If your command drops the baton or never passes it back, the race stops. The terminal hangs, and the user gets stuck.
In skills.tsx, we launch a visual menu (<SkillsMenu />).
At that exact momentβwhen "Enter" is pressedβwe need to trigger the Lifecycle Flow Control to close the interactive mode.
onDone Callback
The "Baton" in our code is a specific function called onDone. The system gives it to us, and we must call it when we are finished.
Let's look at the call function in skills.tsx one last time.
// skills.tsx
export async function call(
onDone: LocalJSXCommandOnDone, // <--- The Baton!
context: LocalJSXCommandContext
) {
// ...
}
Explanation: The first argument, onDone, is a function. We didn't write this function; the System wrote it and handed it to us.
Our call function is just the "Director." It doesn't handle keyboard presses. The <SkillsMenu /> component does that. So, we need to pass the baton down to the actor on stage.
// Inside the call function
return (
<SkillsMenu
onExit={onDone} // We pass the baton to the component
commands={context.options.commands}
/>
);
Explanation:
SkillsMenu component has a prop called onExit.onDone function to this prop.
We don't see the code for SkillsMenu here, but inside that component, there is logic that listens for the "Enter" key. When that happens, it conceptually does this:
// Conceptual code inside SkillsMenu component
handleEnterKey() {
// User selected an item!
// Time to close the menu.
props.onExit(); // <--- Calling the function we passed down
}
Explanation: calling props.onExit() triggers onDone(). This signals the System that the interactive part is over.
How does the system actually "wait" for the user? It uses JavaScript Promises.
When the System calls your function, it pauses execution until your function resolves (completes).
await module.call(...). The await keyword freezes the System's logic.onDone essentially tells the System "The promise is resolved." The await finishes, and the code moves to the next line.Here is the simplified "Runner" code again to show where the pause happens.
// Simplified System Runner
async function runCommand() {
// 1. Create a promise that waits for a signal
let signalFinished;
const waitForUser = new Promise((resolve) => {
// We save the 'resolve' function.
// Calling this unpauses the Promise.
signalFinished = resolve;
});
// 2. Run your command, giving it the signal trigger
// Note: We don't await this line immediately, we await the signal.
module.call(signalFinished, context);
console.log("System is waiting for user...");
// 3. PAUSE HERE until onDone (signalFinished) is called
await waitForUser;
console.log("User is done! Shutting down.");
}
Explanation:
signalFinished which becomes onDone).await waitForUser.In this final chapter, we mastered Lifecycle Flow Control.
onDone callback.call function -> Component (onExit) -> User Action -> onDone() -> System Resumes.
Congratulations! You have completed the skills Project Tutorial. You now understand the core architecture of a scalable CLI plugin system:
With these five patterns, you can build complex, interactive, and high-performance tools that are easy to maintain and expand. Happy coding!
Generated by Code IQ