sagadeck 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Você (ou uma IA) escreve um arquivo `.yaml` com o conteúdo. O sagadeck gera:
9
9
  | **`.pdf`** | um slide por página, para mandar depois da palestra |
10
10
  | **`- roteiro.pdf`** | roteiro do apresentador: miniatura de cada slide + fala + interações + relógio planejado |
11
11
 
12
- Ele vem com 11 temas (incluindo estilo Keynote Apple em prata clean e respirável, artesanal desenhado à mão, azul elétrico vivo e pop alegre), 23 layouts (incluindo KPIs e fluxos de processo), 2.100+ ícones, pictogramas e cenas geradas na hora, diagramas, 7 tipos de gráfico, e um **fiscal automático** que detecta texto estourado, sobreposição, contraste baixo e slides com texto demais ("anti-sono").
12
+ Ele vem com 11 temas (incluindo prata limpo e respirável, artesanal desenhado à mão, azul elétrico vivo e pop alegre), 23 layouts (incluindo KPIs e fluxos de processo), 2.100+ ícones, pictogramas e cenas geradas na hora, diagramas, 7 tipos de gráfico, e um **fiscal automático** que detecta texto estourado, sobreposição, contraste baixo e slides com texto demais ("anti-sono").
13
13
 
14
14
  ## Instalação
15
15
 
@@ -24,11 +24,14 @@ Requer **Node.js 18+** e **Chrome ou Edge** instalados (o PDF, o PNG e as confer
24
24
  ```bash
25
25
  sagadeck studio
26
26
  ```
27
- e abra **http://127.0.0.1:3517** no navegador. **Essa é a página do sagadeck**: a biblioteca das suas
28
- apresentações, o editor (estilo PowerPoint) e o chat com a IA. Não há outra interface para abrir nem para criar.
29
-
30
- Para usar a IA (gerar apresentações, conversar com o chat), clique em **Configurar IA** no alto da biblioteca (ou
31
- em **IA desligada**, no editor), escolha o provedor, cole a chave, clique em **Testar** e **Salvar**. Detalhes em
27
+ O navegador abre sozinho em **http://127.0.0.1:3517** (se não abrir, abra esse endereço). **Essa é a página do
28
+ sagadeck**: a biblioteca das suas apresentações, o editor (estilo PowerPoint) e o chat com a IA. Não há outra interface
29
+ para abrir nem para criar. Deixe o terminal aberto enquanto usa; `--sem-navegador` não abre o navegador.
30
+
31
+ Para usar a IA (gerar apresentações, conversar com o chat), clique em **Configurar IA** (a biblioteca mostra uma faixa
32
+ enquanto ela não está configurada). **Usar a recomendação** preenche o OpenRouter com modelos que funcionam; cole a
33
+ chave, clique em **Testar** e **Salvar**. Se nem a recomendação responder, o problema é a rede ou a chave. Depois,
34
+ troque pelo provedor e pelos modelos que quiser. Detalhes em
32
35
  [IA de verdade](#ia-de-verdade-llm).
33
36
 
34
37
  ## Como usar
@@ -252,8 +255,11 @@ Para ver funcionando sem configurar nada:
252
255
  O painel **Formatar** edita o endereço, o corpo em JSON, o modo (síncrono, polling, streaming, tempo real)
253
256
  e o que guardar para os próximos slides.
254
257
  - **Inserir → Ambientes:** onde ficam os endereços (`{{base}}`), tokens e segredos do *seu* serviço
255
- (dev, hom, prod). É gravado em `~/.sagadeck/ambientes.yaml`, na sua máquina, nunca no deck. O selo no
256
- slide (DEV, HOM, ENSAIO…) troca o ambiente na hora.
258
+ (dev, hom, prod). Preenche-se por campos: endereço base, variáveis, segredos (guardados cifrados) e a
259
+ autenticação (nenhuma, token fixo ou token que expira), com **Testar**; quem prefere edita o YAML na aba
260
+ "Como texto". É gravado em `~/.sagadeck/ambientes.yaml`, na sua máquina, nunca no deck. Sem ambiente seu, o
261
+ **ENSAIO** (API de mentira) já funciona. O selo no slide (DEV, HOM, ENSAIO…) troca o ambiente na hora. No
262
+ servidor multiusuário o botão não aparece (lá os slides de API não executam).
257
263
 
258
264
  Executar só funciona no Studio local; no HTML exportado e no modo multiusuário, o slide mostra a última
259
265
  resposta gravada. Todos os campos estão na [referência](docs/REFERENCIA.md#slide-api-requisição-ao-vivo).
package/SKILL.md CHANGED
@@ -12,12 +12,13 @@ O sagadeck gera apresentações profissionais a partir de um arquivo YAML, ofere
12
12
  Quando a pessoa pede para instalar o sagadeck e deixar pronto:
13
13
 
14
14
  1. `npm install -g sagadeck` (Node 18+; não precisa de Python).
15
- 2. `sagadeck studio` e diga para ela abrir **http://127.0.0.1:3517** (a primeira linha que o comando escreve é
16
- esse endereço). **O Studio é a interface do sagadeck**: biblioteca, editor e chat com IA.
15
+ 2. `sagadeck studio`: o navegador abre sozinho em **http://127.0.0.1:3517** (é também a primeira linha que o
16
+ comando escreve; `--sem-navegador` não abre). **O Studio é a interface do sagadeck**: biblioteca, editor e chat com
17
+ IA. O terminal fica aberto enquanto ela usa.
17
18
  3. **Nunca crie uma página, um servidor ou uma interface própria** para o sagadeck, nem "suba as APIs" à mão: tudo
18
19
  que a pessoa precisa está no Studio.
19
20
  4. Para a IA, mostre onde configurar: **Configurar IA** no alto da biblioteca (ou **IA desligada** no editor):
20
- provedor, chave, Testar e Salvar. A configuração fica em `~/.sagadeck/ia.json`; nunca grave a chave no código nem
21
+ **Usar a recomendação** (OpenRouter com modelos que funcionam), a chave, Testar e Salvar. A configuração fica em `~/.sagadeck/ia.json`; nunca grave a chave no código nem
21
22
  na pasta das apresentações.
22
23
  5. As apresentações moram na biblioteca (`SAGADECK_HOME`, senão Documentos\sagadeck no Windows e `~/sagadeck` fora
23
24
  dele). Não crie decks em outra pasta.
@@ -119,7 +120,7 @@ O agente no terminal deve seguir o fluxo:
119
120
  ## Decisão de Arquitetura: Mesmo Repositório vs. Repositório Separado
120
121
 
121
122
  ### Por que o Estúdio deve ficar no **mesmo repositório** (`sagadeck`):
122
- 1. **Fonte Única da Verdade**: Os layouts (incluindo `stats` para KPIs e `steps` para processos), os 11 temas visuais (incluindo `prata` estilo Keynote Apple, `rabisco` artesanal pintado à mão, `oceano` azul elétrico vivo, `pop` alegre/chiclete e `aurora` neon), as regras de tipografia e as rotinas de verificação geométrica residem no mesmo código (`src/layouts.js`, `src/themes.js`, `src/fiscal/autofix.js`). Qualquer alteração ou novo layout adicionado ao compilador fica disponível **instantaneamente** no estúdio visual.
123
+ 1. **Fonte Única da Verdade**: Os layouts (incluindo `stats` para KPIs e `steps` para processos), os 11 temas visuais (incluindo `prata` limpo e respirável, `rabisco` artesanal pintado à mão, `oceano` azul elétrico vivo, `pop` alegre/chiclete e `aurora` neon), as regras de tipografia e as rotinas de verificação geométrica residem no mesmo código (`src/layouts.js`, `src/themes.js`, `src/fiscal/autofix.js`). Qualquer alteração ou novo layout adicionado ao compilador fica disponível **instantaneamente** no estúdio visual.
123
124
  2. **Zero Descompasso de Versões**: Em repositórios separados, quando o formato YAML ou os temas do SagaDeck evoluem, a interface visual quebra ou fica desatualizada até que alguém publique e atualize dependências externas.
124
125
  3. **Experiência de Uso (DX) Imediata**: O desenvolvedor ou agente clona o repositório ou roda `npm install -g sagadeck` e tem **tudo num só lugar**: linha de comando (`sagadeck build`), estúdio visual (`sagadeck studio`), servidor de agentes (`sagadeck mcp`) e fiscal (`sagadeck check`).
125
126
  4. **Leve e Modular**: A interface web foi construída com tecnologias nativas leves (Vanilla JS, CSS moderno e servidor HTTP embutido do Node.js sem frameworks pesados). Isso mantém o pacote pequeno (~1.1 MB empacotado) sem impactar o desempenho do compilador.
@@ -129,7 +130,7 @@ O agente no terminal deve seguir o fluxo:
129
130
  ## Novos Temas Visuais & Recursos Anti-Textão
130
131
 
131
132
  ### 1. Temas Modernos, Vivos, Jovens e Clean
132
- - **`prata`**: **Estilo Keynote Apple**. Jovem, clean, respirável e sofisticado (levado a sério). Fundo cinza prata acetinado (`#F5F5F7`), texto em ardósia/preto profundo (`#1D1D1F`), cinza titânio intermediário (`#86868B`), divisores finos (`#D2D2D7`) e azul Apple (`#0071E3`). Cartelas com efeito vidro fosco / acabamento acetinado com sombra ultra-suave e tipografia San Francisco / Inter.
133
+ - **`prata`**: **Limpo e espaçoso**. Jovem, clean, respirável e sofisticado (levado a sério). Fundo cinza prata acetinado (`#F5F5F7`), texto em ardósia/preto profundo (`#1D1D1F`), cinza titânio intermediário (`#86868B`), divisores finos (`#D2D2D7`) e azul vivo (`#0071E3`). Cartelas com efeito vidro fosco / acabamento acetinado com sombra ultra-suave e tipografia do sistema / Inter.
133
134
  - **`rabisco`**: Estilo artesanal e lousa com fontes desenhadas à mão (`'Caveat'`, `'Patrick Hand'`, `'Ink Free'`), bordas orgânicas de caderno, sombras desenhadas, fitas adesivas/post-its e ícones em estilo doodle. Ideal quando se quer uma apresentação calorosa, amigável e ilustrada.
134
135
  - **`oceano`**: Azul elétrico ultra vibrante (`#0062FF`), ciano neon (`#00C6FF`), tipografia moderna punchy e fundo luminoso de alta energia. Perfeito para tech, startups e produtos dinâmicos.
135
136
  - **`pop`**: Alegre, colorido e chiclete com paleta super animada (roxo, rosa chiclete, menta, sol) e cantos ultra-arredondados (26px).
@@ -145,7 +146,7 @@ Para apresentações de alto impacto sem "muralhas de texto":
145
146
  - **Sem bloqueios de robô**: Modelos de IA não devem tentar raspar o Google via navegadores headless (o Google bloqueia com CAPTCHA/bot detection). O SagaDeck inclui pesquisa nativa via DuckDuckGo com `sagadeck search "<termo>"` e a ferramenta MCP `sagadeck_web_search`.
146
147
  - **Controle Flexível de Fontes**:
147
148
  - **Quando o usuário pedir fontes citadas**: Inclua `source: "Fonte: Nome do Relatório, Ano"` nos slides com métricas (`stats`, `number`, `chart`, `split`) e a URL completa no roteiro do apresentador (`notes:`).
148
- - **Quando o usuário pedir sem fontes (ou estilo clean/Apple)**: Omita o campo `source:`, mantendo o slide limpo e desobstruído.
149
+ - **Quando o usuário pedir sem fontes (ou estilo clean)**: Omita o campo `source:`, mantendo o slide limpo e desobstruído.
149
150
  - **Garantia para Modelos Econômicos**: Qualquer modelo (inclusive os menores como Haiku, Flash ou GPT-4o-mini) deve sempre finalizar com o comando `sagadeck autofix <deck.yaml>`. Isso corrige problemas cobertos pelas regras; depois confira a renderização, porque não garante perfeição geométrica ou visual.
150
151
 
151
152
  ## Recursos recentes para autoria por IA
package/bin/sagadeck.js CHANGED
@@ -40,13 +40,19 @@ for (let i = 0; i < rawRest.length; i++) {
40
40
 
41
41
  const HELP = `sagadeck — YAML -> apresentação (HTML animado + PowerPoint editável + PDF + roteiro)
42
42
 
43
+ Para começar: sagadeck studio
44
+ abre o Studio no navegador (biblioteca, editor e chat com IA; http://127.0.0.1:3517).
45
+ Deixe o terminal aberto enquanto usa. A IA se configura lá, em Configurar IA.
46
+
47
+ Todos os comandos:
48
+
43
49
  sagadeck new <nome> --prompt "briefing" [--slides=10] [--duration=20] [--theme=x] deck inteiro escrito pelo LLM
44
50
  (pesquisa na web quando o pedido precisa de informação recente ou de um artigo; --sem-pesquisa desliga)
45
51
  (--duration dispensa --slides: ~1 slide a cada 1,5 min)
46
52
  (imagens: peça no briefing; --images = "você decide onde ilustrar", --no-images = nenhuma)
47
53
  sagadeck napkin <texto|arquivo> [-o deck.yaml] [--rules] texto bruto -> slide visual (LLM se houver; --rules força as regras)
48
54
  sagadeck imagens <deck.yaml> gera as imagens pedidas com image_prompt: no YAML (modelo de imagem)
49
- sagadeck scaffold <nome> [--theme=prata] [--type=pitch|keynote|palestra] gera esqueleto narrativo pronto (economiza 80% de tokens)
55
+ sagadeck scaffold <nome> [--theme=prata] [--type=lancamento|pitch|palestra] gera esqueleto narrativo pronto (economiza 80% de tokens)
50
56
  sagadeck new <nome> [--topic=T] [--theme=x] cria um deck de exemplo NA BIBLIOTECA (<biblioteca>/<tópico>/<nome>/)
51
57
  sagadeck build <deck.yaml> gera <deck>.html (abre no navegador; P = modo apresentador)
52
58
  sagadeck check <deck.yaml> procura texto estourado, sobreposição, contraste, excesso de texto
@@ -56,7 +62,7 @@ const HELP = `sagadeck — YAML -> apresentação (HTML animado + PowerPoint edi
56
62
  sagadeck roteiro <deck.yaml> gera <deck> - roteiro.pdf (miniaturas + notas + tempos)
57
63
  sagadeck estudo <deck.yaml> [--html] gera <deck> - material de estudo.pdf (cada slide + texto de consulta)
58
64
  sagadeck all <deck.yaml> build + check + pptx + pdf + roteiro
59
- sagadeck studio [deck.yaml|x.sagadeck] [--port=3517] [--library=PASTA] sem arquivo: abre a biblioteca; já aberto: usa o aberto
65
+ sagadeck studio [deck.yaml|x.sagadeck] [--port=3517] [--library=PASTA] [--sem-navegador] sem arquivo: abre a biblioteca (no navegador); já aberto: usa o aberto
60
66
  (PASTA padrão: SAGADECK_HOME, senão Documentossagadeck no Windows, ~/sagadeck fora dele); com arquivo: abre o editor dele
61
67
  --host=0.0.0.0 abre para a rede (padrão: só esta máquina)
62
68
  --multiuser: uma biblioteca por usuário, atrás de um proxy que envia X-Sagadeck-User
@@ -196,7 +202,7 @@ async function main() {
196
202
  const { generateScaffold } = await import("../src/templates/scaffold.js");
197
203
  const YAML = (await import("yaml")).default;
198
204
  const theme = flags.theme || "prata";
199
- const type = flags.type || "keynote";
205
+ const type = flags.type || "lancamento";
200
206
  const title = flags.title || (args[0] ? path.basename(args[0], path.extname(args[0])) : "Nova Apresentação");
201
207
  const author = flags.author || "Seu Nome";
202
208
  const spec = generateScaffold({ title, theme, type, author });
@@ -387,6 +393,7 @@ async function main() {
387
393
  else { console.error(`O Studio já está aberto em http://${shownHost}:${port} com a biblioteca ${library}, e este deck está fora dela. Use --port=OUTRA para abri-lo à parte.`); process.exit(1); }
388
394
  }
389
395
  console.log(`✓ O SagaDeck Studio ${found.version} já está aberto (processo ${found.pid}): ${url}`);
396
+ { const B = await import("../src/studio/open-browser.js"); if (B.shouldOpenBrowser({ flags, multiuser })) B.openBrowser(url); }
390
397
  break;
391
398
  }
392
399
  if (decision.action === "conflict") {
@@ -403,6 +410,9 @@ async function main() {
403
410
  server.listen(port, host, async () => {
404
411
  const { studioBanner } = await import("../src/studio/banner.js");
405
412
  for (const line of studioBanner({ version: VERSION, url: `http://${shownHost}:${port}/`, library, ai: (await import("../src/ai/llm.js")).llmConfig(), multiuser, usersDir: path.join(library, "usuarios"), isLibrary: !deckFile })) console.log(line);
413
+ // depois do endereço (a primeira linha é sempre ele): abre o navegador na página
414
+ const B = await import("../src/studio/open-browser.js");
415
+ if (B.shouldOpenBrowser({ flags, multiuser }) && B.openBrowser(`http://${shownHost}:${port}/`)) console.log(" (abrindo no navegador; --sem-navegador desliga)");
406
416
  });
407
417
  break;
408
418
  }
package/docs/ANDAMENTO.md CHANGED
@@ -12,10 +12,10 @@ começar. Trabalho em curso vai para o GitHub (commit + push) a cada etapa, nunc
12
12
 
13
13
  **Para quem pega isto numa sessão nova, sem a conversa de origem.** O projeto é o **sagadeck**:
14
14
  - Repositório GitHub `naruminho/sagadeck`, clonado em `C:\Users\narum\src\sagadeck`, branch `main`.
15
- - Publicado no npm como `sagadeck` (1.6.0: IA embutida, só npm; o PyPI parou na 1.5.0).
15
+ - Publicado no npm como `sagadeck` (1.7.1: a IA diz por que o `ia.json` não serve e, no servidor, não mostra a configuração; o PyPI parou na 1.5.0).
16
16
  - Ferramenta Node (ESM) que transforma apresentações escritas em YAML em HTML/PPTX.
17
17
  - Tem um editor web, o Studio (`sagadeck studio`, porta 3517).
18
- - Tem uma IA (via modelrelay em `http://127.0.0.1:8765/v1`) que cria e edita decks, inclusive a partir de PDFs de
18
+ - Tem uma IA embutida (provedor direto, configurado em `~/.sagadeck/ia.json` pela tela Configurar IA) que cria e edita decks, inclusive a partir de PDFs de
19
19
  artigos científicos para apresentar em congresso.
20
20
 
21
21
  Os itens abaixo vieram de duas rodadas de 08/10/2026, ambas descritas nas seções logo abaixo deste diário:
@@ -42,6 +42,42 @@ datadas abaixo). O resto fica para depois e só entra se for pedido. Antes de co
42
42
  ler o conteúdo força o download. Conferir se listar a biblioteca (`openLibrary`, `/api/library`) lê o YAML de
43
43
  cada deck; se lê, listar só pelo nome/metadado e abrir o conteúdo sob demanda.
44
44
 
45
+ ## Sem citar o programa de apresentações da maçã (1.7.1) — 10/10/2026
46
+
47
+ Pedido de Narumi: o código e a documentação não citam o programa de apresentações da maçã, a empresa nem os
48
+ aparelhos dela. O tema `prata` passa a se descrever como "limpo e espaçoso"; o `aluminio`, como "lançamento de
49
+ aparelho premium". O exemplo virou `templates/exemplo-prata.yaml`, e o tipo padrão do `scaffold` (CLI e MCP) virou
50
+ `lancamento` (tipo desconhecido continua caindo nele). Saiu também o slogan da empresa do esqueleto. Ficam só os nomes
51
+ técnicos (a fonte do sistema no CSS, a identificação de navegador da pesquisa). `guards.test.js` falha se voltar
52
+ (olha os arquivos do repositório, menos bibliotecas de terceiros); regra no `CLAUDE.md`.
53
+
54
+ ## No servidor, a configuração da IA não sai para quem usa (1.7.1) — 10/10/2026
55
+
56
+ No multiusuário (portal do Oracle), `/api/ia` e `/api/ai/status` mostravam a qualquer usuário o provedor, o endereço,
57
+ os modelos, o caminho do `ia.json` e os 4 últimos caracteres da chave, embora ele não pudesse editar nada.
58
+
59
+ - `/api/ia` no multiusuário devolve só `{ editable: false, configured }`; `/api/ai/status`, só `{ available,
60
+ configured, server: true }`.
61
+ - O Configurar IA, no servidor, vira um aviso: quem configura é quem administra, e se a IA está configurada. O
62
+ indicador do editor diz "A IA deste servidor está ligada" (ou "não está respondendo / não está configurada. Avise
63
+ quem administra"), sem endereço nem modelo; o progresso do chat diz "Enviando para a IA".
64
+ - Teste (`test/library-ui.test.js`): com a IA configurada no servidor, nada de chave, modelo, endereço ou arquivo nas
65
+ respostas nem na página do editor; o diálogo não tem campos. Falha sem a mudança.
66
+
67
+ ## ia.json ilegível diz o motivo — 10/10/2026
68
+
69
+ Na migração do Oracle (o modelrelay saiu; os serviços `sagadeck` e `sagadeck-dev` falam direto com o OpenRouter, cada
70
+ um com seu `ia.json`), o arquivo foi criado como root e o serviço, que roda com usuário próprio, não conseguia lê-lo:
71
+ o sagadeck dizia só "IA não configurada", como se o arquivo nem existisse.
72
+
73
+ - `readIA` (`src/ai/ia-config.js`) separa "não existe" (só não configurada) de "existe e não serve": sem permissão,
74
+ ilegível, JSON quebrado, sem `url`. O motivo vai em `llmConfig().problem`.
75
+ - Aparece ao subir (`studioBanner`, a linha "IA: não configurada. …", que é o que quem administra vê no log), no erro
76
+ da chamada à IA, em `/api/ia` e `/api/ai/status`, na faixa da biblioteca, no Configurar IA e no indicador do editor.
77
+ - A faixa da biblioteca quebra o texto longo (o caminho do arquivo empurrava o botão para baixo de outro elemento).
78
+ - Testes: `test/ia-config.test.js` (pasta no lugar do arquivo, sem permissão fora do Windows, JSON quebrado, sem
79
+ endereço) e `test/library-ui.test.js` (faixa e Configurar IA dizem "Não deu para ler"). Falham sem a correção.
80
+
45
81
  ## CI verde de novo (e a regra: merge só com a CI verde) — 09/10/2026
46
82
 
47
83
  A CI do GitHub estava vermelha havia dias (desde antes da 1.4.0) e 13 PRs foram mergeados e a 1.5.0 publicada assim.
@@ -108,6 +144,51 @@ os achados para os slides").
108
144
  `missed`, elas entram no inventário com a caixa exata do texto e passam pela mesma releitura do recorte
109
145
  (`transcribeEquation`) que escreve o LaTeX das outras. Saiu da lista.
110
146
 
147
+ ## Primeiro uso: navegador abre sozinho, "Para começar" e a recomendação de IA — 09/10/2026
148
+
149
+ Pergunta de Narumi: quem instala pelo npm sabe pôr no ar, sabe a URL, precisa editar arquivo para a IA? Antes: um
150
+ comando (`sagadeck studio`), mas o navegador não abria, o `sagadeck` sozinho mostrava o `studio` como 19º item de uma
151
+ lista longa, e os campos de modelo do Configurar IA vinham vazios (ninguém sabe o nome exato de um modelo).
152
+
153
+ - `sagadeck studio` **abre o navegador** na página (`src/studio/open-browser.js`), inclusive quando já está aberto;
154
+ só para quem está no terminal: nunca em serviço/teste (sem TTY), CI, multiusuário, Linux sem tela ou
155
+ `--sem-navegador` / `SAGADECK_NO_BROWSER=1`. A primeira linha continua sendo o endereço.
156
+ - `sagadeck` sozinho começa com **"Para começar: sagadeck studio"** (o que faz, o endereço, deixar o terminal aberto).
157
+ - **Configurar IA** com a recomendação (`RECOMMENDED` em `src/ai/ia-config.js`: OpenRouter, DeepSeek para texto e
158
+ visão, Gemini para imagem, `:online` para busca) e **Usar a recomendação**, que preenche provedor e modelos. Ela é
159
+ também o teste da instalação: se nem com ela a IA responde, o problema é rede ou chave. Os campos trazem exemplos e
160
+ a tela diz que imagem e busca dependem do provedor. Sem listar os modelos do provedor (decisão: endereço próprio ou
161
+ de empresa pode não ter a lista, ou ser bloqueado).
162
+ - **Faixa "Configure a IA"** na biblioteca enquanto não está configurada (só onde se pode configurar; some ao salvar).
163
+ - Testes: `test/instance.test.js` (quando abre o navegador e o comando de cada sistema; `sagadeck` sozinho),
164
+ `test/library-ui.test.js` (faixa, recomendação preenche, salvar some a faixa).
165
+ - **Testes isolados de verdade**: a suíte completa falhou só nesta máquina num teste da lixeira, que leu as
166
+ Preferências reais (lixeira em 1 dia). 41 arquivos de teste não carregavam o isolamento (só os que importavam o
167
+ `helpers.js`). Agora `test/isolate.js` (biblioteca, ambientes, Preferências, IA e registro de comandos em pasta
168
+ temporária) é carregado antes de tudo pelo `npm test` (`--import`) e importado por todo arquivo de teste;
169
+ `guards.test.js` reprova arquivo de teste que não carregue.
170
+ - A avaliação ao vivo (`test/bench-live.test.js`) não fechava o navegador da revisão dos slides: o processo ficava
171
+ pendurado (dias). Fecha no fim; `guards.test.js` exige isso de todo teste que gera pelo caminho do Studio.
172
+ ## Ambientes por campos e sem o botão no servidor — 09/10/2026
173
+
174
+ Pergunta de Narumi: quem instala do zero sabe preencher o `ambientes.yaml` (slides de API)? O lugar já era automático
175
+ (o Studio grava em `~/.sagadeck/ambientes.yaml`), mas preencher era editar YAML numa caixa de texto. E no servidor
176
+ (multiusuário) o botão abria a janela com "O serviço recusou o acesso…", parecendo defeito.
177
+
178
+ - **Por campos** (`src/studio/public/api-envs-form.js`, aba padrão): um cartão por ambiente com endereço base,
179
+ variáveis, segredos (digitado vai cifrado; em branco mantém; ou o nome de uma variável de ambiente), autenticação
180
+ (nenhuma; token fixo, que vira `Authorization: Bearer {{secret.token}}`; token que expira, client credentials) e
181
+ **Testar** (salva, chama o endereço base e pede o token). Sem ambiente próprio, a tela explica que o ENSAIO já
182
+ funciona. A aba **Como texto** é o editor de YAML de antes.
183
+ - Motor (`src/api-client.js`): `formState`/`saveForm` (o mesmo arquivo, preservando comentários e o que o
184
+ formulário não conhece: `ca`, outros cabeçalhos), `check` (o Testar), cabeçalhos do ambiente com `{{secret.…}}` e
185
+ client secret do token cifrado. Rotas `/api/http/ambientes/form` e `/api/http/ambientes/testar`.
186
+ - **No servidor o botão some** (o slide de API não executa lá; `/api/http/state` diz `live: false`).
187
+ - Testes: `test/api-client.test.js` (salvar e ler de volta sem o valor dos segredos, comentários e `ca` preservados,
188
+ token fixo no pedido, segredo mantido em branco, Testar), `test/studio.test.js` (instalação do zero pela tela, até o
189
+ Testar confirmar com o token chegando na API), `test/studio-scripts.test.js` (botão some no multiusuário; falhava
190
+ sem o módulo).
191
+
111
192
  ## IA embutida, só npm e o Studio como a página do sagadeck — 09/10/2026
112
193
 
113
194
  Pedido de Narumi, depois de um agente (Copilot) instalar o sagadeck, abrir a página do modelrelay e, sem achar a
@@ -743,8 +824,8 @@ raciocínio, a mesma conferência sai em 1–2 s e acha o que importa (título r
743
824
  resposta parava no meio perdia os itens → os que faltaram vão de novo, um por vez. Respostas que não serviram
744
825
  ficam em `.sagadeck/transform/falhas/` (as 40 últimas).
745
826
  - Casos de 02/10 (decks em `Pedidos de teste/`):
746
- - **Apple prateado**: o `prata` (fiel ao branco da Apple de hoje) achado sem graça. Tema novo **`aluminio`**: a cara
747
- dos lançamentos de iPhone e iPad de 2020-2021 (prata metálico com brilho de estúdio, alumínio escovado, título
827
+ - **Prateado**: o `prata` achado sem graça. Tema novo **`aluminio`**: a cara
828
+ de lançamento de aparelho premium (prata metálico com brilho de estúdio, alumínio escovado, título
748
829
  cromado com a cor sólida em `color` para o fiscal medir, disco de metal polido na capa e na seção, cartões de
749
830
  alumínio com chanfro, seção em azul-pacífico, escuro em grafite).
750
831
  - **história dos videogames em 5 slides, temática, o último como mapa de Mario Kart visto de cima**: saiu no tema
@@ -6,7 +6,7 @@ Na biblioteca, escolha uma experiência para abrir um roteiro pronto e editável
6
6
  |---|---|---|
7
7
  | Sala de decisão | Estratégia e diretoria | Grafite e ouro, gráficos limpos, recomendação → evidências → riscos → decisão |
8
8
  | Matéria de capa | História e investigação | Serifas, papel claro, enquadramentos amplos, pistas e virada narrativa |
9
- | Modo espetáculo | Palco e keynote | Contraste luminoso, escala monumental, pausas, escolhas da plateia e revelação por clique |
9
+ | Modo espetáculo | Palco e palestras | Contraste luminoso, escala monumental, pausas, escolhas da plateia e revelação por clique |
10
10
  | Clube criativo | Workshop | Formas primárias, composição geométrica, desafios e timer |
11
11
  | Laboratório vivo | Programação | Código por etapas, saída ilustrativa, screenshot com focos e previsão antes da resposta |
12
12
  | Essencial | Produto e comunicação objetiva | Espaço vazio, uma ideia por vez, tipografia discreta e comparação |
@@ -282,7 +282,9 @@ No Studio, a aba **Variáveis** mostra o ambiente atual em uma tabela **Nome / V
282
282
  normais ali; nomes e valores são salvos no arquivo de ambientes, nunca no deck. Arraste um nome da tabela
283
283
  para um campo do slide para inserir `{{nome}}`. Segredos são listados pelo nome, mas seu valor nunca aparece.
284
284
  Os campos `save` do deck também aparecem com sua origem e disponibilidade.
285
- O editor de ambientes valida o YAML antes de habilitar **Salvar** e preserva a versão anterior em `ambientes.yaml.bak`.
285
+ O editor de ambientes abre **por campos** (endereço base, variáveis, segredos, autenticação: nenhuma, token fixo
286
+ `Authorization: Bearer {{secret.token}}` ou token que expira; **Testar** chama o endereço base e pede o token). A aba
287
+ **Como texto** valida o YAML antes de habilitar **Salvar**. Os dois preservam a versão anterior em `ambientes.yaml.bak`.
286
288
  O botão **Variáveis** (na barra do slide) é o inspect/watch da apresentação, tudo num lugar só:
287
289
  - **Ambiente**: as variáveis (criar, editar, apagar) e as **protegidas** (marque "protegida" ao criar): na tela só o nome
288
290
  e ••••; o olhinho mostra o valor por 15 segundos. Nos slides e no código gerado aparece o nome (`{{secret.chave}}`),
@@ -334,8 +336,9 @@ texto) para o modo sem Studio.
334
336
 
335
337
  Endereços, credenciais e segredos ficam **na máquina**, nunca no deck (o deck pode ir para o GitHub).
336
338
  Outro lugar: variável `SAGADECK_AMBIENTES`. O selo no slide (DEV, HOM…) troca o ambiente. No Studio,
337
- **Inserir → Ambientes** mostra o arquivo (ou um modelo comentado, se ele ainda não existe), confere o YAML
338
- antes de gravar e troca o ambiente em uso.
339
+ **Inserir → Ambientes** preenche o arquivo por campos (ou mostra o YAML na aba Como texto, com um modelo comentado
340
+ se ele ainda não existe), confere antes de gravar e troca o ambiente em uso. Os cabeçalhos do ambiente (`headers`)
341
+ aceitam `{{secret.nome}}`; o client secret do token pode ficar cifrado (`client_secret: "dpapi:…"` no Windows).
339
342
 
340
343
  ```yaml
341
344
  current: hom
@@ -560,7 +563,7 @@ Linhas que começam com PALAVRAS EM MAIÚSCULAS seguidas de `:` viram etiquetas
560
563
 
561
564
  Como no PowerPoint, são duas escolhas independentes:
562
565
 
563
- - **Tema** (`theme`): fontes, raio das bordas, textura e uma **pele** própria que rearruma capa, seção e encerramento, muda o jeito dos slides de conteúdo (título, cartões, marcadores da lista, citação, número grande, frase) e põe ornamentos (`sinal`: faixa zebrada e selo no chapéu; `editorial`: fios e capa centralizada; `noite`: moldura fina; `bauhaus`: círculo, quadrado e triângulo; `terminal`: janela com barra e prompt; `jornal`: fios de jornal e manchete sublinhada; `rabisco`: fitas adesivas e títulos inclinados; `oceano`: ondas e chapéu em pílula; `pop`: adesivos e sombra chapada; `aurora`: brilhos e traço em gradiente; `prata`: limpo e centralizado (o branco da Apple de hoje); `relevo`: ficção científica sóbria (sala de controle de filme, sem néon): grafite profundo, curvas de nível ao fundo, cantoneiras de mira, título leve embaixo à esquerda, rótulos técnicos em monoespaçada; combina com ilustração 3D realista (terreno, cidade, água) e gráfico 3D (`science` com superfície); `arcade`: videogame e fliperama, tela de tubo com linhas de varredura, neon, título em pixel no meio e chão de tijolos de jogo de plataforma; `aluminio`: prata metálico dos lançamentos de iPhone e iPad de 2020, título cromado embaixo à esquerda e um disco de metal polido; `manual` e `manual-noite`: documentação técnica, sem ornamento, código e texto corrido legíveis — o par claro/escuro do mesmo arranjo, para material de consulta, tutorial e apostila: `manual` imprime bem, `manual-noite` descansa a vista). Nos slides de conteúdo: `sinal` barra de aviso e placas; `editorial` fios finos e citação centralizada; `noite` tudo centralizado e cartões vazados; `bauhaus` faixas de cor primária e círculos; `terminal` título como comando e cartões-janela; `jornal` fios duplos e colunas; `oceano` cartões flutuando e balões; `pop` contorno grosso e sombra dura; `prata` centralizado e sem caixa; `aluminio` cartões de alumínio com chanfro de luz; `relevo` painéis de vidro fosco com traço gelo; `arcade` cartões de 8 bits com borda de pixel e sombra dura; `rabisco` tracejados e números circulados; `aurora` vidro com brilho. Trocar o tema já muda o arranjo: não reescreva slides só para "combinar" com o tema.
566
+ - **Tema** (`theme`): fontes, raio das bordas, textura e uma **pele** própria que rearruma capa, seção e encerramento, muda o jeito dos slides de conteúdo (título, cartões, marcadores da lista, citação, número grande, frase) e põe ornamentos (`sinal`: faixa zebrada e selo no chapéu; `editorial`: fios e capa centralizada; `noite`: moldura fina; `bauhaus`: círculo, quadrado e triângulo; `terminal`: janela com barra e prompt; `jornal`: fios de jornal e manchete sublinhada; `rabisco`: fitas adesivas e títulos inclinados; `oceano`: ondas e chapéu em pílula; `pop`: adesivos e sombra chapada; `aurora`: brilhos e traço em gradiente; `prata`: limpo e centralizado; `relevo`: ficção científica sóbria (sala de controle de filme, sem néon): grafite profundo, curvas de nível ao fundo, cantoneiras de mira, título leve embaixo à esquerda, rótulos técnicos em monoespaçada; combina com ilustração 3D realista (terreno, cidade, água) e gráfico 3D (`science` com superfície); `arcade`: videogame e fliperama, tela de tubo com linhas de varredura, neon, título em pixel no meio e chão de tijolos de jogo de plataforma; `aluminio`: prata metálico de lançamento de aparelho premium, título cromado embaixo à esquerda e um disco de metal polido; `manual` e `manual-noite`: documentação técnica, sem ornamento, código e texto corrido legíveis — o par claro/escuro do mesmo arranjo, para material de consulta, tutorial e apostila: `manual` imprime bem, `manual-noite` descansa a vista). Nos slides de conteúdo: `sinal` barra de aviso e placas; `editorial` fios finos e citação centralizada; `noite` tudo centralizado e cartões vazados; `bauhaus` faixas de cor primária e círculos; `terminal` título como comando e cartões-janela; `jornal` fios duplos e colunas; `oceano` cartões flutuando e balões; `pop` contorno grosso e sombra dura; `prata` centralizado e sem caixa; `aluminio` cartões de alumínio com chanfro de luz; `relevo` painéis de vidro fosco com traço gelo; `arcade` cartões de 8 bits com borda de pixel e sombra dura; `rabisco` tracejados e números circulados; `aurora` vidro com brilho. Trocar o tema já muda o arranjo: não reescreva slides só para "combinar" com o tema.
564
567
  - **Paleta** (`palette`): só as cores, em qualquer tema. `tinta`, `floresta`, `mar`, `entardecer`, `lavanda`, `grafite`, `neon`, `areia`, `cereja`, `corporativo`, `luzquente`, ou as suas: `palette: { paper: "F4F1EA", ink: "161616", accent: "D7263D", alert: "1B998B" }`. Os quatro tons saem dessas quatro cores, com contraste garantido. A paleta troca também o fundo: em tema escuro por natureza (`relevo`, `arcade`, `aurora`, `noite`, `terminal`), uma paleta de papel claro (`safira`, `corporativo`, `tinta`…) deixa o tema claro; para manter o visual escuro que o pedido quer, não ponha paleta (ou use uma de papel escuro, como `neon`).
565
568
  - **Paletas de família** (para identidade de marca sem cansar a vista): a cor forte da marca fica só no detalhe (`alert`, a ênfase pontual) e a página trabalha com os parentes mais agradáveis dela (`accent` e `family`). `rubi` (rosas, magenta e vinho; vermelho só no detalhe), `ametista` (roxos e lilases), `tangerina` (laranja com azul-marinho), `safira` (azuis com um toque âmbar), `esmeralda` (verdes). Na sua: `palette: { paper: "FFFFFF", ink: "3B2B33", accent: "B83A6E", alert: "CC092F", family: ["F9DCE5", "EFA3BC", "D9668F", "7E2349"] }` (de 2 a 8 parentes). `family` pinta as séries extras dos gráficos e os grupos e ramos dos diagramas. Pedido de "cores da empresa X": monte uma paleta de família assim, com a cor forte em `alert`. Para clima futurista de bom gosto (luz quente de prédios e shows de drones, nunca neon barato): `luzquente` — brancos quentes e âmbar que funcionam no claro e, no `tone: dark`, viram HUD âmbar sobre carvão quente.
566
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sagadeck",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Gerador de apresentações: YAML -> HTML animado + PowerPoint editável, com temas e figuras geradas na hora.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,9 +39,9 @@
39
39
  "dev": "node bin/sagadeck.js studio",
40
40
  "build:code-lab": "node scripts/bundle-code-lab.mjs",
41
41
  "test:live": "node tools/test-live.mjs",
42
- "test": "node --test --test-concurrency=1 \"test/*.test.js\"",
43
- "test:unit": "node --test test/engine.test.js",
44
- "test:ui": "node --test --test-concurrency=1 test/runtime.test.js test/studio.test.js"
42
+ "test": "node --import ./test/isolate.js --test --test-concurrency=1 \"test/*.test.js\"",
43
+ "test:unit": "node --import ./test/isolate.js --test test/engine.test.js",
44
+ "test:ui": "node --import ./test/isolate.js --test --test-concurrency=1 test/runtime.test.js test/studio.test.js"
45
45
  },
46
46
  "repository": {
47
47
  "type": "git",
@@ -13,6 +13,12 @@ export const PROVIDERS = {
13
13
  openrouter: { label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" },
14
14
  openai: { label: "OpenAI", url: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY" },
15
15
  };
16
+ // Recomendação para começar, mostrada na tela Configurar IA. Serve também de teste da instalação: se nem com ela a IA
17
+ // responde, o problema é o ambiente (rede, chave), não o modelo escolhido. Depois, cada um põe o modelo que quiser.
18
+ export const RECOMMENDED = {
19
+ provider: "openrouter",
20
+ models: { text: "deepseek/deepseek-v4.1-flash", vision: "deepseek/deepseek-v4.1-flash", image: "google/gemini-3.1-flash-image", search: "deepseek/deepseek-v4.1-flash:online" },
21
+ };
16
22
  // os papéis que o sagadeck usa; cada um aponta para um modelo do provedor
17
23
  export const ROLES = ["text", "vision", "image", "search"];
18
24
 
@@ -34,10 +40,25 @@ export function cleanIA(raw) {
34
40
  return out.url || out.adaptador ? out : null;
35
41
  }
36
42
 
37
- export function loadIA(file = iaFile()) {
38
- try { return cleanIA(JSON.parse(fs.readFileSync(file, "utf8"))); } catch { return null; }
43
+ // lê o arquivo e, quando ele existe mas não serve, diz por quê (sem isso, um ia.json de outro dono, que o serviço não
44
+ // consegue ler, aparecia só como "IA não configurada"). Sem arquivo não é problema: é só não configurada.
45
+ export function readIA(file = iaFile()) {
46
+ let text;
47
+ try { text = fs.readFileSync(file, "utf8"); }
48
+ catch (e) {
49
+ if (e.code === "ENOENT") return { config: null, problem: "" };
50
+ if (e.code === "EACCES" || e.code === "EPERM") return { config: null, problem: `O arquivo ${file} existe, mas este processo não tem permissão para lê-lo. Confira o dono e as permissões dele.` };
51
+ return { config: null, problem: `Não deu para ler ${file} (${e.code || e.message}).` };
52
+ }
53
+ let raw;
54
+ try { raw = JSON.parse(text); } catch (e) { return { config: null, problem: `O arquivo ${file} não é um JSON válido (${e.message}).` }; }
55
+ const config = cleanIA(raw);
56
+ return config ? { config, problem: "" } : { config: null, problem: `O arquivo ${file} não tem o endereço do provedor ("url").` };
39
57
  }
40
58
 
59
+ export const loadIA = (file = iaFile()) => readIA(file).config;
60
+ export const iaProblem = (file = iaFile()) => readIA(file).problem;
61
+
41
62
  // grava de forma atômica, só para quem é dono da conta (a chave mora aqui)
42
63
  export function saveIA(cfg, file = iaFile()) {
43
64
  const clean = cleanIA(cfg);
package/src/ai/llm.js CHANGED
@@ -17,7 +17,7 @@ import { mediaPrompt } from './art-direction.js';
17
17
  import { currentUsage, recordUsage } from './usage.js';
18
18
  import path from 'node:path';
19
19
  import { pathToFileURL } from 'node:url';
20
- import { iaFile, loadIA, keyOf, migrateModelrelay } from './ia-config.js';
20
+ import { iaFile, readIA, keyOf, migrateModelrelay } from './ia-config.js';
21
21
  import {setTimeout as delay} from 'node:timers/promises';
22
22
 
23
23
  export class LLMError extends Error {
@@ -38,7 +38,7 @@ let migrated = false;
38
38
  export function llmConfig(env = process.env) {
39
39
  const fromEnv = !!env.SAGADECK_LLM_URL;
40
40
  if (!fromEnv && !migrated) { migrated = true; try { migrateModelrelay({ env, file: iaFile(env) }); } catch { /* segue sem */ } }
41
- const ia = fromEnv ? null : loadIA(iaFile(env));
41
+ const { config: ia, problem } = fromEnv ? { config: null, problem: "" } : readIA(iaFile(env));
42
42
  const m = ia?.models || {};
43
43
  return {
44
44
  url: (env.SAGADECK_LLM_URL || ia?.url || "").replace(/\/+$/, ""),
@@ -51,6 +51,8 @@ export function llmConfig(env = process.env) {
51
51
  headers: ia?.headers || {},
52
52
  adaptador: ia?.adaptador || "",
53
53
  source: fromEnv ? "variável" : ia ? "arquivo" : "nenhuma",
54
+ // o ia.json existe mas não serve (sem permissão, JSON quebrado): o motivo, para não dizer só "não configurada"
55
+ problem,
54
56
  timeoutMs: Number(env.SAGADECK_LLM_TIMEOUT || 180) * 1000,
55
57
  firstTimeoutMs: Number(env.SAGADECK_LLM_FIRST_TIMEOUT || 600) * 1000,
56
58
  };
@@ -79,7 +81,7 @@ async function send(cfg, url, init) {
79
81
  if (typeof fn !== "function") throw new LLMError(`O adaptador ${cfg.adaptador} não exporta uma função padrão (url, init) => Response.`);
80
82
  return fn(url, init);
81
83
  }
82
- const notConfigured = () => new LLMError("A IA não está configurada: no Studio, abra Configurar IA (ou crie ~/.sagadeck/ia.json).", { code: "AI_NOT_CONFIGURED" });
84
+ const notConfigured = (cfg) => new LLMError(cfg?.problem ? `A IA não está configurada. ${cfg.problem}` : "A IA não está configurada: no Studio, abra Configurar IA (ou crie ~/.sagadeck/ia.json).", { code: "AI_NOT_CONFIGURED" });
83
85
  const howToFix = "Confira o provedor e a chave em Configurar IA, no Studio.";
84
86
 
85
87
  // Disponibilidade com cache curto, para o Studio decidir entre LLM e as regras determinísticas.
@@ -182,7 +184,7 @@ async function chatOnce(messages, opts = {}) {
182
184
  }
183
185
 
184
186
  async function chatOnceRaw(messages, { model, temperature, maxTokens, onDelta, signal, reasoningOff, cfg = llmConfig() } = {}) {
185
- if (!llmConfigured(cfg)) throw notConfigured();
187
+ if (!llmConfigured(cfg)) throw notConfigured(cfg);
186
188
  const body = { model: resolveModel(cfg, model || cfg.textModel), messages };
187
189
  if (reasoningOff) body.reasoning = { enabled: false };
188
190
  if (temperature !== undefined) body.temperature = temperature;
package/src/ai/variety.js CHANGED
@@ -38,7 +38,7 @@ export function varietyReport(spec) {
38
38
  // Direções criativas: cada deck gerado sorteia um jeito de contar (e de se parecer), para dois decks sobre
39
39
  // assuntos parecidos não saírem iguais.
40
40
  export const CREATIVE_DIRECTIONS = [
41
- "Keynote minimalista: uma frase de abertura sem ilustração, grandes espaços vazios, composição central, um número por ideia. Considere prata; destaque em cor apenas na virada.",
41
+ "Palco minimalista: uma frase de abertura sem ilustração, grandes espaços vazios, composição central, um número por ideia. Considere prata; destaque em cor apenas na virada.",
42
42
  "Investigação editorial: abra com uma cena concreta, revele pistas por enquadramentos e comparações, entregue a conclusão no final. Considere editorial; serifas, linhas finas, papel claro e um único contraste escuro.",
43
43
  "Workshop geométrico: abra com uma escolha da plateia, alterne desafios curtos, diagramas e timer. Considere bauhaus ou pop; formas primárias, assimetria e revelação por clique.",
44
44
  "Estudo de caso documental: contexto → problema → tentativa → resultado → lição. Considere jornal; manchetes, evidências visuais, cronologia e legendas curtas. Só use dados do briefing ou dados verificados.",
package/src/api-client.js CHANGED
@@ -25,6 +25,7 @@ import { connectWs } from "./ws.js";
25
25
  import { protect, unprotect, unprotectAll, PROTECTION } from "./protect.js";
26
26
 
27
27
  const C = globalThis.SagadeckApiCore;
28
+ const BEARER = "Bearer {{secret.token}}"; // o "token fixo" do formulário: cabeçalho em todo pedido do ambiente
28
29
 
29
30
  // As variáveis e segredos dos slides de API moram FORA das apresentações: ~/.sagadeck/ambientes.yaml (no Windows,
30
31
  // C:\Users\<você>\.sagadeck\ambientes.yaml) ou SAGADECK_AMBIENTES. Mandar um deck para alguém nunca leva segredo
@@ -128,7 +129,7 @@ export class ApiEnvironments {
128
129
  catch (e) { throw new ApiError(`${this.file}: YAML inválido: ${e.message}`, "config"); }
129
130
  const envs = data.environments && typeof data.environments === "object" ? data.environments : {};
130
131
  // segredos cifrados (dpapi:…): decifra todos de uma vez (um PowerShell só)
131
- try { unprotectAll(Object.values(envs).flatMap((e) => Object.values((e && e.secrets) || {}))); }
132
+ try { unprotectAll(Object.values(envs).flatMap((e) => [...Object.values((e && e.secrets) || {}), e?.token?.client_secret].filter((v) => typeof v === "string"))); }
132
133
  catch (e) { throw new ApiError(`${this.file}: ${e.message}`, "config"); }
133
134
  return { current: data.current || null, environments: envs };
134
135
  }
@@ -236,6 +237,106 @@ export class ApiEnvironments {
236
237
  return this.state();
237
238
  }
238
239
 
240
+ // ---- tela Ambientes por campos (src/studio/public/api-envs-form.js) ----------------------------------------------
241
+ // O mesmo arquivo, visto como formulário: endereço base, variáveis, segredos (só o nome e de onde vêm; o valor nunca
242
+ // sai) e a autenticação (nenhuma, token fixo ou token que expira). O que o formulário não conhece (ca, insecure,
243
+ // outros cabeçalhos) fica no arquivo como estava.
244
+ formState() {
245
+ const raw = fs.existsSync(this.file) ? (YAML.parse(fs.readFileSync(this.file, "utf8")) || {}) : {};
246
+ const envs = raw.environments && typeof raw.environments === "object" ? raw.environments : {};
247
+ return {
248
+ file: this.file, exists: fs.existsSync(this.file), protection: PROTECTION, current: raw.current || null,
249
+ builtin: Object.keys(this.builtin),
250
+ envs: Object.entries(envs).map(([name, e = {}]) => {
251
+ const bearer = e.headers?.Authorization === BEARER;
252
+ const auth = e.token?.url ? { type: "token", url: e.token.url, client_id: e.token.client_id || "", field: e.token.field || "", secretEnv: e.token.client_secret_env || "", secretSet: e.token.client_secret != null }
253
+ : bearer ? { type: "bearer", secretEnv: typeof e.secrets?.token === "object" ? e.secrets.token.env || "" : "", secretSet: typeof e.secrets?.token === "string" }
254
+ : { type: "nenhuma" };
255
+ const otherHeaders = Object.keys(e.headers || {}).filter((h) => !(bearer && h === "Authorization"));
256
+ return {
257
+ name, base: e.vars?.base != null ? String(e.vars.base) : "",
258
+ vars: Object.entries(e.vars || {}).filter(([k]) => k !== "base").map(([k, v]) => ({ name: k, value: String(v) })),
259
+ secrets: Object.entries(e.secrets || {}).filter(([k]) => !(bearer && k === "token")).map(([k, v]) => ({ name: k, env: typeof v === "object" && v ? v.env || "" : "", set: typeof v === "string" })),
260
+ auth, advanced: !!(e.ca || e.insecure || otherHeaders.length),
261
+ };
262
+ }),
263
+ };
264
+ }
265
+
266
+ saveForm(list, { current } = {}) {
267
+ if (!Array.isArray(list)) throw new ApiError("Mande a lista de ambientes.", "config");
268
+ const NAME = /^[A-Za-z0-9_-]{1,40}$/, VAR = /^[A-Za-z_][A-Za-z0-9_]*$/;
269
+ const seen = new Set();
270
+ for (const f of list) {
271
+ if (!NAME.test(String(f?.name || ""))) throw new ApiError(`Nome de ambiente inválido: "${f?.name || ""}". Use letras, números, - e _ (ex.: dev, hom, prod).`, "config");
272
+ if (seen.has(f.name)) throw new ApiError(`O ambiente "${f.name}" aparece duas vezes.`, "config");
273
+ seen.add(f.name);
274
+ if (f.base && !/^https?:\/\//i.test(f.base)) throw new ApiError(`Ambiente "${f.name}": o endereço base começa com http:// ou https://.`, "config");
275
+ for (const v of [...(f.vars || []), ...(f.secrets || [])]) if (!VAR.test(String(v?.name || ""))) throw new ApiError(`Ambiente "${f.name}": nome inválido "${v?.name || ""}". Use letras, números e _ (ex.: path_id).`, "config");
276
+ if (f.auth?.type === "token" && !/^https?:\/\//i.test(f.auth.url || "")) throw new ApiError(`Ambiente "${f.name}": falta o endereço do token (https://…).`, "config");
277
+ }
278
+ this.editFile((doc) => {
279
+ const old = doc.toJS()?.environments || {};
280
+ for (const name of Object.keys(old)) if (!seen.has(name)) doc.deleteIn(["environments", name]);
281
+ for (const f of list) {
282
+ const at = (...k) => ["environments", f.name, ...k];
283
+ const prev = old[f.name] || {};
284
+ if (!doc.hasIn(at())) doc.setIn(at(), doc.createNode({}));
285
+ // variáveis: base + as da tabela; a que saiu da tabela sai do arquivo
286
+ const vars = { ...(f.base ? { base: f.base } : {}), ...Object.fromEntries((f.vars || []).map((v) => [v.name, String(v.value ?? "")])) };
287
+ for (const k of Object.keys(prev.vars || {})) if (!(k in vars)) doc.deleteIn(at("vars", k));
288
+ for (const [k, v] of Object.entries(vars)) doc.setIn(at("vars", k), v);
289
+ // segredos: valor novo (cifrado), variável de ambiente, ou fica o que já estava
290
+ const keepToken = f.auth?.type === "bearer";
291
+ const wanted = new Set([...(f.secrets || []).map((x) => x.name), ...(keepToken ? ["token"] : [])]);
292
+ for (const k of Object.keys(prev.secrets || {})) if (!wanted.has(k)) doc.deleteIn(at("secrets", k));
293
+ const putSecret = (name, { value, env } = {}) => {
294
+ if (env) doc.setIn(at("secrets", name), doc.createNode({ env }));
295
+ else if (value) doc.setIn(at("secrets", name), protect(String(value)));
296
+ else if (!doc.hasIn(at("secrets", name))) throw new ApiError(`Ambiente "${f.name}": falta o valor do segredo "${name}".`, "config");
297
+ };
298
+ for (const x of f.secrets || []) putSecret(x.name, x);
299
+ // autenticação
300
+ const ourBearer = doc.getIn(at("headers", "Authorization")) === BEARER;
301
+ if (f.auth?.type === "bearer") {
302
+ putSecret("token", { value: f.auth.value, env: f.auth.secretEnv });
303
+ doc.setIn(at("headers", "Authorization"), BEARER);
304
+ } else if (ourBearer) doc.deleteIn(at("headers", "Authorization"));
305
+ if (doc.getIn(at("headers")) && !Object.keys(doc.getIn(at("headers")).toJSON?.() || {}).length) doc.deleteIn(at("headers"));
306
+ if (f.auth?.type === "token") {
307
+ const t = { url: f.auth.url, ...(f.auth.client_id ? { client_id: f.auth.client_id } : {}), ...(f.auth.field ? { field: f.auth.field } : {}) };
308
+ for (const k of ["url", "client_id", "field"]) if (!(k in t)) doc.deleteIn(at("token", k)); else doc.setIn(at("token", k), t[k]);
309
+ if (f.auth.secretEnv) { doc.setIn(at("token", "client_secret_env"), f.auth.secretEnv); doc.deleteIn(at("token", "client_secret")); }
310
+ else if (f.auth.value) { doc.setIn(at("token", "client_secret"), protect(String(f.auth.value))); doc.deleteIn(at("token", "client_secret_env")); }
311
+ else if (!doc.hasIn(at("token", "client_secret")) && !doc.hasIn(at("token", "client_secret_env"))) throw new ApiError(`Ambiente "${f.name}": falta o segredo do token (client secret).`, "config");
312
+ } else if (doc.hasIn(at("token"))) doc.deleteIn(at("token"));
313
+ }
314
+ const names = list.map((f) => f.name);
315
+ doc.set("current", names.includes(current) ? current : names[0] || null);
316
+ });
317
+ this.tokens.clear();
318
+ return this.formState();
319
+ }
320
+
321
+ // Testar um ambiente salvo: o endereço base responde? o token sai? (só diz o que aconteceu; nada vai para a tela)
322
+ async check(name) {
323
+ const env = this.env(name);
324
+ const out = { name: env.name };
325
+ const base = env.vars?.base;
326
+ if (base) {
327
+ try {
328
+ const res = await open({ method: "GET", url: String(base), headers: { Accept: "*/*", ...C.render(env.headers || {}, this.secretVars(env)) }, ca: this.ca(env), insecure: !!env.insecure, timeout: 10000 });
329
+ res.resume();
330
+ out.base = { ok: true, status: res.statusCode };
331
+ } catch (e) { out.base = { ok: false, error: this.maskText(env, e.message) }; }
332
+ }
333
+ if (env.token?.url) {
334
+ try { const v = await this.token(env, { renew: true }); out.token = { ok: true, last4: v.slice(-4) }; }
335
+ catch (e) { out.token = { ok: false, error: this.maskText(env, e.message) }; }
336
+ }
337
+ return out;
338
+ }
339
+
239
340
  env(name) {
240
341
  const data = this.load();
241
342
  const n = name || this.currentName(data);
@@ -257,7 +358,7 @@ export class ApiEnvironments {
257
358
 
258
359
  secrets(env) {
259
360
  const t = env.token || {};
260
- const out = [this.tokens.get(env.name)?.value, t.client_secret, t.client_secret_env && process.env[t.client_secret_env], ...Object.values(env.headers || {}), ...Object.values(this.secretVars(env))];
361
+ const out = [this.tokens.get(env.name)?.value, t.client_secret != null ? unprotect(t.client_secret) : null, t.client_secret_env && process.env[t.client_secret_env], ...Object.values(env.headers || {}), ...Object.values(this.secretVars(env))];
261
362
  return out.filter((s) => typeof s === "string" && s.length >= 6);
262
363
  }
263
364
 
@@ -273,7 +374,7 @@ export class ApiEnvironments {
273
374
  const cached = this.tokens.get(env.name);
274
375
  if (!renew && cached && Date.now() < cached.renewAt) return cached.value;
275
376
  const id = t.client_id ?? (t.client_id_env && process.env[t.client_id_env]);
276
- const secret = t.client_secret ?? (t.client_secret_env && process.env[t.client_secret_env]);
377
+ const secret = (t.client_secret != null ? unprotect(t.client_secret) : null) ?? (t.client_secret_env && process.env[t.client_secret_env]);
277
378
  if (!id || !secret) throw new ApiError(`Ambiente "${env.name}": falta client_id/client_secret do token (ou a variável ${t.client_secret_env || t.client_id_env || "…"})`, "config");
278
379
  const fields = { [t.id_field || "client_id"]: id, [t.secret_field || "client_secret"]: secret, ...(t.extra || {}) };
279
380
  const form = t.format === "form";
@@ -320,7 +421,7 @@ export class ApiEnvironments {
320
421
  throw new ApiError(`Ambiente "${env.name}" não tem ${names.join(", ")} configurado em secrets:`, "config");
321
422
  }
322
423
  req = { ...req, url: C.render(req.url, sv), headers: C.render(req.headers || {}, sv), body: C.render(req.body, sv) };
323
- const headers = { Accept: "application/json, text/event-stream, */*", ...(env.headers || {}), ...(req.headers || {}) };
424
+ const headers = { Accept: "application/json, text/event-stream, */*", ...C.render(env.headers || {}, sv), ...(req.headers || {}) };
324
425
  let body = req.body;
325
426
  const file = req.file || null; // { name, type, data: Buffer } — o arquivo do slide (upload ou base64)
326
427
  const needsFile = (req.form && Object.values(req.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(body ?? ""));
@@ -488,7 +589,7 @@ export class ApiEnvironments {
488
589
  const sv = this.secretVars(env);
489
590
  let url = C.render(String(rt.url || ""), { ...(env.vars || {}), ...sv });
490
591
  const t = env.token || {};
491
- const headers = { ...(env.headers || {}) };
592
+ const headers = { ...C.render(env.headers || {}, sv) };
492
593
  const auth = String(rt.auth || "header");
493
594
  if (auth !== "none") {
494
595
  const tok = await this.token(env);