skills-templates/ink/SKILL.md
Ink - React for CLI applications, build interactive command-line interfaces using React components with Flexbox layout, hooks for input/focus, and testing support
npx skillsauth add enuno/claude-command-and-control inkInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ink is a framework for building command-line interfaces using React components. It provides the same component-based UI building experience that React offers in the browser, but for command-line apps. The framework leverages Yoga, a Flexbox layout engine, enabling developers to use familiar CSS-like properties in terminal environments.
Ink is used by major projects including:
npm install ink react
For TypeScript:
npm install ink react
npm install -D @types/react
npx create-ink-app my-ink-cli
cd my-ink-cli
npm start
import React from 'react';
import {render, Text} from 'ink';
const App = () => <Text color="green">Hello, Ink!</Text>;
render(<App />);
import React, {useState, useEffect} from 'react';
import {render, Text} from 'ink';
const Counter = () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => c + 1);
}, 100);
return () => clearInterval(timer);
}, []);
return <Text color="green">{counter} tests passed</Text>;
};
render(<Counter />);
The <Text> component handles all text rendering. Text content must be wrapped in this component.
import {Text} from 'ink';
// Basic text
<Text>Hello World</Text>
// With colors
<Text color="green">Success</Text>
<Text color="#ff0000">Error (hex)</Text>
<Text color="rgb(255, 128, 0)">Warning (rgb)</Text>
// With styles
<Text bold>Bold text</Text>
<Text italic>Italic text</Text>
<Text underline>Underlined</Text>
<Text strikethrough>Strikethrough</Text>
<Text inverse>Inverted colors</Text>
<Text dimColor>Dimmed text</Text>
// Combined styles
<Text bold color="cyan" backgroundColor="blue">
Styled text
</Text>
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| color | string | - | Text color (named, hex, rgb) |
| backgroundColor | string | - | Background color |
| dimColor | boolean | false | Dim the color |
| bold | boolean | false | Bold text |
| italic | boolean | false | Italic text |
| underline | boolean | false | Underlined text |
| strikethrough | boolean | false | Strikethrough text |
| inverse | boolean | false | Swap foreground/background |
| wrap | string | "wrap" | Text wrapping mode |
Wrap modes:
wrap - Wrap text (default)truncate / truncate-end - Truncate at end with ellipsistruncate-start - Truncate at starttruncate-middle - Truncate in middle<Box> is the primary layout component - a flexbox container for organizing UI elements.
import {Box, Text} from 'ink';
// Basic layout
<Box>
<Text>Left</Text>
<Text>Right</Text>
</Box>
// Column layout
<Box flexDirection="column">
<Text>Top</Text>
<Text>Bottom</Text>
</Box>
// With padding and margin
<Box padding={2} margin={1}>
<Text>Padded content</Text>
</Box>
// With border
<Box borderStyle="round" borderColor="green" padding={1}>
<Text>Bordered box</Text>
</Box>
// Centered content
<Box justifyContent="center" alignItems="center" height={10}>
<Text>Centered</Text>
</Box>
| Prop | Type | Description |
|------|------|-------------|
| width | number | string | Width in spaces or percentage |
| height | number | string | Height in lines or percentage |
| minWidth | number | Minimum width |
| minHeight | number | Minimum height |
| Prop | Type | Description |
|------|------|-------------|
| padding | number | All sides |
| paddingX | number | Left and right |
| paddingY | number | Top and bottom |
| paddingTop | number | Top only |
| paddingBottom | number | Bottom only |
| paddingLeft | number | Left only |
| paddingRight | number | Right only |
| Prop | Type | Description |
|------|------|-------------|
| margin | number | All sides |
| marginX | number | Left and right |
| marginY | number | Top and bottom |
| marginTop | number | Top only |
| marginBottom | number | Bottom only |
| marginLeft | number | Left only |
| marginRight | number | Right only |
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| flexDirection | string | "row" | row, row-reverse, column, column-reverse |
| flexGrow | number | 0 | Grow factor |
| flexShrink | number | 1 | Shrink factor |
| flexBasis | number | string | - | Initial size |
| flexWrap | string | "nowrap" | nowrap, wrap, wrap-reverse |
| alignItems | string | - | flex-start, center, flex-end |
| alignSelf | string | - | auto, flex-start, center, flex-end |
| justifyContent | string | - | flex-start, center, flex-end, space-between, space-around, space-evenly |
| gap | number | - | Gap between children |
| columnGap | number | - | Horizontal gap |
| rowGap | number | - | Vertical gap |
| Prop | Type | Description |
|------|------|-------------|
| borderStyle | string | object | Border style |
| borderColor | string | Border color |
| borderTop | boolean | Show top border |
| borderBottom | boolean | Show bottom border |
| borderLeft | boolean | Show left border |
| borderRight | boolean | Show right border |
Border styles:
single - Single line (─│)double - Double line (═║)round - Rounded corners (╭╮╰╯)bold - Bold line (━┃)singleDouble - Single horizontal, double verticaldoubleSingle - Double horizontal, single verticalclassic - ASCII (+, -, |)| Prop | Type | Default | Description |
|------|------|---------|-------------|
| display | string | "flex" | flex or none |
| overflow | string | "visible" | visible or hidden |
| overflowX | string | - | Horizontal overflow |
| overflowY | string | - | Vertical overflow |
| backgroundColor | string | - | Background color |
Insert line breaks within text.
import {Text, Newline} from 'ink';
<Text>
First line
<Newline />
Second line
<Newline count={2} />
After two newlines
</Text>
Flexible space that expands along the flex direction.
import {Box, Text, Spacer} from 'ink';
// Push items apart
<Box>
<Text>Left</Text>
<Spacer />
<Text>Right</Text>
</Box>
// Equivalent to flexGrow: 1
<Box>
<Text>Left</Text>
<Box flexGrow={1} />
<Text>Right</Text>
</Box>
Render output once without updates. Perfect for logs or completed tasks.
import {Static, Box, Text} from 'ink';
const App = ({tests}) => (
<>
<Static items={tests}>
{test => (
<Box key={test.id}>
<Text color="green">✔ {test.title}</Text>
</Box>
)}
</Static>
<Box marginTop={1}>
<Text>Running more tests...</Text>
</Box>
</>
);
Props:
items - Array of items to renderchildren - Function (item, index) => ReactNodeOnly new items trigger renders; previously rendered items are not updated.
Modify text output before rendering.
import {Transform, Text} from 'ink';
// Uppercase transform
<Transform transform={output => output.toUpperCase()}>
<Text>hello world</Text>
</Transform>
// Output: HELLO WORLD
// Per-line transform (hanging indent)
<Transform transform={(line, index) =>
index === 0 ? line : ` ${line}`
}>
<Text>First line{'\n'}Second line{'\n'}Third line</Text>
</Transform>
Capture keyboard input.
import {useInput, useApp} from 'ink';
const App = () => {
const {exit} = useApp();
useInput((input, key) => {
// Character input
if (input === 'q') {
exit();
}
// Arrow keys
if (key.upArrow) {
// Move up
}
if (key.downArrow) {
// Move down
}
// Modifiers
if (key.ctrl && input === 'c') {
exit();
}
// Special keys
if (key.return) {
// Enter pressed
}
if (key.escape) {
// Escape pressed
}
});
return <Text>Press 'q' to quit</Text>;
};
Key object properties:
| Property | Description |
|----------|-------------|
| upArrow | Up arrow pressed |
| downArrow | Down arrow pressed |
| leftArrow | Left arrow pressed |
| rightArrow | Right arrow pressed |
| return | Enter key pressed |
| escape | Escape key pressed |
| ctrl | Ctrl modifier held |
| shift | Shift modifier held |
| meta | Meta/Cmd modifier held |
| tab | Tab key pressed |
| backspace | Backspace pressed |
| delete | Delete pressed |
| pageUp | Page Up pressed |
| pageDown | Page Down pressed |
Options:
// Disable input handling conditionally
useInput(handler, {isActive: false});
Access app-level methods.
import {useApp} from 'ink';
const App = () => {
const {exit} = useApp();
const handleDone = () => {
exit(); // Exit successfully
};
const handleError = () => {
exit(new Error('Something went wrong')); // Exit with error
};
return <Text>App running...</Text>;
};
Access stdin stream and raw mode.
import {useStdin} from 'ink';
const App = () => {
const {stdin, isRawModeSupported, setRawMode} = useStdin();
useEffect(() => {
if (isRawModeSupported) {
setRawMode(true);
}
return () => setRawMode(false);
}, []);
return <Text>Raw mode: {isRawModeSupported ? 'enabled' : 'not supported'}</Text>;
};
Access output streams.
import {useStdout, useStderr} from 'ink';
const App = () => {
const {stdout, write} = useStdout();
const {stderr, write: writeError} = useStderr();
const logToStdout = () => {
write('Direct stdout output\n');
};
const logToStderr = () => {
writeError('Error output\n');
};
return <Text>Stream access available</Text>;
};
Enable focus on components for Tab navigation.
import {useFocus, Box, Text} from 'ink';
const FocusableItem = ({label}) => {
const {isFocused} = useFocus();
return (
<Box>
<Text color={isFocused ? 'green' : 'gray'}>
{isFocused ? '>' : ' '} {label}
</Text>
</Box>
);
};
const App = () => (
<Box flexDirection="column">
<FocusableItem label="Option 1" />
<FocusableItem label="Option 2" />
<FocusableItem label="Option 3" />
</Box>
);
Options:
useFocus({
autoFocus: true, // Focus on mount
isActive: true, // Enable/disable focus
id: 'my-component' // Unique identifier for programmatic focus
});
Control focus programmatically.
import {useFocusManager, useInput} from 'ink';
const App = () => {
const {
enableFocus,
disableFocus,
focusNext,
focusPrevious,
focus
} = useFocusManager();
useInput((input, key) => {
if (key.tab) {
if (key.shift) {
focusPrevious();
} else {
focusNext();
}
}
});
return (
<Box flexDirection="column">
<FocusableItem id="item-1" />
<FocusableItem id="item-2" />
</Box>
);
};
import {render} from 'ink';
const {rerender, unmount, waitUntilExit, clear} = render(<App />, options);
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| stdout | stream | process.stdout | Output stream |
| stdin | stream | process.stdin | Input stream |
| stderr | stream | process.stderr | Error stream |
| exitOnCtrlC | boolean | true | Exit on Ctrl+C |
| patchConsole | boolean | true | Intercept console output |
| debug | boolean | false | Debug mode (no clear) |
| maxFps | number | 30 | Maximum frame rate |
| incrementalRendering | boolean | false | Only update changed lines |
// Re-render with new props
rerender(<App newProp={value} />);
// Unmount and exit
unmount();
// Wait for exit (returns promise)
await waitUntilExit();
// Clear rendered output
clear();
Use ink-testing-library for testing Ink components.
npm install --save-dev ink-testing-library
import {render} from 'ink-testing-library';
import App from './App';
test('renders greeting', () => {
const {lastFrame} = render(<App name="World" />);
expect(lastFrame()).toContain('Hello, World');
});
test('updates on input', () => {
const {lastFrame, stdin} = render(<Counter />);
expect(lastFrame()).toContain('Count: 0');
stdin.write('i'); // Increment
expect(lastFrame()).toContain('Count: 1');
});
| Method | Description |
|--------|-------------|
| lastFrame() | Get last rendered output as string |
| frames | Array of all rendered frames |
| stdin | Writable stream for simulating input |
| rerender(element) | Re-render with new element |
| unmount() | Unmount the component |
Ink supports ARIA attributes for screen reader compatibility.
<Box
aria-role="list"
aria-label="Menu options"
>
<Box aria-role="listitem">
<Text>Option 1</Text>
</Box>
<Box aria-role="listitem">
<Text>Option 2</Text>
</Box>
</Box>
| Prop | Type | Description |
|------|------|-------------|
| aria-label | string | Accessible label |
| aria-hidden | boolean | Hide from screen readers |
| aria-role | string | ARIA role |
| aria-state | object | State properties |
Supported roles:
button, checkbox, radio, radiogrouplist, listitemmenu, menuitemprogressbartab, tablisttimer, toolbar, tableimport {useIsScreenReaderEnabled} from 'ink';
const App = () => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
return (
<Box aria-label="Main content">
{isScreenReaderEnabled ? (
<Text>Screen reader friendly content</Text>
) : (
<Text>Visual content with colors</Text>
)}
</Box>
);
};
import React, {useState, useEffect} from 'react';
import {Text} from 'ink';
const Spinner = () => {
const [frame, setFrame] = useState(0);
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
useEffect(() => {
const timer = setInterval(() => {
setFrame(f => (f + 1) % frames.length);
}, 80);
return () => clearInterval(timer);
}, []);
return <Text color="cyan">{frames[frame]} Loading...</Text>;
};
import {Box, Text} from 'ink';
const ProgressBar = ({percent}) => {
const width = 20;
const filled = Math.round(width * percent / 100);
const empty = width - filled;
return (
<Box>
<Text color="green">{'█'.repeat(filled)}</Text>
<Text color="gray">{'░'.repeat(empty)}</Text>
<Text> {percent}%</Text>
</Box>
);
};
import React, {useState} from 'react';
import {Box, Text, useInput} from 'ink';
const Menu = ({items, onSelect}) => {
const [selected, setSelected] = useState(0);
useInput((input, key) => {
if (key.upArrow) {
setSelected(s => Math.max(0, s - 1));
}
if (key.downArrow) {
setSelected(s => Math.min(items.length - 1, s + 1));
}
if (key.return) {
onSelect(items[selected]);
}
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={item} color={i === selected ? 'green' : 'white'}>
{i === selected ? '> ' : ' '}{item}
</Text>
))}
</Box>
);
};
import {Box, Text} from 'ink';
const TwoColumn = () => (
<Box>
<Box width="50%" borderStyle="single" padding={1}>
<Text>Left Column</Text>
</Box>
<Box width="50%" borderStyle="single" padding={1}>
<Text>Right Column</Text>
</Box>
</Box>
);
| Package | Description |
|---------|-------------|
| ink-spinner | Spinner component |
| ink-text-input | Text input component |
| ink-select-input | Select/menu component |
| ink-table | Table component |
| ink-link | Clickable links |
| ink-gradient | Gradient text |
| ink-big-text | Large ASCII text |
| ink-testing-library | Testing utilities |
tools
MemPalace local-first AI memory system. Use when setting up persistent memory for Claude Code sessions, mining project files or conversation transcripts, querying past context, configuring MCP tools, managing the knowledge graph, or troubleshooting palace operations.
tools
LangSmith Python SDK — trace, evaluate, and monitor LLM applications. Covers @traceable decorator, trace context manager, Client API, evaluate() / aevaluate(), comparative evaluation, custom evaluators, dataset management, prompt caching, ASGI middleware, and pytest plugin.
development
LangGraph (Python) — build stateful, controllable agent graphs with checkpointing, streaming, persistence, interrupts, fault tolerance, and durable execution. Covers both Graph API (StateGraph) and Functional API (@entrypoint/@task).
development
LangGraph Graph API (Python) — build explicit DAG agent workflows with StateGraph, typed state, nodes, edges, Command routing, Send fan-out, checkpointers, interrupts, and streaming. Use when you need explicit control flow and graph topology.