Metadata-Version: 2.5
Name: shade-cli
Version: 0.1.10
Summary: O comando `shade`: cria, ensaia, testa e empacota robôs da ShadeOne escritos com o shade-sdk.
Author: ShadeOne
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: keyring>=25.7
Requires-Dist: pathspec>=1.1
Provides-Extra: mcp
Requires-Dist: jsonschema>=4.20; extra == 'mcp'
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
Description-Content-Type: text/markdown

# shade-cli

O comando `shade`: cria, ensaia, testa, empacota e envia robôs da ShadeOne escritos com o
`shade-sdk`. O ciclo local roda sem conta; `auth`, `deploy`, `releases` e `runs` falam com a
plataforma pela chave de API, e `shade mcp` expõe o ciclo a um agente (Claude Code). Nunca importa o runtime no
próprio processo: o projeto roda em subprocesso, no venv dele (`uv run` na pasta), com o
`shade-sdk` que ele fixa.

```bash
uv tool install shade-cli
shade init cadastro-erp          # starter de produção (--minimo para o enxuto)
cd cadastro-erp
uv run playwright install chromium
shade test                       # lint de contrato + pytest do projeto
shade run                        # ensaio: para antes de cada efetivar()
shade build                      # artefato determinístico em .shade/build/
```

## Comandos

| Comando | Faz |
|---|---|
| `shade init <pasta> [--minimo] [--modelo lista\|tarefa] [--editor vscode] [--id ID] [--nome NOME] [--sem-sync]` | o starter (produção por padrão) e o `uv sync`; nunca pergunta |
| `shade run [pasta] [--real [--sim]] [--entrada ARQ] [--item CHAVE] [--debug] [--trace] [--como-plataforma] [--headless]` | ensaio por padrão; `--real` pede TTY ou `--sim` |
| `shade test [pasta]` | lint (L1 a L8) e o pytest do projeto |
| `shade build [pasta]` | `projeto.tar.gz` (o mesmo pacote que a plataforma monta de uma gravação) e `manifesto.json` em `.shade/build/`; recusa o plugin de `[plugins]` que a imagem não tem, ou cujos hosts e credenciais o projeto não declara |
| `shade doctor [pasta]` | Python, uv, Chromium do Playwright, Docker e o `shade-sdk` do projeto |
| `shade docs [tópico] [--pasta P]` | a referência da versão do `shade-sdk` do projeto |
| `shade inspecionar <url> [--pagina Nome] [--pasta P]` | abre a página no Chromium (contexto efêmero, rede só em `[hosts] permitidos`), grava o que você clica e preenche, e propõe `Pagina` com `Alvo`s já conferidos pelo diagnóstico do SDK; Enter no terminal encerra (`--headless` varre a página sem janela) |

### Com a chave de API

| Comando | Faz |
|---|---|
| `shade auth login [--url URL]` | lê a chave sem eco (ou de uma linha do stdin, sem terminal), confere com a plataforma e guarda URL, chave e workspace no keyring do sistema |
| `shade auth status` | de onde vem a conta, o workspace e os escopos da chave, conferidos agora |
| `shade auth logout` | apaga a conta do keyring |
| `shade deploy [pasta] [--criar] [--nao-esperar] [--prazo S]` | confere `/sdk/compat`, monta o artefato em memória, envia e acompanha `recebida → conferida → em ensaio → aguardando aprovação`; o `robo.id` novo pede `--criar` sem terminal |
| `shade releases list [pasta] [--robo ID] [--limite N]` | as versões do robô |
| `shade releases show <id> [--esperar] [--prazo S]` | uma versão; com `--esperar`, acompanha como o `deploy` |
| `shade releases ensaiar <id> [--esperar] [--prazo S]` | repete o ensaio que caiu por infraestrutura ou esperava credencial |

`SHADE_API_KEY` e `SHADE_URL` vencem o keyring (o caminho do CI). A chave nunca é argumento,
nunca vai para arquivo e sai redigida de todo texto. `http://` só para `localhost`.

O deploy é idempotente: o mesmo código dá o mesmo `sha256`, e o servidor devolve a release que
já existe. Se a CLI cair no meio, repetir `shade deploy` (ou `shade releases show <id>
--esperar`) retoma a mesma versão. O prazo da espera (10 min) sai 6 com esse comando.

Tudo o que a CLI escreve num projeto fica em `.shade/` (que se ignora sozinho no git): o
`git status` do projeto continua limpo depois de qualquer comando, menos o `init`.

## Servidor MCP (`shade mcp`)

`shade mcp [--pasta P]` sobe um servidor MCP local, por stdio, sem porta, para o Claude Code (ou
outro cliente MCP) escrever, testar e enviar o robô com ferramentas observáveis. Vem num extra:

```bash
uv tool install 'shade-cli[mcp]'
cd cadastro-erp
claude mcp add shade -- shade mcp
```

O projeto é o `shade.toml` mais próximo do diretório em que o cliente sobe o servidor (ou
`--pasta`), fixo na sessão. A chave vem do keyring (`shade auth login`) ou de `SHADE_API_KEY`
e `SHADE_URL`. As ferramentas listadas são as que a chave e o projeto permitem: sem projeto, só
as da plataforma; sem chave, só as locais; chave só de leitura, nada que envie.

| Risco | Ferramentas | Precisa |
|---|---|---|
| local, sem rede | `ler_manifesto`, `montar_artefato`, `rodar_lint` | o projeto |
| leitura da plataforma | `listar_robos`, `ver_release`, `listar_runs`, `ver_run`, `listar_itens`, `ver_falha`, `buscar_eventos`, `ler_gravacao` | `processes:read` ou `runs:read` |
| navegação local | `snapshot_da_pagina` (árvore de acessibilidade e `Alvo`s candidatos), `conferir_seletor` (em que elemento um `Alvo` cai) | o projeto |
| executa código local | `rodar_testes` (`shade test`), `rodar_ensaio` (`shade run` em ensaio, headless) | o projeto |
| envia sem pessoa | `enviar_release` (`shade deploy`, até `aguardando_aprovacao`) | o projeto e `releases:write` |

Recursos: `shade://sdk/referencia`, `shade://projeto/manifesto`, `shade://projeto/ultimo-ensaio`,
`shade://protocolo/runtime` e `shade://ferramentas/schema`; cada um tem o equivalente em
ferramenta ou em `shade docs`.

O que o servidor garante, e o que não garante:

- **Nenhuma ferramenta roda o robô em modo real, aprova nem ativa uma versão.** O ensaio para
  antes de cada `efetivar()`; o `--real` fica no terminal, com você.
- **A resposta é o documento do `--json`**, com `comando` = o nome da ferramenta, redigida pelo
  mesmo serializador: cada valor do `.env`, das credenciais declaradas, da chave e de variável
  com nome de segredo sai como `[redigido]`, também URL-encoded e em base64. Nenhuma ferramenta
  lê `.env`, keyring, cookie ou o conteúdo de um trace; a captura da falha sai como caminho,
  tamanho e sha256.
- **O texto que veio do cliente** (o log do run, a página, a gravação) chega só em
  `dados.cliente` (`{"dados_do_cliente": true, "origem", "truncado", "conteudo"}`), sem
  caractere de controle e com teto de 16 KiB e 200 linhas. É rótulo e teto, não barreira contra
  injeção: o modelo lê os mesmos tokens. Ids, estados e contagens ficam no topo, conferidos pela
  forma. Na gravação, todo valor digitado sai como `[digitado]`.
- **Caminhos** são relativos ao projeto e presos a ele (`..`, absoluto, symlink para fora e
  laço caem); `.env*`, `.git/`, `*.pem` e `.shade/credenciais` são recusados. Leitura até
  256 KiB por arquivo, listagens até 500 entradas.
- **O navegador** é sempre um Chromium headless num contexto novo (nunca o seu perfil, sem
  service worker, sem download, sem o storage de outra chamada), com a rede só em `[hosts]
  permitidos`, em HTTP e WebSocket, salto a salto nos redirecionamentos. IP privado, loopback,
  link-local, `localhost` e `*.local` só passam listados por inteiro. O que a página pediu fora
  da lista volta em `bloqueados`. Risco que fica: o DNS rebinding de um nome da lista.

## CI

A mesma sequência em qualquer CI: instalar a CLI numa versão **exata**, `shade test`, `shade
deploy`. A chave vem de `SHADE_API_KEY`, num segredo do CI e no env do job, nunca em argumento nem
em arquivo. Sem terminal a CLI nunca pergunta (`--criar` diz que o robô novo é de propósito), e o
`--json` põe o resultado no stdout. A release fica aguardando a aprovação de uma pessoa: o CI não
ativa nada. A saída 6 (prazo da espera) não é falha: a release segue no servidor e o
`erro.recuperacao` do JSON traz `shade releases show <id> --esperar`.

**GitHub.** A Action `shade-deploy` (`.github/actions/shade-deploy/`) roda essa sequência e devolve
os outputs `release_id`, `project_sha256` e `estado`, lidos do JSON. O `shade init` já gera o
workflow com ela (job `publicar`, na branch padrão):

```yaml
- uses: SamuelPerroni/shade-one/.github/actions/shade-deploy@cli-v0.1.6
  env:
    SHADE_API_KEY: ${{ secrets.SHADE_API_KEY }}
  with:
    versao-da-cli: "0.1.6"            # exata; sem latest
    # pasta: robos/cadastro            # onde está o shade.toml (padrão: a raiz)
    # url-da-plataforma: https://...   # sem ela, vale o SHADE_URL ou o padrão da CLI
```

O repositório do ShadeOne é privado: para outro repositório usar a Action, libere o acesso em
*Settings > Actions > General > Access* do repositório do ShadeOne.

**GitLab.**

```yaml
publicar:
  image: ghcr.io/astral-sh/uv:python3.14-bookworm
  script:
    - uv tool install shade-cli==0.1.6
    - export PATH="$(uv tool dir --bin):$PATH"
    - uv sync --locked
    - shade test --json
    - shade deploy --json --criar | tee deploy.json
    - python3 -c "import json; print(json.load(open('deploy.json'))['dados']['estado'])"
  variables:
    SHADE_API_KEY: $SHADE_API_KEY      # variável mascarada do projeto
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```

**Azure Pipelines.**

```yaml
- script: |
    pip install uv
    uv tool install shade-cli==0.1.6
    export PATH="$(uv tool dir --bin):$PATH"
    uv sync --locked
    shade test --json
    shade deploy --json --criar
  displayName: Publicar o robô
  env:
    SHADE_API_KEY: $(SHADE_API_KEY)    # variável secreta: mapeie-a em env, o Azure não a expõe sozinho
```

Nos dois, o código de saída é o do `shade`: 1 gate reprovado, 3 chave, 6 prazo (retome com o
comando do JSON), 7 rede. Trate o 6 como aviso se quiser o comportamento da Action.

## `--json` e códigos de saída

Com `--json`, um documento no stdout; progresso e logs no stderr, redigidos:

```json
{"versao": 1, "comando": "build", "ok": false, "dados": {},
 "erro": {"codigo": "lint_reprovado", "mensagem": "L3 robo.py:1 import fora do lock: yaml",
          "recuperacao": "corrija o que o lint aponta (shade test mostra a lista inteira)"}}
```

| Código | Significado |
|---|---|
| 0 | ok (inclusive o deploy que acha a release existente, e a release que espera uma pessoa: aprovar ou preencher o cofre) |
| 1 | gate, lint ou teste reprovou; release reprovada na conferência ou no ensaio (e o defeito da própria CLI, com `erro.codigo = "erro_interno"`) |
| 2 | uso inválido ou `shade.toml` inválido; robô novo sem `--criar` |
| 3 | chave ausente, recusada ou sem o escopo (401/403) |
| 4 | conflito de estado (409): ensaiar uma release reprovada, a release arquivada |
| 5 | incompatibilidade de versão: o `shade-sdk` do projeto não é o da imagem, ou o `/sdk/compat` não casa |
| 6 | o prazo da espera acabou; a release segue no servidor (`erro.recuperacao` traz a retomada) |
| 7 | rede ou servidor indisponível (5xx, 429) |
| 130 | interrompido (Ctrl-C, SIGTERM ou SIGHUP: o robô em andamento morre junto) |

## Lint de contrato

| Regra | Efeito | O quê |
|---|---|---|
| L3 | reprova | import de pacote fora do lock da imagem e dos plugins da imagem que o projeto declara em `[plugins]` |
| L7 | reprova (avisa no `run`) | credencial lida sem declaração, ou o valor dela chegando a `goto`, ao HTTP do Playwright, a `shade_sdk.http` ou a arquivo |
| L8 | reprova | entrada ausente ou inválida (`main.py`, `[robo] classe`, arquivo que não compila) |
| L1 | avisa | `subprocess`, `os.system`, `os.exec*`, `os.fork`, `ctypes`, `multiprocessing` |
| L2 | avisa | `socket`, `http.client`, `urllib.request`, `requests`, `httpx`, `aiohttp` |
| L4 | avisa | `eval`, `exec`, `compile`, import dinâmico |
| L5 | avisa | caminho literal fora do workdir |
| L6 | avisa | clique ou envio em alvo com cara de submissão fora de `efetivar()` |

O lint não é controle de segurança: quem isola o robô é o executor da plataforma.

## Desenvolvimento

```bash
cd cli && uv sync
uv run pytest -q -m "not starter"   # rápido
uv run pytest -q -m starter         # os starters de verdade: uv sync, Chromium, ~45 s
```

`src/shade_cli/imagem.py` é a cópia do `requirements.lock` da imagem do run:
`backend/tests/test_hu294_artefato_da_cli.py` a confere, e confere que o artefato do
`shade build` passa no `ler()` do backend.
