Não é novidade que existem CLIs. Nem que elas podem ser criadas das mais diversas formas, desde algo pequeno e objetivo como um simples ls
, até editores complexos e poderosos como o Neovim.
Recentemente estive pesquisando sobre as novas ferramentas de IA e percebi um detalhe bem interessante sobre o Claude Code, Codex, Copilot... Todos utilizam React!
E é isso que vou mostrar como fazer nesse artigo.
TL;DR
Nesse artigo vamos focar no mínimo necessário e o código final está disponível em jrmmendes/ink-cli-minimal-app.
Caso procure uma aplicação um pouco mais completa, com alguns padrões adicionais e testes unitários, há um template disponível em jrmmendes/create-cli-app, que pode ser testado localmente:
npm i -g @jrmmendes/create-cli-app
create-cli-app --shell
Veja a demo.
Learn Once, Write Anywhere
O meu objetivo não é falar sobre react de forma filosófica aqui, porem é útil refletir sobre a ideia central dessa tecnologia: leve o conhecimento sobre como criar interfaces de um lugar, para outro.
Dado que a tecnologia é a mesma (React), em cada plataforma existe uma forma de utilizar os componentes básicos sobre os quais algo pode ser construído.
Se está trabalhando com web, você utiliza o react-dom e elementos HTML como div
e span
; se está criando algo mobile, React Native e e suas tags serão View
s. CLI? É aí que entra o ink - e vale comentar: não se trata de uma versão adaptada ou menor do React.
Veja um exemplo:
import { Box, Text } from 'ink';
export const Banner = (props: { name: string, version: string, cwd: string }) => {
const ORANGE_COLOR = "#FE9900";
return (
<Box
borderStyle="round"
paddingX={1}
borderColor={ORANGE_COLOR}
flexDirection="column"
>
<Box paddingBottom={1}>
<Text bold color={ORANGE_COLOR}>React + Ink Example ApplicationText>
Box>
<Text>cwd: {props.cwd}Text>
<Text>{props.name}Text>
<Text>version: {props.version}Text>
Box>
)
};
Construindo a aplicação
Vamos começar da forma mais direta possível: qual é o mínimo necessário para executar algo com Ink?
Bem, você certamente vai precisar de um projeto javascript/jsx (e typescript, nesse caso), o que implica em ter um sistema de build com todas as etapas configuradas - a magia que o create-react-app
e seus sucessores entregam em um único comando.
Felizmente, estamos em 2025. Isso significa que o 7x1 foi há 11 anos, mas também que o bun
, que resolve tudo de forma simples e performática, pode ser utilizado. Com efeito, executando:
bun init -y
Temos o esqueleto do projeto:
.
├── bun.lock
├── index.ts
├── package.json
├── README.md
└── tsconfig.json
A partir disso, alguns itens precisam ser ajustados para que o build funcione corretamente no caso de uma aplicação CLI:
- Instalar dependências do React/Ink.
- Definir um (ou mais) caminhos de execução, através da chave
bin
; - Definir quais arquivos serão utilizados para gerar o pacote npm durante a fase de publicação;
Dependências
É necessário instalar, além de ink e react, alguns pacotes para escrita de testes unitários (ink-testing-library) e as respectivas definições de tipo. Também utilizaremos o rimraf
, para permitir limpar arquivos de build no futuro:
bun i ink react@18
bun i -D @types/{ink,ink-testing-library,react} ink-testing-library rimraf react-devtools-core
Configurações
Crie um arquivo bin.js
com o seguinte conteúdo:
#!/bin/env node
import('./dist/bundle.js');
Após isso, marque o arquivo como executável, o que pode ser feito em sistemas unix por meio do chmod
:
chmod +x bin.js
Ajuste o package.json
, adicionando o nome do executável e os arquivos do build:
{
"bin": {
"ink-cli-app": "./bin.js"
},
"files": [
"dist",
"bin.js"
]
}
Também inclua alguns scripts para build, test e execução da aplicação:
{
"scripts": {
"clear": "rimraf dist",
"test": "FORCE_COLOR=0 NODE_ENV=test bun test",
"build": "bun run clear && bun build src/application.tsx --outfile=./dist/bundle.js --target=node --format=esm --minify",
"start": "node ./bin.js",
"start:dev": "bun ./src/application.tsx",
"prepublishOnly": "bun run build",
}
}
Remova o arquivo index.ts
e crie uma nova pasta src
, com o arquivo application.tsx
:
import { exit } from 'process';
import { Box, render, Text } from 'ink';
import pkg from '../package.json';
type ApplicationProps = {
name: string;
version: string;
}
export const Application = ({ name, version }: ApplicationProps) => {
const ORANGE_COLOR = "#FE9900";
return (
<Box width={80} paddingX={1} flexDirection="column">
<Box
borderStyle="round"
paddingX={1}
borderColor={ORANGE_COLOR}
flexDirection="column"
>
<Box paddingBottom={1}>
<Text bold color={ORANGE_COLOR}>React + Ink Example ApplicationText>
Box>
<Text>{name}Text>
<Text>version: {version}Text>
Box>
Box>
)
}
try {
const { waitUntilExit } = render(
<Application
name={pkg.name}
version={pkg.version}
/>
)
await waitUntilExit();
} catch(error) {
console.error({ status: 'app exited with error', error });
exit(1);
}
Por fim, basta executar os scripts build
e start
para ter a aplicação funcionando:
Conclusão
A partir desse ponto, não existe mais nada muito específico do ink: você pode utilizar o que já conhece de react para construir a aplicação.
Talvez seja útil dar um passo atrás e ler este guideline sobre como criar aplicações CLI com qualidade (escrito por autores de ferramentas populares, como docker compose).
Um outro componente que pode ser útil é um parser para os argumentos, bem como geração de documentação (o famoso --help
). Uma das soluções mais completas é o Commander, que abstrai grande parte desse trabalho.