Welcome back!
In the previous chapter, Dynamic Command Loading, we learned how to "lazy load" our code. We retrieved the heavy logic from the warehouse only when the user asked for it.
Now, we have the code in our hands. But what do we do with it?
If this were a standard command line tool, we might just console.log some text and quit. But we are building something interactive. We want menus, colors, and key-press handling. To do this, we use Local JSX Execution.
Imagine you want to open a Pop-up Store. You can't just throw merchandise on the sidewalk; you need a process.
In our CLI framework, Local JSX Execution is the manager of this Pop-up Store. It handles the lifecycle of an interactive command.
The Problem: Standard scripts run top-to-bottom and finish instantly. The Solution: We need a way to keep the application "alive" while the user interacts with the interface, and a clean way to exit when they are done.
call Function
Every command in our system that shows a UI must export a specific function named call. This function is the manager.
Let's look at the signature of this function.
Input: hooks.tsx (Concept)
import type { LocalJSXCommandCall } from '../../types/command.js';
// The function signature
export const call: LocalJSXCommandCall = async (onDone, context) => {
// 1. Setup Phase
// 2. Render Phase
};
Explanation: The framework passes us two critical tools:
onDone: This is the "Close Store" button. When we call this function, the interactive mode ends, and the terminal returns to normal.context: This is our toolbox. It allows us to access the application's global data (see Application State Context).
Before we show anything to the user, we need to gather data. In our hooks command, we want to show a list of tools.
Input: hooks.tsx (Logic)
// inside the 'call' function...
// Get the global application state
const appState = context.getAppState();
// Get the specific permissions we need
const permissionContext = appState.toolPermissionContext;
// Extract a list of tool names (e.g., ['linter', 'tester'])
const toolNames = getTools(permissionContext).map(tool => tool.name);
Explanation:
We are preparing the inventory for our store. We ask the context for the current state of the app, and we filter out the list of tools we need to display.
This is where the magic happens. Instead of returning text or a number, we return JSX.
If you have used React for the web, this looks familiar. We are using a library called Ink to render React components as text in the terminal.
Input: hooks.tsx (Rendering)
import React from 'react';
import { HooksConfigMenu } from './HooksConfigMenu.js';
// inside the 'call' function...
return (
<HooksConfigMenu
toolNames={toolNames}
onExit={onDone}
/>
);
Explanation:
<HooksConfigMenu />: This is our visual component (the shelves and counters).toolNames={...}: We pass the data we fetched in the Setup Phase.onExit={onDone}: This is crucial! We pass the "Close Store" button into the component. The component will press this button when the user hits Esc or selects "Exit".How does the framework handle a function that returns HTML-like tags? Let's visualize the flow.
The CLI Runner does some heavy lifting to make this seamless. It doesn't just print the JSX; it "mounts" it, effectively taking over the terminal window.
Here is a simplified look at how the framework executes your call function.
Input: cli-runner.ts (Simplified Internal Logic)
import { render } from 'ink';
async function executeLocalJsx(command, context) {
// A promise that resolves only when the command says it's done
let finishedResolver;
const finishedPromise = new Promise(r => finishedResolver = r);
// Run the user's setup logic
const uiComponent = await command.call(finishedResolver, context);
// Tell Ink to take over the terminal screen
const { unmount } = render(uiComponent);
// PAUSE here until the user clicks "Exit" (calling onDone)
await finishedPromise;
// Clean up the terminal screen
unmount();
}
Explanation:
finishedPromise: The framework creates a "pause button". It waits indefinitely at await finishedPromise.render(uiComponent): The Ink library draws your UI component to the screen and starts listening for keyboard events.unmount(): When finishedResolver (which is your onDone) is finally called, the framework clears the screen and returns the user to their standard shell.
This Local JSX architecture is powerful because it allows plugins to bring their own UIs.
In this chapter, we learned:
call function acts as the store manager: setting up data and opening the UI.onDone is the mechanism to cleanly exit the application loop.Now that we know how to render the UI for our tools, we need to understand where these tools actually come from. How does the system know which tools are available to populate our menu?
Next, we will explore the Tool Ecosystem Integration.
Next: Tool Ecosystem Integration
Generated by Code IQ