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 Application</Text> </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 Application</Text> </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.
Top comments (0)