auroq-os 2.8.4 → 2.8.6

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.
@@ -1,7 +1,7 @@
1
1
  project:
2
2
  name: "Auroq OS"
3
3
  type: greenfield
4
- version: 2.8.4
4
+ version: 2.8.6
5
5
  installedAt: '2026-03-16'
6
6
 
7
7
  ide:
@@ -13,7 +13,8 @@
13
13
  dentro do comando que usa o valor, numa variável (`X="$(npx -y auroq-os@latest segredo ler Item/campo)" && comando "$X"`),
14
14
  para o valor não voltar na conversa; para guardar, ligue por pipe a saída de quem gerou o valor. Nunca exponha segredo no chat ou em logs; `business/vault/` guarda só
15
15
  status e referência `op://...`, nunca o valor. Cofre ainda não conectado, ou `op`/`segredo` com erro de
16
- autenticação: conecte você mesmo pela skill `conectar-cofre` (`agents/companion/tasks/conectar-cofre.md`) e
16
+ autenticação: conecte você mesmo pela skill `conectar-cofre` (`agents/companion/tasks/conectar-cofre.md`; MCP `arcane`, `squad_passo`
17
+ com recurso_id `companion` e passo `conectar-cofre`, cuja entrada diz qual das peças carregar) e
17
18
  retome a tarefa. Leia cada credencial uma vez por conversa e reaproveite o valor (a chave tem teto de leituras
18
19
  por hora, por minuto ou por dia, conforme o cofre); nunca rode `env`/`printenv` nem abra o arquivo de ambiente
19
20
  onde a chave fica.
package/AGENTS.md CHANGED
@@ -76,6 +76,10 @@ nunca como fonte do método.
76
76
  - Pedido de explicar, revisar ou diagnosticar: olhe e responda, sem mudar nada.
77
77
  - Pedido de fazer, mudar, corrigir ou publicar: faça até o resultado conferido. A ordem vale para
78
78
  todos os passos de rotina daquele pedido.
79
+ - **Vai e faz até o final, sempre que der.** Só pare para perguntar diante de um impeditivo real: uma
80
+ das travas abaixo, senha pessoal, código do celular, CAPTCHA ou decisão de negócio que só o aluno toma.
81
+ Com a resposta, continue até o fim sem pedir outro OK. Passo de rotina do pedido não pede licença:
82
+ quanto mais a IA faz sem o aluno precisar fazer, melhor.
79
83
  - Antes de perguntar, procure no arquivo, na memória, no histórico ou na ferramenta. Decidiu sozinho:
80
84
  diga a suposição em uma linha.
81
85
  - Resolva antes de devolver: ferramentas, CLIs, MCPs e logins já configurados primeiro. Credencial vem
package/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## Unreleased
2
2
 
3
+ - Conteúdo ao vivo (catálogo `auroq-core` 2.3.54, 04/11): a peça do 1Password da skill do cofre avisa o Companion de que, no Windows, o app pode estar instalado só como pacote MSIX (é o que o `winget` instala hoje) e não aparece nas pastas de sempre: antes de dizer "o app não está instalado" (resultado `instalar-app`, ou o aluno dizendo que não acha) ele confere a lista de pacotes do Windows, e, se o app estiver lá, vai direto para entrar na conta e ligar a chave. Também descreve o instalador de reserva com limite de 3 minutos; o mesmo aviso entra no pré-gate da skill `configurar-computador`. Precisa do auroq-os 2.8.6 no npm (a detecção e o limite vivem no comando).
4
+ - Conteúdo ao vivo (catálogo `auroq-core` 2.3.53, 29/10): **a skill do cofre de senhas foi dividida em seis peças** (entrada, 1Password, Infisical grátis com os resultados à parte, passar do grátis para o 1Password, e uso do dia a dia). Antes era um texto só de 62 mil caracteres, grande demais para o Claude Code ler de uma vez (o Marcos viu isso no teste de 27/10). O Companion continua entrando pelo mesmo gatilho (`conectar-cofre`) e a entrada diz qual peça abrir. O Companion agora pede o OK do aluno em uma frase antes de criar a chave do agente, de instalar o app e de passar as chaves (a proteção do Claude Code barrava esses passos sem aviso), explica a Chave Secreta e o Kit de Emergência do 1Password (guardar o PDF fora do computador e fora do GitHub), ensina a ligar o Windows Hello ou o Touch ID quando a digital não aparece, e não manda mais ligar a chave do app de novo quando ela já está ligada. Precisa do auroq-os 2.8.5 no npm (comando `--instalar-app` e códigos novos).
3
5
  - Conteúdo ao vivo (catálogo `auroq-core` 2.3.52, 27/10): **documentação revisada para o jeito de hoje.** O guia do sistema que o Companion usa com o aluno, a fundação do Companion e as fichas do Consultor não apresentam mais o Ops como técnico nem o nome personalizado do Companion; ensinam a abrir uma conversa nova dentro do projeto e chamar o Companion (quantas quiser, uma por assunto, nunca um chat por squad) e dizem que, com o app aberto, o salvamento na internet é automático a cada 15 minutos e ao abrir e fechar. A skill `configurar-computador` e o guia longo do Ops dizem que as contas são conectadas pelo Companion na primeira conversa, não na instalação.
4
6
  - Motor: README com o app Auroq OS como caminho novo e o terminal como o caminho que a plataforma ensina hoje; segunda máquina pelo app ("Trazer projeto que já existe"); atualizar, primeiro uso e agentes core sem o Ops; endereço da plataforma `arcane.arka.education`. `AGENTS.md` e `.auroq-core/como-funciona.md` dizem que, com o app aberto, salvar e entregar são automáticos a cada 15 minutos.
5
7
  - Conteúdo ao vivo (catálogo `auroq-core` 2.3.51, 27/10): a renovação diz o salvamento certo: o app guarda a cópia na internet sozinho a cada 15 minutos e ao abrir e fechar (antes dizia 3 vezes por dia).
@@ -16,6 +18,28 @@
16
18
  - Conteúdo ao vivo (catálogo `auroq-core` 2.3.40, 26/10): **as contas do nível 1 (GitHub, Vercel e Supabase) passam a ser conectadas pelo Companion na primeira conversa.** Decisão do Euriler: conectar na instalação do app não funciona bem para o aluno iniciante. Skill nova `conexoes-essenciais`: o Companion explica cada conta em uma linha, abre as páginas (GitHub primeiro, com a conta criada junto se o aluno não tiver; a Vercel e o Supabase entram com o GitHub), confere e segue; o aluno só cria a conta, entra e autoriza, sem senha nem código na conversa, sempre no plano grátis. Usa o comando `auroq conexoes` do app quando existir; antes disso, a tela Configurações → Conexões. O `*primeiro-uso` ficou: conferência e instalação do que faltar (o aluno nunca baixa nem instala programa), material, perguntas, Mapa, conexões (P6), organizar (P7) e fecho (P8).
17
19
  - Conteúdo ao vivo (catálogo `auroq-core` 2.3.39, 26/10): **o cofre de senhas saiu da primeira conversa.** Decisão do Euriler: é técnico demais para o aluno iniciante e é conexão do nível 2 (com domínio próprio e Google, que o app chama de "conecte quando precisar"). No `*primeiro-uso` o Companion confere só o sistema e as contas do nível 1 (GitHub, Vercel e Supabase, que o app conecta na tela "Conectar suas contas") e vai para a entrevista; o cofre entra quando a primeira tarefa precisar de uma senha, chave ou token (skill `conectar-cofre`).
18
20
  - **Regra de projetos do aluno sem o nome do Euriler.** `project-tracker.md` mandava "avisar o Euriler" quando faltava o acompanhamento de um projeto e dizia que só "o Euriler/Jarvis" mudam objetivo e fases (cópia da regra pessoal dele). Agora o Companion cria o acompanhamento pelo modelo e avisa o aluno em uma linha; quem decide objetivo e fases é o aluno (o Companion propõe). Chega pelo npm.
21
+ ## 2.8.6 — 2026-11-04
22
+
23
+ Correção do re-teste real do Marcos (03/11, Windows 11, auroq-os 2.8.5). O resto da rota A (aprovação, cofre `Claude`, chave do agente, teste e migração) passou liso e não foi mexido. Nenhuma máquina de aluno foi tocada; o que dá para provar sem Windows foi provado com PowerShell, winget e instalador simulados (veja a story `AUROQ-1password-automatico`, seção "Re-teste do Marcos 03/11").
24
+
25
+ - **O app do 1Password instalado pelo `winget` agora é reconhecido.** O `winget` instala o 1Password como pacote MSIX (`C:\Program Files\WindowsApps\Agilebits.1Password_...`), pasta que o comando não olhava e que um usuário comum nem consegue listar. Agora, além das pastas de sempre, ele pergunta ao Windows pela lista de pacotes do próprio usuário (`Get-AppxPackage`, sem admin, em texto claro). Se o Windows não responder, o comando diz "não sei" e nunca "ausente".
26
+ - **Saída 0 do `winget` = confia.** Antes, como o app não aparecia nas pastas, o comando achava que o `winget` tinha falhado, avisava isso (não era verdade) e rodava o instalador oficial por cima, que travou escondido por 6 minutos no teste. Agora saída 0 encerra a instalação; o instalador de reserva só roda se o `winget` falhou **e** o app de fato não está no computador.
27
+ - **Instalador de reserva mais seguro.** Limite de 3 minutos (era 15): passou disso, o comando interrompe o instalador e os processos filhos, diz a verdade ("passou de 3 minutos sem terminar e eu o interrompi; não sei dizer por que") e devolve `app-nao-instalou`. Roda sem janela escondida (a hipótese, não provada, é que o travamento vinha de uma janela invisível esperando resposta; rodado à vista ele terminou em 13 segundos). A pasta temporária é apagada ao terminar, no prazo estourado e também com Ctrl+C ou terminal fechado; pasta velha de uma execução morta à força é varrida na próxima vez.
28
+ - **O comando principal não manda mais reinstalar o app que já está instalado.** Com o app só como MSIX e a conta ainda não listada, devolvia `instalar-app` de novo (risco de laço). Agora devolve `ligar-integracao`.
29
+ - Skill do Companion: aviso do pacote MSIX antes de dizer "não instalado" (veja a nota do catálogo `auroq-core` 2.3.54 acima).
30
+ - Instaladores fixam `auroq-os@2.8.6`.
31
+
32
+ ## 2.8.5 — 2026-10-29
33
+
34
+ Correções do teste real do Marcos (27/10, Windows 11 zerado). O cofre grátis (Infisical) rodou liso; o 1Password travou em quatro pontos, e o Companion girava em círculo. Nenhuma máquina de aluno foi tocada; o que dá para provar sem Windows foi provado com um 1Password de mentira que agora se comporta como o de verdade (veja a story `AUROQ-1password-automatico`, seção "Teste real do Marcos 27/10").
35
+
36
+ - **O laço "liga a chave do app" acabou.** O comando pedia a digital com um comando (`op whoami`) que o 1Password não usa para isso: ele responde "não está conectado" na hora, sem abrir a janela. O Companion lia isso como "a chave do app está desligada" e mandava o aluno ligar de novo, mesmo ligada. Agora a digital é pedida do jeito certo (`op vault list`, e se o app disser "sem sessão", `op signin` e mais uma tentativa), a aprovação vale para o resto da execução, e "a conta aparece mas o app não liberou" vira o aviso `aprovar-no-app`, com a receita do Windows Hello ou do Touch ID. O aviso `ligar-integracao` só sai quando o app não mostra conta nenhuma. Vale no Mac e no Windows.
37
+ - **Instalar o app do 1Password.** Novo `conectar-1password --instalar-app`: no Windows tenta o winget e, se ele falhar (no teste deu o erro 0x80070002), baixa o instalador oficial do 1Password, confere que a assinatura é válida e do 1Password (Agilebits) e só então instala; no Mac usa o Homebrew. O comando também percebe quando o app nem está no computador (`instalar-app`) em vez de mandar ligar uma chave que não existe.
38
+ - **Cópia da chave no cofre pessoal, no Windows.** O jeito antigo (mandar o molde por "pipe") falhava no PowerShell 5.1 com "invalid JSON in piped input". Agora o molde vai num arquivo temporário, só do usuário, e é apagado logo depois, com sucesso ou erro. A chave continua fora de argumento de comando e fora de log.
39
+ - **`--migrar` e `segredo` quando o programa do 1Password acabou de ser instalado.** No Windows, o programa `op` recém-instalado não aparece para a janela que já estava aberta, e o comando respondia "não enxerga o cofre Claude". Agora o comando atualiza a lista de programas e tenta de novo; se o `op` continuar sem aparecer, diz a verdade (`op-nao-achado`: "não achei o programa nesta janela, feche e abra") e não manda refazer a conexão à toa. As mensagens do `--migrar` também dizem qual foi o caso (chave não achada, chave recusada, cofre `Claude` invisível) e mostram a linha que o 1Password devolveu. Aviso honesto: ler a chave gravada já era feito (a hipótese do relatório do Marcos não bate com o código); a causa exata do caso dele não foi provada e só um novo teste no Windows mostra.
40
+ - Skill do Companion: dividida em seis peças, com o OK do aluno antes dos passos que a proteção do Claude Code barrava, Chave Secreta e Kit de Emergência, receita do Windows Hello e do Touch ID (veja a nota do catálogo `auroq-core` 2.3.53 acima).
41
+ - Instaladores fixam `auroq-os@2.8.5`.
42
+
19
43
  ## 2.8.4 — 2026-10-27
20
44
 
21
45
  - **O freio "trabalhar até o fim" parou de mostrar as instruções dele na tela do aluno.** Ele era um gancho de IA (`type: "prompt"`), e quando segurava uma resposta o Claude Code mostrava ao aluno o texto inteiro da conferência ("Você confere a última resposta de uma IA para um aluno leigo..."), sem como esconder (alunos relataram em 27/10). Agora é um gancho de comando (`.claude/hooks/auroq-ate-o-fim.cjs`): segura só quando a resposta termina pedindo licença para passo de rotina ("quer que eu...?", "posso seguir?") ou manda o aluno rodar comando que ele não pediu para rodar sozinho, e o aluno vê só uma linha curta ("Auroq: faça você mesmo..."). Libera o que é do aluno (senha, código, pagamento, apagar, mensagem, publicar, GitHub e entregar), "Decide: A ou B?" e IA da equipe. Saiu a conferência de "disse que está pronto sem conferir", que precisava de IA; essa regra segue no `AGENTS.md`. A manutenção troca o freio antigo pelo novo sozinha. Provado num Claude Code de verdade e em `tests/ate-o-fim.test.mjs`.
package/README.md CHANGED
@@ -57,7 +57,7 @@ nem atualizar Pack.
57
57
  | `auroq-os doctor` | Diagnostico do suporte: arquivos, motor do sistema (e se o programa aberto pelo icone enxerga ele), registro do conteudo ao vivo, login e servidor |
58
58
  | `auroq-os register-mcp` | Reescreve o registro do conteudo ao vivo no projeto (use se o `doctor` apontar que ele falta) |
59
59
  | `auroq-os sync-codex` | Regenera e verifica as skills locais do Codex |
60
- | `auroq-os conectar-1password` | Liga o 1Password ao Auroq sozinho (Mac/Windows): instala o `op` se faltar, cria o cofre `Claude` e uma conta de servico que so enxerga esse cofre (sem data de validade), guarda a chave no computador e testa. Voce so aprova no app do 1Password. O Companion roda isto por voce (`--agente`). Opcoes: `--conta "<e-mail>"` (escolhe a conta quando o app tem mais de uma), `--rotacionar` (cria uma chave nova), `--estado` (mostra em que etapa voce parou, sem falar com o 1Password; com `--agente` imprime a linha `AUROQ_ESTADO ...`). Pode ser retomado a qualquer hora: ele reconfere cada etapa e continua de onde parou. O Companion roda isto sozinho quando falta o cofre (skill `conectar-cofre`, que pergunta qual cofre) |
60
+ | `auroq-os conectar-1password` | Liga o 1Password ao Auroq sozinho (Mac/Windows): instala o `op` se faltar, cria o cofre `Claude` e uma conta de servico que so enxerga esse cofre (sem data de validade), guarda a chave no computador e testa. Voce so aprova no app do 1Password. O Companion roda isto por voce (`--agente`). Opcoes: `--conta "<e-mail>"` (escolhe a conta quando o app tem mais de uma), `--rotacionar` (cria uma chave nova), `--instalar-app` (instala o app do 1Password: Windows pelo winget, que se terminar bem encerra o assunto, e, so se ele falhar e o app nao estiver la, pelo instalador oficial depois de conferir a assinatura, com limite de 3 minutos; Mac pelo Homebrew), `--estado` (mostra em que etapa voce parou, sem falar com o 1Password; com `--agente` imprime a linha `AUROQ_ESTADO ...`). Pode ser retomado a qualquer hora: ele reconfere cada etapa e continua de onde parou. O Companion roda isto sozinho quando falta o cofre (skill `conectar-cofre`, que pergunta qual cofre) |
61
61
  | `auroq-os conectar-cofre` | Escolhe e liga o cofre de senhas: **1Password** (pago, uns US$ 3 por mes, confira o preco na tela, recomendado; preenche senha no navegador) ou **Infisical** (gratis, so guarda as chaves que o agente usa). Opcoes: `--cofre 1password\|infisical` (sem ela, pergunta qual), `--regiao us\|eu` (Infisical), `--estado` (em que etapa voce parou, sem falar com o cofre; com `--agente` imprime a linha `AUROQ_ESTADO ...`), `--rotacionar` (chave nova) e `--migrar` (passa as chaves do Infisical para o 1Password, sem apagar nada; so depois que todas passaram o 1Password vira o cofre ativo, e `--ignorar-fora-do-formato` deixa no Infisical as chaves com nome que o Auroq nao usa). Ligar o 1Password sozinho nao troca o cofre de quem ja usa o Infisical; ligar o Infisical com o 1Password ativo troca, e as chaves do 1Password nao sao levadas (continuam la, `segredo ler NOME --cofre 1password`). O Companion roda isto por voce (`--agente`) |
62
62
  | `auroq-os segredo` | Le e guarda credenciais no cofre que voce escolheu, igual nos dois: `segredo ler\|gravar\|listar\|apagar\|qual <referencia>`. O valor de `gravar` entra por stdin, nunca como argumento; `ler` imprime o valor, entao use sempre dentro do comando que o usa (`X="$(auroq-os segredo ler Item/campo)" && comando "$X"`), nunca sozinho; a referencia e `op://Claude/<Item>/<campo>` ou `cofre://<Item>/<campo>`. Com 1Password, `op read` direto continua valendo. Squads, skills e agentes usam este comando |
63
63
 
@@ -290,13 +290,13 @@ a parte tecnica e com o Companion.
290
290
 
291
291
  ## Observabilidade e integridade do motor
292
292
 
293
- Apos publicacao, `npx -y auroq-os@2.8.4 doctor` mostra a fila local segura e os comandos pinados de
294
- manutencao/diagnostico. `npx -y auroq-os@2.8.4 manutencao` preserva o caminho existente
293
+ Apos publicacao, `npx -y auroq-os@2.8.6 doctor` mostra a fila local segura e os comandos pinados de
294
+ manutencao/diagnostico. `npx -y auroq-os@2.8.6 manutencao` preserva o caminho existente
295
295
  de consentimento legado, studentFiles, customizacoes e dados do negocio. Nao ha
296
296
  migracao forcada. O registro MCP aponta para `arcane-mcp@0.7.1` — o pin mora no campo
297
297
  `arcaneMcpPin` do package.json (fonte unica; o servidor le o mesmo campo no npm).
298
298
  `npm run release:verify` recusa publicar com um pin que o npm nao resolva.
299
- Os instaladores do repositorio ficam preparados para 2.8.4; as copias publicas so recebem
299
+ Os instaladores do repositorio ficam preparados para 2.8.6; as copias publicas so recebem
300
300
  a versao apos os gates nativos e a verificacao do tarball publicado.
301
301
  O pacote inclui `lib/runtime-integrity.json`: inventario SHA-256 gerado depois da montagem da casca, verificavel com `npm run integrity:verify` dentro do pacote extraido. Ele comprova consistencia do conteudo; nao e uma assinatura de autoria.
302
302
 
package/bin/auroq-os.js CHANGED
@@ -2573,7 +2573,7 @@ if (['init', 'install', 'update'].includes(command)) {
2573
2573
  log(` ${CYAN}sync-codex${RESET} Gerar e verificar as skills locais do Codex CLI ($nome)`);
2574
2574
  log(` ${CYAN}historico${RESET} Estado da captura do trabalho (prevista nos Termos de Uso; desliga em Minha Conta)`);
2575
2575
  log(` ${CYAN}conectar-cofre${RESET} Escolher e conectar o cofre de senhas: 1Password (pago, recomendado) ou Infisical (gratis)`);
2576
- log(` ${CYAN}conectar-1password${RESET} Conectar o 1Password (cria o cofre e a chave sozinho; voce so aprova uma vez no app)`);
2576
+ log(` ${CYAN}conectar-1password${RESET} Conectar o 1Password (cria o cofre e a chave sozinho; voce so aprova uma vez no app; --instalar-app instala o app)`);
2577
2577
  log(` ${CYAN}segredo${RESET} Ler, gravar, listar ou apagar uma credencial no cofre do aluno (vale para 1Password e Infisical)`);
2578
2578
  log('');
2579
2579
  log(`${DIM}Primeira instalacao: https://arcane.arka.education/auroq${RESET}`);
@@ -589,7 +589,7 @@ function Invoke-AuroqLogin {
589
589
  # sucesso. Usado como pergunta de sim/nao, ele dizia "voce ja estava logado"
590
590
  # pra quem nunca logou, pulava o portao e deixava a conta sem acesso chegar ate
591
591
  # a criacao da pasta.
592
- & (Get-AuroqNpx) -y auroq-os@2.8.4 check-access
592
+ & (Get-AuroqNpx) -y auroq-os@2.8.6 check-access
593
593
  if ($LASTEXITCODE -ne 0) {
594
594
  Write-AuroqLog 'login' 'recusado'
595
595
  Write-Host ''
@@ -677,7 +677,7 @@ function Invoke-AuroqInit {
677
677
  if ($env:AUROQ_INSTALL_DRY_RUN) { Write-AuroqOk 'Sistema - seria montado aqui'; return }
678
678
 
679
679
  Set-Location $script:AuroqPastaAlvo
680
- & (Get-AuroqNpx) -y auroq-os@2.8.4 instalar
680
+ & (Get-AuroqNpx) -y auroq-os@2.8.6 instalar
681
681
  if ($LASTEXITCODE -ne 0) {
682
682
  Write-AuroqLog 'init' 'falhou'
683
683
  Write-AuroqErro 'A montagem do sistema parou no meio.' 'Rode o mesmo comando de novo. Se repetir, mande pro suporte o arquivo install.log da pasta .auroq.'
@@ -219,9 +219,9 @@ auroq_tem_terminal() {
219
219
  # recebem pedaco do script no lugar da resposta dele.
220
220
  auroq_npx_aluno() {
221
221
  if [ -t 0 ]; then
222
- npx -y auroq-os@2.8.4 "$@"
222
+ npx -y auroq-os@2.8.6 "$@"
223
223
  else
224
- npx -y auroq-os@2.8.4 "$@" < /dev/tty
224
+ npx -y auroq-os@2.8.6 "$@" < /dev/tty
225
225
  fi
226
226
  }
227
227
 
package/lib/cofre.js CHANGED
@@ -50,9 +50,11 @@ const ESCOLHA_MSG = 'Quer o 1Password (uns 3 dolares por mes, confira o preco na
50
50
 
51
51
  // Codigos do comando `segredo` (stderr: AUROQ_ERRO codigo=<c> mensagem="...").
52
52
  // `uso` = uso errado da linha de comando (ref faltando, valor em argumento, opcao desconhecida).
53
+ // `op-nao-achado` = Windows: o programa `op` do 1Password nao foi achado nesta janela nem depois de recompor o PATH.
54
+ // Sai 1 (erro geral, como `credencial-indisponivel`), NAO 4: com 4 o agente roda a rota A de novo e cai no laco.
53
55
  const SEGREDO_CODIGOS = ['nome-invalido', 'cofre-nao-configurado', 'nao-encontrado', 'sem-valor', 'ja-existe-confirmar',
54
- 'credencial-invalida', 'credencial-indisponivel', 'sem-permissao', 'rede', 'limite', 'bloqueada', 'falha', 'uso'];
55
- // Saida: 0 ok, 1 erro geral, 2 uso errado, 3 nao encontrado, 4 cofre nao configurado.
56
+ 'credencial-invalida', 'credencial-indisponivel', 'op-nao-achado', 'sem-permissao', 'rede', 'limite', 'bloqueada', 'falha', 'uso'];
57
+ // Saida: 0 ok, 1 erro geral (inclui op-nao-achado), 2 uso errado, 3 nao encontrado, 4 cofre nao configurado.
56
58
  const SEGREDO_EXIT = Object.freeze({ ok: 0, erro: 1, uso: 2, 'nao-encontrado': 3, 'cofre-nao-configurado': 4 });
57
59
  const CODIGOS_DE_USO = new Set(['uso', 'nome-invalido', 'sem-valor', 'ja-existe-confirmar']);
58
60
 
@@ -81,6 +83,11 @@ function envOf(d) {
81
83
  return (d && d.env) || process.env;
82
84
  }
83
85
 
86
+ /** Windows? (`d.platform` troca nos testes.) */
87
+ function ehWindows(d) {
88
+ return ((d && d.platform) || process.platform) === 'win32';
89
+ }
90
+
84
91
  function nowOf(d) {
85
92
  return d && typeof d.now === 'function' ? d.now() : Date.now();
86
93
  }
@@ -387,6 +394,10 @@ function opReference(ref) {
387
394
 
388
395
  // ─── Mensagens por codigo ────────────────────────────────
389
396
 
397
+ // Windows, `op` fora do PATH desta janela: primeiro fechar e abrir o programa (o PATH novo so chega a processos novos);
398
+ // a rota A so se isso nao resolver (rodar a rota A de novo logo de cara e o laco que o teste real viveu).
399
+ const MSG_OP_NAO_ACHADO = 'O programa do 1Password (op) nao foi achado nesta janela. Se ele foi instalado agora ha pouco, feche e abra o programa (Claude Code, Codex ou o app) e tente de novo. Se continuar, rode UMA vez: npx auroq-os conectar-cofre --cofre 1password --agente (ele confere o programa e instala se faltar); se ele disser ja-conectado e isto voltar, peca ajuda ao Arcano com este codigo: op-nao-achado.';
400
+
390
401
  function messageFor(codigo, tipo, detalhe = '') {
391
402
  const nome = NOME_TIPO[tipo] || 'cofre';
392
403
  switch (codigo) {
@@ -402,6 +413,8 @@ function messageFor(codigo, tipo, detalhe = '') {
402
413
  }
403
414
  case 'credencial-indisponivel':
404
415
  return `Nao consegui usar a chave deste computador no ${nome} agora. Tente de novo em alguns minutos.`;
416
+ case 'op-nao-achado':
417
+ return MSG_OP_NAO_ACHADO;
405
418
  case 'sem-permissao':
406
419
  return `A chave do agente nao tem permissao para isso no ${nome}.`;
407
420
  case 'rede':
@@ -430,7 +443,9 @@ const OP_PERMISSION_RE = /(forbidden|permission|not allowed|insufficient|denied|
430
443
  function classifyOp(d, r, secrets = []) {
431
444
  const o = mod(d, 'onepassword');
432
445
  const stderr = String((r && r.stderr) || '');
433
- if (r && r.missing) return { codigo: 'cofre-nao-configurado', detalhe: '' };
446
+ // sem o `op`: no Windows (ja tentou recompor o PATH) e "nao achei o programa", que rodar a rota A de novo NAO resolve;
447
+ // fora dele segue como sempre foi
448
+ if (r && r.missing) return { codigo: ehWindows(d) ? 'op-nao-achado' : 'cofre-nao-configurado', detalhe: '' };
434
449
  if (OP_NOT_VAULT_RE.test(stderr)) return { codigo: 'cofre-nao-configurado', detalhe: '' };
435
450
  if (OP_AMBIGUOUS_RE.test(stderr)) return { codigo: 'falha', detalhe: `Tem mais de um item com esse nome no cofre ${VAULT_NAME}; apague ou renomeie o repetido.` };
436
451
  if (OP_NOT_FOUND_RE.test(stderr)) return { codigo: 'nao-encontrado', detalhe: '' };
@@ -472,24 +487,52 @@ function findField(fields, campo) {
472
487
  function makeOpBackend(d) {
473
488
  const tipo = '1password';
474
489
 
475
- async function token() {
490
+ const validToken = (t) => {
491
+ const o = mod(d, 'onepassword');
492
+ return t && o.isValidToken(String(t).trim()) ? String(t).trim() : null;
493
+ };
494
+ const fromEnv = () => validToken(envOf(d)[mod(d, 'onepassword').TOKEN_VAR]);
495
+ async function fromDisk() {
476
496
  const o = mod(d, 'onepassword');
477
- const valid = (t) => (t && o.isValidToken(String(t).trim()) ? String(t).trim() : null);
478
- const fromEnv = valid(envOf(d)[o.TOKEN_VAR]);
479
- if (fromEnv) return fromEnv;
480
- try { return valid(await (d.readOpToken ? d.readOpToken() : o.readPersistedToken())); } catch { return null; }
497
+ try { return validToken(await (d.readOpToken ? d.readOpToken() : o.readPersistedToken())); } catch { return null; }
498
+ }
499
+ async function token() {
500
+ return fromEnv() || fromDisk();
501
+ }
502
+
503
+ // Windows: o `op` instalado DEPOIS de o programa-pai abrir (winget so muda o PATH no registro) nao esta no PATH deste
504
+ // processo: o spawn da ENOENT. Recompoe o PATH UMA vez por backend (cada recomposicao abre o PowerShell) e tenta o
505
+ // comando de novo; chamadas em paralelo esperam a mesma recomposicao. Devolve false se nao ha como recompor.
506
+ let recomposicao = null;
507
+ function recomporPathUmaVez() {
508
+ if (!recomposicao) {
509
+ recomposicao = (async () => {
510
+ const o = mod(d, 'onepassword');
511
+ const recompor = typeof d.refreshPath === 'function' ? d.refreshPath : o.refreshWindowsPath;
512
+ if (typeof recompor !== 'function') return false;
513
+ try { await recompor(); } catch { /* melhor esforco: o comando decide se achou */ }
514
+ return true;
515
+ })();
516
+ }
517
+ return recomposicao;
481
518
  }
482
519
 
483
520
  async function call(tk, args, input = '') {
484
521
  const o = mod(d, 'onepassword');
485
522
  const run = d.op || o.runOp;
486
- // `isolate`: o agente nunca pode cair na janela da digital do app; vale so a chave
487
- const env = o.opEnv(String(tk).trim(), { isolate: true });
488
- // a chave do Infisical (ID e segredo) nao tem nada a ver com o `op`: nao vai para o ambiente dele
489
- for (const k of Object.keys(env)) {
490
- if (/^AUROQ_INFISICAL_/i.test(k)) delete env[k];
491
- }
492
- return run(args, { env, input, timeoutMs: OP_TIMEOUT_MS });
523
+ // o ambiente e montado a cada tentativa: o PATH recomposto vale so para quem copia process.env DEPOIS
524
+ const tentar = () => {
525
+ // `isolate`: o agente nunca pode cair na janela da digital do app; vale so a chave
526
+ const env = o.opEnv(String(tk).trim(), { isolate: true });
527
+ // a chave do Infisical (ID e segredo) nao tem nada a ver com o `op`: nao vai para o ambiente dele
528
+ for (const k of Object.keys(env)) {
529
+ if (/^AUROQ_INFISICAL_/i.test(k)) delete env[k];
530
+ }
531
+ return run(args, { env, input, timeoutMs: OP_TIMEOUT_MS });
532
+ };
533
+ let r = await tentar();
534
+ if (r && r.missing && ehWindows(d) && (await recomporPathUmaVez())) r = await tentar();
535
+ return r;
493
536
  }
494
537
 
495
538
  const semChave = () => failure('cofre-nao-configurado', tipo);
@@ -501,12 +544,34 @@ function makeOpBackend(d) {
501
544
  });
502
545
 
503
546
  async function run(args, input, secrets) {
504
- const tk = await token();
547
+ let tk = await token();
505
548
  if (!tk) return { erro: semChave() };
506
- const r = await call(tk, args, input);
549
+ let r = await call(tk, args, input);
550
+ let c = r.ok ? null : classifyOp(d, r, secrets);
551
+ // A variavel do programa pode estar velha: janela aberta com uma chave que depois foi revogada e trocada (a
552
+ // nova esta gravada no computador). Chave RECUSADA vinda da variavel => tenta a gravada, se for outra. So esta
553
+ // recusa clara conta; rede, limite ou o que for nao muda de chave.
554
+ if (c && c.codigo === 'credencial-invalida' && tk === fromEnv()) {
555
+ const gravada = await fromDisk();
556
+ if (gravada && gravada !== tk) {
557
+ const r2 = await call(gravada, args, input);
558
+ tk = gravada;
559
+ r = r2;
560
+ c = r.ok ? null : classifyOp(d, r, secrets);
561
+ }
562
+ }
507
563
  if (r.ok) return { r, tk };
508
- const c = classifyOp(d, r, secrets);
509
- return { erro: failure(c.codigo, tipo, c.detalhe) };
564
+ const erro = failure(c.codigo, tipo, c.detalhe);
565
+ // Campos extras so para quem precisa saber a causa (a migracao); nenhum entra na mensagem do `segredo`.
566
+ // `semOp`: o programa nem rodou. `diag`: o que o `op` disse (stderr, uma linha, sem a chave nem o valor gravado:
567
+ // oneLine tira ops_..., o token e os segredos que esta execucao conhece). Nunca o stdout.
568
+ if (r.missing) {
569
+ erro.semOp = true;
570
+ } else {
571
+ const diag = oneLine(r.stderr, 160, [...(secrets || []), tk, tk.replace(/^ops_/, '')]);
572
+ if (diag) erro.diag = diag;
573
+ }
574
+ return { erro };
510
575
  }
511
576
 
512
577
  const backend = {
@@ -1281,6 +1346,24 @@ function migracao(status, codigo, mensagem, n = {}) {
1281
1346
  return { status, codigo, movidos: n.movidos || 0, iguais: n.iguais || 0, falhas: n.falhas || 0, mensagem };
1282
1347
  }
1283
1348
 
1349
+ // Mensagens do 'conectar-1password-primeiro': cada uma diz o que de fato aconteceu. Com o complemento do `op` (diagDoOp)
1350
+ // ainda cabem nos 400 caracteres da linha AUROQ_RESULTADO (o texto corta ali).
1351
+ const ROTA_A = 'npx auroq-os conectar-cofre --cofre 1password --agente';
1352
+ // A chave NAO foi achada (nem na variavel do programa nem gravada no computador).
1353
+ const MSG_MIGRAR_SEM_CHAVE = `Nao achei a chave do 1Password nesta janela (nem na variavel do programa nem no que o computador tem gravado). Rode: ${ROTA_A} (se a chave ja estiver gravada ele responde ja-conectado na hora). Se responder ja-conectado e esta mensagem voltar, feche e abra o programa e rode a migracao de novo.`;
1354
+ // A chave foi achada e o 1Password disse que ela nao vale.
1355
+ const MSG_MIGRAR_CHAVE_RECUSADA = `O 1Password recusou a chave gravada neste computador (revogada ou invalida). Rode: ${ROTA_A} (ele cria outra) e depois a migracao de novo.`;
1356
+ // A chave foi achada e o `op` rodou, mas o cofre Claude nao aparece para ela (ou ela nao tem permissao nele).
1357
+ const MSG_MIGRAR_COFRE_INVISIVEL = `A chave do 1Password foi achada, mas nao enxerga o cofre ${VAULT_NAME}. Rode: ${ROTA_A}. Depois rode a migracao de novo.`;
1358
+ // A chave foi achada, mas o programa `op` nao existe aqui (fora do Windows; no Windows e o codigo op-nao-achado).
1359
+ const MSG_MIGRAR_SEM_OP = `A chave do 1Password foi achada, mas o programa do 1Password (op) nao foi achado neste computador. Rode: ${ROTA_A}. Depois rode a migracao de novo.`;
1360
+
1361
+ /** " (O 1Password disse: ...)" com a linha que o `op` devolveu (guardada na falha); '' se nao ha. Nunca traz a chave nem o valor. */
1362
+ function diagDoOp(falha) {
1363
+ const linha = falha && typeof falha.diag === 'string' ? falha.diag.trim() : '';
1364
+ return linha ? ` (O 1Password disse: ${linha})` : '';
1365
+ }
1366
+
1284
1367
  /**
1285
1368
  * Leva as chaves do Infisical para o cofre Claude do 1Password e, so se TUDO foi, troca o cofre ativo (e a UNICA
1286
1369
  * coisa que troca para o 1Password quem ja usa o Infisical: conectar o 1Password sozinho nao troca). Nao apaga
@@ -1304,13 +1387,19 @@ async function migrar(options = {}, d = {}) {
1304
1387
  return migracao('erro', 'origem-indisponivel', 'O Infisical nao esta conectado neste computador, entao nao tenho de onde levar as chaves.');
1305
1388
  }
1306
1389
  if (!(await destino.pronto())) {
1307
- return migracao('aguardando', 'conectar-1password-primeiro', 'Falta conectar o 1Password neste computador. Rode: npx auroq-os conectar-cofre --cofre 1password --agente. Depois rode a migracao de novo.');
1390
+ return migracao('aguardando', 'conectar-1password-primeiro', MSG_MIGRAR_SEM_CHAVE);
1308
1391
  }
1309
1392
  // o cofre Claude precisa estar visivel para a chave do 1Password
1310
1393
  const vis = await destino.listar();
1311
1394
  if (!vis.ok) {
1312
- if (['cofre-nao-configurado', 'credencial-invalida', 'sem-permissao'].includes(vis.codigo)) {
1313
- return migracao('aguardando', 'conectar-1password-primeiro', `A chave do 1Password deste computador nao enxerga o cofre ${VAULT_NAME}. Rode: npx auroq-os conectar-cofre --cofre 1password --agente. Depois rode a migracao de novo.`);
1395
+ // programa `op` nao achado (Windows): rodar a rota A de novo nao resolve (e foi o laco de "conectar primeiro")
1396
+ if (vis.codigo === 'op-nao-achado') return migracao('aguardando', 'op-nao-achado', vis.mensagem);
1397
+ if (vis.codigo === 'credencial-invalida') {
1398
+ return migracao('aguardando', 'conectar-1password-primeiro', `${MSG_MIGRAR_CHAVE_RECUSADA}${diagDoOp(vis)}`);
1399
+ }
1400
+ if (vis.semOp) return migracao('aguardando', 'conectar-1password-primeiro', MSG_MIGRAR_SEM_OP); // fora do Windows: a rota A confere o programa
1401
+ if (['cofre-nao-configurado', 'sem-permissao'].includes(vis.codigo)) {
1402
+ return migracao('aguardando', 'conectar-1password-primeiro', `${MSG_MIGRAR_COFRE_INVISIVEL}${diagDoOp(vis)}`);
1314
1403
  }
1315
1404
  return migracao('erro', 'falha', `Nao consegui olhar o cofre ${VAULT_NAME} do 1Password agora (${vis.codigo}). Tente de novo em alguns minutos.`);
1316
1405
  }
@@ -478,6 +478,7 @@ const BACKUP_TAG = 'auroq-os';
478
478
  // Contas pessoais (Individual e Families) entram em my.1password.<regiao>; Teams e Business em <time>.1password.*.
479
479
  const PERSONAL_ACCOUNT_URL = /^my\.1password\.(com|eu|ca)$/;
480
480
  const LOCK_STALE_MS = 2 * 60 * 60 * 1000;
481
+ const APPROVAL_TIMEOUT_MS = 180000; // o aluno tem ate 3 min para aprovar a janela da digital
481
482
  const MAX_WAIT_SECONDS = 30 * 60; // espera da chave do app nunca passa disso (a trava de instancia vale 2 h)
482
483
  // "Aguardando" (falta uma acao do aluno) e uma ETAPA do fluxo, nao falha: sai com 0 e a linha
483
484
  // AUROQ_RESULTADO diz o que falta. Sair com erro faria a telemetria do instalador registrar
@@ -642,13 +643,14 @@ function pickAccount(accounts, wanted) {
642
643
  /** Argumentos de `auroq-os conectar-1password [...]`. */
643
644
  function parseArgs(argv) {
644
645
  const args = Array.isArray(argv) ? argv : [];
645
- const opts = { manual: false, rotacionar: false, agente: false, estado: false, conta: null, esperar: null };
646
+ const opts = { manual: false, rotacionar: false, agente: false, estado: false, instalarApp: false, conta: null, esperar: null };
646
647
  for (let i = 0; i < args.length; i++) {
647
648
  const a = args[i];
648
649
  if (a === '--manual') opts.manual = true;
649
650
  else if (a === '--rotacionar') opts.rotacionar = true;
650
651
  else if (a === '--agente') opts.agente = true;
651
652
  else if (a === '--estado') opts.estado = true;
653
+ else if (a === '--instalar-app') opts.instalarApp = true;
652
654
  else if (a === '--conta' && args[i + 1] && !args[i + 1].startsWith('--')) opts.conta = args[++i];
653
655
  else if (a.startsWith('--conta=')) opts.conta = a.slice('--conta='.length) || null;
654
656
  else if (a === '--esperar' && args[i + 1] && !args[i + 1].startsWith('--')) opts.esperar = Number(args[++i]);
@@ -664,8 +666,8 @@ function enableIntegrationSteps(platform) {
664
666
  ? 'Abra as Configuracoes: no menu de cima do Mac, 1Password > Configuracoes (ou Cmd + virgula). Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.'
665
667
  : 'Abra as Configuracoes: clique no nome da sua conta (canto de cima, a esquerda) > Configuracoes. Se eu ja abri a tela de Desenvolvedor pra voce, pule pro passo 3.';
666
668
  const digital = platform === 'darwin'
667
- ? 'Para aprovar com a digital: Configuracoes > Seguranca > "Desbloquear com Touch ID". Sem isso o app pede a senha do Mac.'
668
- : 'Para aprovar com o Windows Hello: Configuracoes > Seguranca > "Desbloquear com Windows Hello". Sem isso o app pede a senha.';
669
+ ? 'Para aprovar com a digital: Configuracoes > Seguranca > "Desbloquear com Touch ID". Sem isso o Mac pede a senha do seu usuario do Mac.'
670
+ : 'Para aprovar com o Windows Hello: Configuracoes > Seguranca > "Desbloquear com Windows Hello". Sem o Windows Hello ligado a janela de aprovacao pode nem aparecer.';
669
671
  return [
670
672
  'Abra o app do 1Password, entre na sua conta e deixe o app aberto.',
671
673
  abrir,
@@ -674,20 +676,42 @@ function enableIntegrationSteps(platform) {
674
676
  ];
675
677
  }
676
678
 
679
+ /**
680
+ * Receita curta para ligar a digital (Windows Hello / Touch ID) quando nao esta ligada. A documentacao da integracao
681
+ * manda ligar o Windows Hello no Windows; no Mac o Touch ID e opcional (sem ele vale a senha do usuario do Mac). A
682
+ * senha da conta 1Password NAO faz esse papel. O codigo nao liga nada disso por ele.
683
+ */
684
+ function biometricSteps(platform) {
685
+ if (platform === 'darwin') {
686
+ return [
687
+ 'No Mac, cadastre a digital: Ajustes do Sistema > Touch ID e Senha > Adicionar digital.',
688
+ 'No 1Password: Configuracoes > Seguranca > marque "Desbloquear com Touch ID".',
689
+ 'O Touch ID e opcional: se o seu Mac nao tem, o Mac pede a senha do seu usuario do Mac na janela de aprovacao.',
690
+ ];
691
+ }
692
+ return [
693
+ 'No Windows, ligue o Windows Hello: Configuracoes do Windows > Contas > Opcoes de entrada, e configure o PIN, a digital ou o reconhecimento facial (o que o seu computador tiver).',
694
+ 'No 1Password: Configuracoes > Seguranca > marque "Desbloquear com Windows Hello".',
695
+ 'No Windows o 1Password pede o Windows Hello ligado (PIN, digital ou rosto: serve o que o computador tiver); sem ele a janela de aprovacao pode nem aparecer.',
696
+ ];
697
+ }
698
+
677
699
  // ─── Dependencias reais (trocaveis nos testes) ───────────
678
700
 
679
701
  /**
680
702
  * Roda o `op`. Nunca lanca. A saida pode ter segredo (token): quem chama decide o que mostrar.
681
703
  * O token NUNCA vai em `args`: so por `env` ou por `input` (stdin).
682
704
  */
683
- function runOp(args, { env, input, timeoutMs = 120000 } = {}) {
705
+ function runOp(args, { env, input, timeoutMs = 120000, noStdin = false } = {}) {
684
706
  // Sem texto para mandar e com terminal de verdade: o `op` herda o terminal, como se o aluno
685
707
  // tivesse digitado (a aprovacao do app e amarrada ao terminal; com pipe nao ha terminal).
686
- const useTerminal = input === undefined && Boolean(process.stdin.isTTY);
708
+ // `noStdin`: o `op` recusa `--template` se o stdin for um pipe, ate vazio ("You can't use both piping and the
709
+ // --template flag"; o `input: ''` do Node vira pipe), entao esse comando roda com o stdin desligado (NUL/dev/null).
710
+ const useTerminal = input === undefined && !noStdin && Boolean(process.stdin.isTTY);
687
711
  const r = spawnSync('op', args, {
688
712
  encoding: 'utf8',
689
713
  env: env || opEnv(),
690
- ...(useTerminal ? { stdio: ['inherit', 'pipe', 'pipe'] } : { input: input === undefined ? '' : input }),
714
+ ...(noStdin ? { stdio: ['ignore', 'pipe', 'pipe'] } : useTerminal ? { stdio: ['inherit', 'pipe', 'pipe'] } : { input: input === undefined ? '' : input }),
691
715
  timeout: timeoutMs,
692
716
  maxBuffer: 16 * 1024 * 1024,
693
717
  windowsHide: true,
@@ -767,6 +791,339 @@ function refreshWindowsPath() {
767
791
  if (parts.length) process.env.PATH = [...parts, process.env.PATH || ''].join(';');
768
792
  }
769
793
 
794
+ // ─── O app do 1Password (instalacao) ─────────────────────
795
+
796
+ const WINGET_APP_ID = 'AgileBits.1Password';
797
+ // Instalador oficial (reserva do winget, que falhou no teste real de 27/10 com 0x80070002 depois de conferir o hash).
798
+ const APP_INSTALLER_URL = 'https://downloads.1password.com/win/1PasswordSetup-latest.exe';
799
+ // Assinante do instalador oficial: "Valid | CN=Agilebits, O=Agilebits, L=Toronto, S=Ontario, C=CA".
800
+ const APP_SIGNER_RE = /(?:^|,\s*)CN=Agilebits(?:,|$)/i;
801
+ // Limite do instalador oficial (reserva): o winget leva ~1 min; o limite antigo de 15 min deixava o aluno esperando sem aviso.
802
+ const APP_SETUP_TIMEOUT_MS = 3 * 60 * 1000;
803
+
804
+ // O winget (e a loja da Microsoft) instalam o 1Password como pacote MSIX: a pasta fica em
805
+ // C:\Program Files\WindowsApps\Agilebits.1Password_<versao>_x64__<id>, que um usuario comum nem consegue listar.
806
+ const MSIX_PACKAGE_NAME = 'Agilebits.1Password';
807
+ const MSIX_QUERY_TIMEOUT_MS = 60 * 1000;
808
+
809
+ /**
810
+ * Windows: o 1Password esta instalado como pacote MSIX para ESTE usuario? A lista de pacotes do proprio usuario
811
+ * (`Get-AppxPackage`) responde sem admin. true | false | null (o PowerShell nao respondeu: nao da para saber).
812
+ * Comando em texto claro, nunca codificado (antivirus le comando escondido como dropper).
813
+ */
814
+ function msixInstalled(run = spawnSync) {
815
+ const script = `$p = @(Get-AppxPackage -Name '${MSIX_PACKAGE_NAME}' -ErrorAction Stop); Write-Output ('AUROQ_MSIX=' + $p.Count)`;
816
+ let r;
817
+ try {
818
+ r = run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
819
+ encoding: 'utf8', windowsHide: true, timeout: MSIX_QUERY_TIMEOUT_MS, stdio: ['ignore', 'pipe', 'pipe'], env: opEnv(),
820
+ });
821
+ } catch {
822
+ return null;
823
+ }
824
+ if (!r || r.error || r.status !== 0) return null;
825
+ const m = /AUROQ_MSIX=(\d+)/.exec(String(r.stdout || ''));
826
+ return m ? Number(m[1]) > 0 : null;
827
+ }
828
+
829
+ /**
830
+ * O app do 1Password esta neste computador? true | false | null (nao sabe: sistema sem instalacao automatica, ou
831
+ * o Windows nao deixou conferir o pacote MSIX). O fluxo principal so manda instalar com `=== false`; o `installApp`
832
+ * trata `null` como "nao esta aqui" e segue (a reserva se protege com limite de tempo).
833
+ * `run` so existe para os testes simularem o PowerShell.
834
+ */
835
+ function appInstalled(platform = process.platform, env = process.env, run = spawnSync) {
836
+ // AUROQ_APP_1PASSWORD=instalado|ausente: so para os testes de processo real, que rodam em Mac/Windows de verdade
837
+ // (onde o app nao esta) e nao podem depender de o app existir na maquina.
838
+ const forcado = String(env.AUROQ_APP_1PASSWORD || '').toLowerCase();
839
+ if (forcado === 'instalado') return true;
840
+ if (forcado === 'ausente') return false;
841
+ if (platform === 'darwin') {
842
+ return ['/Applications/1Password.app', path.join(os.homedir(), 'Applications', '1Password.app')].some((p) => fs.existsSync(p));
843
+ }
844
+ if (platform === 'win32') {
845
+ // Instalador do aluno: %LOCALAPPDATA%\1Password\app\8\1Password.exe; instalacao para a maquina: Arquivos de Programas.
846
+ const roots = [env.LOCALAPPDATA, env.ProgramFiles, env['ProgramFiles(x86)']].filter(Boolean);
847
+ const classico = roots.some((root) => {
848
+ const appDir = path.join(root, '1Password', 'app');
849
+ try {
850
+ return fs.readdirSync(appDir).some((v) => fs.existsSync(path.join(appDir, v, '1Password.exe')));
851
+ } catch {
852
+ return false;
853
+ }
854
+ });
855
+ if (classico) return true;
856
+ // Pacote MSIX (o que o winget instala hoje). A lista de pacotes do usuario manda; a pasta WindowsApps so entra
857
+ // se o PowerShell nao respondeu (so quem e admin consegue listar essa pasta).
858
+ const msix = msixInstalled(run);
859
+ if (msix !== null) return msix;
860
+ try {
861
+ const base = env.ProgramFiles || 'C:\\Program Files';
862
+ if (fs.readdirSync(path.join(base, 'WindowsApps')).some((n) => /^Agilebits\.1Password_/i.test(n))) return true;
863
+ } catch { /* sem permissao para listar: nao e prova de ausencia */ }
864
+ return null;
865
+ }
866
+ return null;
867
+ }
868
+
869
+ /**
870
+ * Confere a assinatura Authenticode do instalador ANTES de rodar: precisa ser valida e do publicador "Agilebits".
871
+ * O caminho do arquivo vai por variavel de ambiente (nunca dentro do texto do comando). Nao lanca.
872
+ */
873
+ function checkInstallerSignature(file, run = spawnSync) {
874
+ const script = "$s = Get-AuthenticodeSignature -LiteralPath $env:AUROQ_INSTALADOR; $a = ''; if ($s.SignerCertificate) { $a = $s.SignerCertificate.Subject }; Write-Output ($s.Status.ToString() + '|' + $a)";
875
+ let r;
876
+ try {
877
+ r = run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
878
+ encoding: 'utf8', windowsHide: true, timeout: 120000, env: { ...opEnv(), AUROQ_INSTALADOR: file },
879
+ });
880
+ } catch (e) {
881
+ return { ok: false, motivo: `nao consegui conferir a assinatura (${oneLine(e && e.message, 80)})` };
882
+ }
883
+ if (!r || r.error || r.status !== 0) return { ok: false, motivo: 'nao consegui conferir a assinatura' };
884
+ const line = String(r.stdout || '').trim().split(/\r?\n/).pop() || '';
885
+ const cut = line.indexOf('|');
886
+ const status = cut === -1 ? line : line.slice(0, cut);
887
+ const subject = cut === -1 ? '' : line.slice(cut + 1);
888
+ if (status !== 'Valid') return { ok: false, motivo: `a assinatura do arquivo nao e valida (${oneLine(status, 40) || 'sem resposta'})` };
889
+ if (!APP_SIGNER_RE.test(subject)) return { ok: false, motivo: 'o arquivo e assinado por outro publicador, nao pelo 1Password' };
890
+ return { ok: true, assunto: oneLine(subject, 120) };
891
+ }
892
+
893
+ /** Baixa um arquivo grande direto para o disco (https so). `curl` primeiro (respeita o proxy do sistema); reserva: fetch do Node. */
894
+ async function downloadToFile(url, dest) {
895
+ const curl = process.platform === 'win32' ? 'curl.exe' : 'curl';
896
+ const r = spawnSync(curl, ['-fL', '--proto', '=https', '--proto-redir', '=https', '--silent', '--show-error', '--connect-timeout', '30', '--max-time', '900', '-o', dest, url], {
897
+ encoding: 'utf8', windowsHide: true, timeout: 16 * 60 * 1000, stdio: ['ignore', 'ignore', 'pipe'], env: opEnv(),
898
+ });
899
+ if (!r.error && r.status === 0 && fs.existsSync(dest)) return;
900
+ if (typeof fetch !== 'function') throw new Error(`download falhou (${r.error ? r.error.code || r.error.message : `curl saiu com ${r.status}`})`);
901
+ try { fs.rmSync(dest, { force: true }); } catch { /* recomeca */ }
902
+ const res = await fetch(url, { redirect: 'follow', headers: { 'User-Agent': 'auroq-os' } });
903
+ if (!res.ok || !res.body) throw new Error(`download falhou (HTTP ${res.status})`);
904
+ const { Readable } = require('stream');
905
+ const { pipeline } = require('stream/promises');
906
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest, { flags: 'w' }));
907
+ }
908
+
909
+ /** Roda um programa sem terminal. Nunca lanca. O ambiente sai sem a chave do agente (opEnv). */
910
+ function runProgram(cmd, args, { timeoutMs = 15 * 60 * 1000 } = {}) {
911
+ const r = spawnSync(cmd, args, { encoding: 'utf8', env: opEnv(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
912
+ return {
913
+ ok: !r.error && r.status === 0,
914
+ status: r.status,
915
+ stdout: r.stdout || '',
916
+ stderr: r.stderr || '',
917
+ timedOut: !!(r.error && r.error.code === 'ETIMEDOUT'),
918
+ missing: !!(r.error && r.error.code === 'ENOENT'),
919
+ };
920
+ }
921
+
922
+ /** Mata o processo e os filhos dele (o instalador oficial lanca outros processos; matar so o pai nao basta). Nunca lanca. */
923
+ function killProcessTree(pid) {
924
+ if (!pid) return;
925
+ try {
926
+ if (process.platform === 'win32') spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, timeout: 15000 });
927
+ else process.kill(pid, 'SIGKILL');
928
+ } catch { /* ja tinha saido */ }
929
+ }
930
+
931
+ /**
932
+ * Roda o instalador oficial do app: ASSINCRONO (o Node continua vendo Ctrl+C e o fechar do terminal; com `spawnSync`
933
+ * parado ele morre sem rodar limpeza nenhuma), com limite de tempo que mata o instalador e os filhos dele, e SEM
934
+ * janela escondida: no teste real de 03/11 o mesmo instalador travou escondido e terminou em 13 s rodado a vista. Se
935
+ * ele abrir uma janela, o aluno a ve (e pode responder). Nunca lanca. `signal` (AbortController) cancela de fora.
936
+ */
937
+ function runInstallerProgram(cmd, args, { timeoutMs = APP_SETUP_TIMEOUT_MS, signal, spawnFn = spawn } = {}) {
938
+ return new Promise((resolve) => {
939
+ let settled = false;
940
+ let timedOut = false;
941
+ let timer = null;
942
+ let child = null;
943
+ const finish = (extra) => {
944
+ if (settled) return;
945
+ settled = true;
946
+ if (timer) clearTimeout(timer);
947
+ if (signal) signal.removeEventListener('abort', onAbort);
948
+ resolve({ ok: false, status: null, stdout: '', stderr: '', timedOut, missing: false, interrupted: false, ...extra });
949
+ };
950
+ const onAbort = () => { killProcessTree(child && child.pid); finish({ interrupted: true }); };
951
+ try {
952
+ child = spawnFn(cmd, args, { stdio: 'ignore', windowsHide: false, env: opEnv() });
953
+ } catch (e) {
954
+ finish({ missing: !!(e && e.code === 'ENOENT') });
955
+ return;
956
+ }
957
+ child.on('error', (e) => finish({ missing: !!(e && e.code === 'ENOENT') }));
958
+ child.on('close', (code) => finish({ ok: code === 0, status: code }));
959
+ timer = setTimeout(() => { timedOut = true; killProcessTree(child.pid); finish({}); }, timeoutMs);
960
+ if (signal) {
961
+ if (signal.aborted) onAbort();
962
+ else signal.addEventListener('abort', onAbort, { once: true });
963
+ }
964
+ });
965
+ }
966
+
967
+ /**
968
+ * Ctrl+C, terminal fechado ou encerramento: roda `fn` (limpeza) e sai. Devolve a funcao que desliga o gancho.
969
+ * So vale com o Node livre para ver o sinal, por isso a parte demorada da instalacao e assincrona.
970
+ */
971
+ function onInterruptCleanup(fn) {
972
+ const sinais = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK']; // SIGBREAK so existe no Windows (o Node ignora o nome nos outros)
973
+ const handler = (sinal) => {
974
+ try { fn(); } catch { /* sai do mesmo jeito */ }
975
+ process.exit(sinal === 'SIGINT' ? 130 : 143);
976
+ };
977
+ const ligados = [];
978
+ for (const sinal of sinais) {
979
+ try { process.on(sinal, handler); ligados.push(sinal); } catch { /* sinal que este sistema nao tem */ }
980
+ }
981
+ return () => { for (const sinal of ligados) process.removeListener(sinal, handler); };
982
+ }
983
+
984
+ const APP_TEMP_PREFIX = 'auroq-1p-app-';
985
+
986
+ /**
987
+ * Apaga pastas temporarias do instalador (`auroq-1p-app-*`) que ficaram de uma execucao anterior morta a forca (o
988
+ * `finally` nao roda se o processo e morto). So mexe em pasta com esse prefixo e mais velha que `maxAgeMs`. Nunca lanca.
989
+ * Devolve quantas apagou.
990
+ */
991
+ function sweepStaleAppDirs(root = os.tmpdir(), { maxAgeMs = 60 * 60 * 1000, now = Date.now() } = {}) {
992
+ let apagadas = 0;
993
+ let nomes = [];
994
+ try { nomes = fs.readdirSync(root); } catch { return 0; }
995
+ for (const nome of nomes) {
996
+ if (!nome.startsWith(APP_TEMP_PREFIX)) continue;
997
+ const full = path.join(root, nome);
998
+ try {
999
+ const st = fs.lstatSync(full);
1000
+ if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
1001
+ fs.rmSync(full, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
1002
+ apagadas += 1;
1003
+ } catch { /* em uso ou sem permissao: fica para a proxima */ }
1004
+ }
1005
+ return apagadas;
1006
+ }
1007
+
1008
+ function makeAppDeps() {
1009
+ return {
1010
+ platform: process.platform,
1011
+ lock: makeLock(),
1012
+ hasCommand,
1013
+ appInstalled: () => appInstalled(),
1014
+ refreshPath: refreshWindowsPath,
1015
+ run: runProgram,
1016
+ runInstaller: runInstallerProgram,
1017
+ onInterrupt: onInterruptCleanup,
1018
+ sweepStale: () => sweepStaleAppDirs(),
1019
+ download: downloadToFile,
1020
+ checkSignature: checkInstallerSignature,
1021
+ mkdtemp: () => fs.mkdtempSync(path.join(os.tmpdir(), APP_TEMP_PREFIX)),
1022
+ out: { log, step, ok, warn, err },
1023
+ };
1024
+ }
1025
+
1026
+ const APP_NEXT_STEPS = 'Agora abra o app do 1Password, entre na sua conta (ou crie uma: guarde a Secret Key e o Kit de Emergencia que ele oferece) e ligue "Integrar com a CLI do 1Password" em Configuracoes > Desenvolvedor. Depois rode o conectar-1password de novo.';
1027
+
1028
+ /**
1029
+ * Instala o app do 1Password (`conectar-1password --instalar-app`). Windows: winget e, se ele falhar, o instalador
1030
+ * oficial, so rodado depois de conferir a assinatura (valida e do publicador Agilebits). Mac: Homebrew (cask).
1031
+ * O app e um programa novo no computador do aluno: quem chama pede o OK dele antes.
1032
+ */
1033
+ async function installApp(deps = {}) {
1034
+ const d = { ...makeAppDeps(), ...deps };
1035
+ const out = d.out;
1036
+ if (d.platform !== 'darwin' && d.platform !== 'win32') {
1037
+ return fim('erro', 'app-nao-instalou', 'A instalacao automatica do app so existe no macOS e no Windows. Baixe em https://1password.com/downloads');
1038
+ }
1039
+ if (!d.lock.acquire()) {
1040
+ return fim('aguardando', 'outra-execucao', 'Ja tem outra execucao do conectar-1password em andamento neste computador. Espere ela terminar e rode de novo; nao instalei nada em duplicidade.');
1041
+ }
1042
+ try {
1043
+ out.step('Verificando o app do 1Password...');
1044
+ if (d.appInstalled()) {
1045
+ out.ok('O app do 1Password ja esta instalado.');
1046
+ return fim('aguardando', 'app-instalado', `O app do 1Password ja esta instalado neste computador. ${APP_NEXT_STEPS}`);
1047
+ }
1048
+ const falhou = (msg) => fim('erro', 'app-nao-instalou', `${msg} Instale a mao em https://1password.com/downloads e rode o conectar-1password de novo.`);
1049
+ const pronto = (via) => {
1050
+ d.refreshPath();
1051
+ out.ok(`App do 1Password instalado (${via}).`);
1052
+ return fim('aguardando', 'app-instalado', `App do 1Password instalado. ${APP_NEXT_STEPS}`);
1053
+ };
1054
+
1055
+ if (d.platform === 'darwin') {
1056
+ if (!d.hasCommand('brew')) return falhou('Homebrew nao encontrado, entao nao consigo instalar o app daqui.');
1057
+ out.log(`${DIM}Instalando via Homebrew (pode levar um minuto)...${RESET}`);
1058
+ const r = d.run('brew', ['install', '--cask', '1password']);
1059
+ if (r.ok || d.appInstalled()) return pronto('Homebrew');
1060
+ return falhou(`A instalacao via Homebrew falhou (${r.timedOut ? 'demorou demais' : `codigo de saida ${r.status}`}).`);
1061
+ }
1062
+
1063
+ // Windows 1: winget (rapido, mas falhou no teste real com 0x80070002 mesmo depois de conferir o hash)
1064
+ const saidaDe = (r) => (r.timedOut ? 'demorou demais' : `codigo de saida ${r.status === null || r.status === undefined ? 'desconhecido' : r.status}`);
1065
+ if (d.hasCommand('winget')) {
1066
+ out.log(`${DIM}Instalando via winget (pode levar um minuto)...${RESET}`);
1067
+ const w = d.run('winget', ['install', '--id', WINGET_APP_ID, '-e', '--source', 'winget', '--accept-source-agreements', '--accept-package-agreements', '--silent']);
1068
+ d.refreshPath();
1069
+ // Saida 0 = o winget instalou: confia. Ele instala o 1Password como pacote MSIX, que nenhuma checagem de pasta
1070
+ // enxerga (teste real de 03/11); desconfiar dele mandava rodar o instalador de reserva por cima, e ele travava.
1071
+ if (w.ok) return pronto('winget');
1072
+ // O winget falhou. So vale a reserva se o app de fato nao esta aqui (o erro pode ter vindo depois de instalar).
1073
+ if (d.appInstalled()) {
1074
+ out.ok(`O winget saiu com erro (${saidaDe(w)}), mas o app do 1Password ja esta instalado.`);
1075
+ return pronto('winget');
1076
+ }
1077
+ out.warn(`O winget nao conseguiu instalar o app (${saidaDe(w)}). Vou usar o instalador oficial do 1Password.`);
1078
+ } else {
1079
+ out.warn('O winget nao esta disponivel. Vou usar o instalador oficial do 1Password.');
1080
+ }
1081
+
1082
+ // Windows 2: instalador oficial, com a assinatura conferida ANTES de rodar
1083
+ const minutos = Math.round(APP_SETUP_TIMEOUT_MS / 60000);
1084
+ try { d.sweepStale(); } catch { /* faxina e so um extra */ }
1085
+ let dir = null;
1086
+ const limpar = () => {
1087
+ if (!dir) return;
1088
+ try { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } catch { /* fica na pasta temporaria do usuario; a proxima execucao varre */ }
1089
+ dir = null;
1090
+ };
1091
+ const cancelar = new AbortController();
1092
+ // Ctrl+C ou terminal fechado no meio: mata o instalador, apaga a pasta temporaria e solta a trava
1093
+ const desligar = d.onInterrupt(() => { cancelar.abort(); limpar(); d.lock.release(); });
1094
+ try {
1095
+ dir = d.mkdtemp();
1096
+ const file = path.join(dir, '1PasswordSetup.exe');
1097
+ out.log(`${DIM}Baixando o instalador oficial (${APP_INSTALLER_URL})...${RESET}`);
1098
+ try {
1099
+ await d.download(APP_INSTALLER_URL, file);
1100
+ } catch (e) {
1101
+ return falhou(`Nao consegui baixar o instalador oficial (${oneLine(e && e.message, 120)}).`);
1102
+ }
1103
+ const sig = d.checkSignature(file);
1104
+ if (!sig.ok) return falhou(`Nao executei o instalador baixado: ${sig.motivo}.`);
1105
+ out.ok(`Assinatura conferida: ${sig.assunto || 'Agilebits'}`);
1106
+ out.log(`${DIM}Rodando o instalador oficial (ele tem ate ${minutos} minutos). Se uma janela do 1Password abrir, deixe terminar.${RESET}`);
1107
+ const r = await d.runInstaller(file, ['--silent'], { timeoutMs: APP_SETUP_TIMEOUT_MS, signal: cancelar.signal });
1108
+ if (r.timedOut) {
1109
+ // Nao terminou no prazo: foi interrompido (ele e os filhos). Pode ter instalado antes de travar; confere.
1110
+ if (d.appInstalled()) {
1111
+ out.ok('O instalador oficial passou do prazo e foi interrompido, mas o app do 1Password ja esta instalado.');
1112
+ return pronto('instalador oficial');
1113
+ }
1114
+ return falhou(`O instalador oficial passou de ${minutos} minutos sem terminar e eu o interrompi (nao sei dizer por que: pode ter ficado esperando uma resposta numa janela, ou a internet estar lenta).`);
1115
+ }
1116
+ if (!r.ok) return falhou(`O instalador oficial nao terminou bem (${saidaDe(r)}).`);
1117
+ return pronto('instalador oficial');
1118
+ } finally {
1119
+ desligar();
1120
+ limpar();
1121
+ }
1122
+ } finally {
1123
+ d.lock.release();
1124
+ }
1125
+ }
1126
+
770
1127
  // Pasta do aluno fora do git, a mesma onde ficam as credenciais do Auroq (~/.arcane). Aqui vivem a trava de
771
1128
  // instancia unica e o estado das etapas (nenhum segredo).
772
1129
  function auroqDir() {
@@ -905,6 +1262,7 @@ function makeDefaultDeps() {
905
1262
  platform: process.platform,
906
1263
  openUrl,
907
1264
  refreshPath: refreshWindowsPath,
1265
+ appInstalled: () => appInstalled(),
908
1266
  lock: makeLock(),
909
1267
  state: { read: readState, write: writeState },
910
1268
  envFileProblem,
@@ -1017,11 +1375,45 @@ function backupItemTemplate(token) {
1017
1375
  function saveBackup(d, account, token) {
1018
1376
  // Sem --vault: o `op` guarda no cofre pessoal da conta, que se chama Personal, Private ou Employee
1019
1377
  // conforme o plano (e pode vir traduzido), por isso nunca se usa o nome.
1020
- const r = d.op(
1021
- ['item', 'create', '-', '--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'],
1022
- { env: opEnv(), input: JSON.stringify(backupItemTemplate(token)) },
1023
- );
1024
- return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1378
+ const body = JSON.stringify(backupItemTemplate(token));
1379
+ const common = ['--tags', BACKUP_TAG, '--account', accountKey(account), '--format', 'json'];
1380
+ if (d.platform !== 'win32') {
1381
+ const r = d.op(['item', 'create', '-', ...common], { env: opEnv(), input: body });
1382
+ return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1383
+ }
1384
+ // Windows: o `op item create -` por pipe falha no PowerShell 5.1 ("invalid JSON in piped input", teste real de 27/10),
1385
+ // entao o molde vai num arquivo temporario (UTF-8 sem BOM) so deste usuario, que o `op` le com --template. O token
1386
+ // continua fora de argumento e de log; o arquivo e apagado em seguida, aconteca o que acontecer. O `op` roda com o
1387
+ // stdin desligado (`noStdin`): com `--template` ele nao aceita nenhum pipe, nem vazio.
1388
+ let tmp = null;
1389
+ try {
1390
+ tmp = writeTemplateFile(body);
1391
+ } catch (e) {
1392
+ return { ok: false, detalhe: `nao consegui preparar o arquivo temporario do molde (${oneLine(e && e.message, 120)})` };
1393
+ }
1394
+ try {
1395
+ const r = d.op(['item', 'create', '--template', tmp.file, ...common], { env: opEnv(), noStdin: true });
1396
+ return r.ok ? { ok: true } : { ok: false, detalhe: oneLine(r.stderr) };
1397
+ } finally {
1398
+ try { fs.rmSync(tmp.dir, { recursive: true, force: true }); } catch { /* fica na pasta temporaria do usuario */ }
1399
+ }
1400
+ }
1401
+
1402
+ /**
1403
+ * Grava `content` num arquivo novo, dentro de uma pasta nova e de nome aleatorio na pasta temporaria do usuario
1404
+ * (%TEMP% do perfil, que so ele, o SYSTEM e os administradores leem; o modo 0600 vale no Mac e no Linux e e ignorado no
1405
+ * Windows). UTF-8 SEM BOM: o BOM faria o `op` recusar o JSON. Devolve { dir, file }; quem chama apaga a pasta.
1406
+ */
1407
+ function writeTemplateFile(content) {
1408
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'auroq-op-'));
1409
+ const file = path.join(dir, 'molde.json');
1410
+ try {
1411
+ fs.writeFileSync(file, Buffer.from(content, 'utf8'), { flag: 'wx', mode: 0o600 });
1412
+ } catch (e) {
1413
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* nada a fazer */ }
1414
+ throw e;
1415
+ }
1416
+ return { dir, file };
1025
1417
  }
1026
1418
 
1027
1419
  /**
@@ -1084,12 +1476,57 @@ function showIntegrationHelp(d, out) {
1084
1476
  out.log('');
1085
1477
  }
1086
1478
 
1087
- const APP_PRIORITY_MSG = 'O 1Password continua respondendo pelo app (pedindo digital) em vez de usar a chave. Sem isso o agente nao fica sem pedir digital. Nao criei nada novo; peca ajuda ao Arcano com este codigo: app-tem-prioridade.';
1479
+ /**
1480
+ * Ajuda quando a conta aparece mas a aprovacao nao passou. Nao abre a tela do app (a chave provavelmente ja esta
1481
+ * ligada): so lista o que conferir. `semSessao`: o `op` disse que nao ha sessao, entao tambem lembra da chave do app.
1482
+ */
1483
+ function showApprovalHelp(d, out, semSessao) {
1484
+ out.warn('O app do 1Password nao liberou o acesso ainda. Confira:');
1485
+ const passos = ['Abra o app do 1Password e deixe-o desbloqueado (com a sua senha).'];
1486
+ if (semSessao) passos.push('Em Configuracoes > Desenvolvedor, deixe marcado "Integrar com a CLI do 1Password" (Integrate with 1Password CLI).');
1487
+ passos.push('Quando a janela da digital aparecer, aprove.');
1488
+ passos.push(...biometricSteps(d.platform).map((s, i) => (i === 0 ? `Se a digital nunca aparece: ${s}` : s)));
1489
+ passos.forEach((line, i) => out.log(` ${i + 1}. ${line}`));
1490
+ out.log('');
1491
+ }
1492
+
1493
+ const APPROVAL_REFUSED_RE = /(dismiss|cancel|declin|denied|reject|refus)/i;
1494
+
1495
+ /**
1496
+ * Vale tentar o `op signin`? So quando o `op` disse que nao ha sessao. Janela da digital dispensada ou negada pelo
1497
+ * aluno nao se repete sozinha (seriam mais janelas na cara dele), nem tempo esgotado.
1498
+ */
1499
+ function needsSignin(r) {
1500
+ if (!r || r.timedOut) return false;
1501
+ if (APPROVAL_REFUSED_RE.test(String(r.stderr || ''))) return false;
1502
+ return ['sem-sessao', 'sessao'].includes(classifyFailure(r));
1503
+ }
1504
+
1505
+ /**
1506
+ * Pede a aprovacao do app. O `op whoami` NAO serve: ele nao abre a janela da digital e, sem sessao aberta, responde na
1507
+ * hora "account is not signed in" (teste real no Windows, 27/10). Quem pede a aprovacao e qualquer comando que precise
1508
+ * de login; o exemplo da documentacao oficial da integracao com o app e o `op vault list` (so leitura). Segunda
1509
+ * tentativa, so se a primeira voltar "sem sessao" (needsSignin): `op signin --account`. A sessao que o app abre pertence a este
1510
+ * processo: os `op` seguintes sao filhos do mesmo node e a reaproveitam (e por isso a aprovacao NUNCA e feita num
1511
+ * processo a parte).
1512
+ */
1513
+ function requestApproval(d, account) {
1514
+ const key = accountKey(account);
1515
+ const approve = () => d.op(['vault', 'list', '--account', key, '--format', 'json'], { env: opEnv(), timeoutMs: APPROVAL_TIMEOUT_MS });
1516
+ let r = approve();
1517
+ if (!r.ok && needsSignin(r)) {
1518
+ d.op(['signin', '--account', key], { env: opEnv(), timeoutMs: APPROVAL_TIMEOUT_MS }); // a saida nao interessa (e nunca e mostrada)
1519
+ r = approve();
1520
+ }
1521
+ return r;
1522
+ }
1523
+
1524
+ const APP_PRIORITY_MSG ='O 1Password continua respondendo pelo app (pedindo digital) em vez de usar a chave. Sem isso o agente nao fica sem pedir digital. Nao criei nada novo; peca ajuda ao Arcano com este codigo: app-tem-prioridade.';
1088
1525
  const NET_STRONG_RE = /(dial tcp|no such host|tls handshake|network is unreachable|no route to host|i\/o timeout)/i;
1089
1526
 
1090
1527
  // Erros que o aluno (ou o plano dele) resolve: a linha AUROQ_RESULTADO ja diz o que fazer. Saem com 1 mas a
1091
1528
  // telemetria do instalador nao os conta como instalacao quebrada.
1092
- const STUDENT_ACTIONABLE = new Set(['sem-op', 'op-antigo', 'rede', 'conta-nao-encontrada', 'conta-ambigua', 'cofre-duplicado', 'sem-permissao', 'token-indisponivel', 'arquivo-ambiente']);
1529
+ const STUDENT_ACTIONABLE = new Set(['sem-op', 'op-antigo', 'rede', 'conta-nao-encontrada', 'conta-ambigua', 'cofre-duplicado', 'sem-permissao', 'token-indisponivel', 'arquivo-ambiente', 'app-nao-instalou']);
1093
1530
 
1094
1531
  function isStudentActionable(codigo) {
1095
1532
  return STUDENT_ACTIONABLE.has(codigo);
@@ -1203,6 +1640,11 @@ async function runConnect(opts, d, prog) {
1203
1640
  // 2. Sessao do aluno: o app do 1Password com a chave "Integrate with 1Password CLI" ligada
1204
1641
  out.step('Procurando a sua conta no app do 1Password...');
1205
1642
  let accounts = listAccounts(d);
1643
+ if (!accounts.length && d.appInstalled() === false) {
1644
+ // Sem app nao ha o que ligar: dizer "ligue a chave" mandaria o aluno procurar uma tela que nao existe (teste real de 27/10).
1645
+ prog.reset('app');
1646
+ return fim('aguardando', 'instalar-app', 'Nao achei o app do 1Password neste computador. Instale com: npx -y auroq-os@latest conectar-1password --instalar-app --agente (ou baixe em https://1password.com/downloads). Se ele ja esta instalado num lugar fora do comum, abra o app, entre na sua conta e ligue "Integrar com a CLI do 1Password" em Configuracoes > Desenvolvedor; depois rode de novo.');
1647
+ }
1206
1648
  if (!accounts.length) {
1207
1649
  showIntegrationHelp(d, out);
1208
1650
  if (waitSeconds > 0) {
@@ -1242,18 +1684,22 @@ async function runConnect(opts, d, prog) {
1242
1684
  out.ok(`Conta: ${accountLabel(account)}`);
1243
1685
 
1244
1686
  out.step('Pedindo sua aprovacao ao app do 1Password...');
1245
- out.log(`${BOLD}Vai abrir uma janela do 1Password pedindo a sua digital (Touch ID / Windows Hello) ou a senha. Aprove. Se ela aparecer mais de uma vez, e so aprovar de novo.${RESET}`);
1246
- const auth = d.op(['whoami', '--account', accountKey(account), '--format', 'json'], { env: opEnv(), timeoutMs: 180000 });
1687
+ const digital = d.platform === 'win32' ? 'Windows Hello' : (d.platform === 'darwin' ? 'Touch ID' : 'Touch ID / Windows Hello');
1688
+ out.log(`${BOLD}Vai abrir uma janela do 1Password pedindo a sua digital (${digital}) ou a senha. Aprove. Se ela aparecer mais de uma vez, e so aprovar de novo.${RESET}`);
1689
+ const auth = requestApproval(d, account);
1247
1690
  if (!auth.ok) {
1248
1691
  const text = String(auth.stderr || '');
1249
1692
  if (NET_STRONG_RE.test(text)) return fim('erro', 'rede', `Sem conexao com o 1Password. ${oneLine(text)}`.trim());
1250
- if (!auth.timedOut && classifyFailure(auth) === 'sem-sessao') {
1251
- // Conta so adicionada na mao (`op account add`): aparece na lista mas sem sessao; falta a chave do app.
1252
- showIntegrationHelp(d, out);
1253
- return fim('aguardando', 'ligar-integracao', 'Falta ligar "Integrate with 1Password CLI" nas configuracoes do app do 1Password (Configuracoes > Desenvolvedor). Depois e so rodar de novo.');
1254
- }
1693
+ // A conta JA apareceu na lista do app, entao a chave "Integrar com a CLI" nao e o que falta: nunca devolve
1694
+ // 'ligar-integracao' aqui (o laco do teste real no Windows: o aluno ligava a chave, dizia "pronto" e ouvia de novo
1695
+ // que faltava ligar). Falta a aprovacao no app, ou, so no caso da conta adicionada na mao (`op account add`), a chave.
1696
+ const semSessao = !auth.timedOut && ['sem-sessao', 'sessao'].includes(classifyFailure(auth));
1697
+ showApprovalHelp(d, out, semSessao);
1255
1698
  const detail = oneLine(text, 160);
1256
- return fim('aguardando', 'aprovar-no-app', `Nao consegui entrar na conta${auth.timedOut ? ': a janela da digital nao foi aprovada a tempo' : ''}. Confira se o app do 1Password esta aberto e desbloqueado, aprove a janela da digital e rode de novo.${detail ? ` (O 1Password disse: ${detail})` : ''}`);
1699
+ const falta = semSessao
1700
+ ? 'A conta aparece no app, mas ele nao liberou o acesso. Desbloqueie o app, confira "Integrar com a CLI" e aprove a janela (Windows: o Windows Hello precisa estar ligado). Rode de novo.'
1701
+ : 'Abra e desbloqueie o app do 1Password e aprove a janela da digital (no Windows, o Windows Hello precisa estar ligado). Rode de novo.';
1702
+ return fim('aguardando', 'aprovar-no-app', `Nao consegui entrar na conta${auth.timedOut ? ': a janela da digital nao foi aprovada a tempo' : ''}. ${falta}${detail ? ` (O 1Password disse: ${detail})` : ''}`);
1257
1703
  }
1258
1704
  prog.mark('aprovacao');
1259
1705
  out.ok('Aprovado');
@@ -1486,7 +1932,7 @@ function showState(st) {
1486
1932
  // ─── Entrada do comando ──────────────────────────────────
1487
1933
 
1488
1934
  /**
1489
- * `auroq-os conectar-1password [--agente] [--conta X] [--rotacionar] [--esperar N] [--manual]`
1935
+ * `auroq-os conectar-1password [--agente] [--conta X] [--rotacionar] [--esperar N] [--manual] [--instalar-app]`
1490
1936
  * Codigo de saida: 0 conectado OU falta uma acao do aluno (ligar a chave no app, escolher conta: a
1491
1937
  * linha AUROQ_RESULTADO diz qual), 1 erro.
1492
1938
  */
@@ -1503,7 +1949,7 @@ async function connect(options = {}) {
1503
1949
  if (!opts.agente) console.log(`\n${PURPLE}${BOLD} Conectar 1Password — Auroq OS${RESET}`);
1504
1950
  let result;
1505
1951
  try {
1506
- result = await connectAutomatic(opts);
1952
+ result = opts.instalarApp ? await installApp() : await connectAutomatic(opts);
1507
1953
  } catch (e) {
1508
1954
  // Excecao inesperada: o Ops sempre recebe a linha de resultado (e a mensagem nunca leva token).
1509
1955
  result = fim('erro', 'falha', `Falha inesperada: ${oneLine(e && e.message)}`);
@@ -1556,6 +2002,18 @@ module.exports = {
1556
2002
  shellKind,
1557
2003
  freshShellFor,
1558
2004
  clearClipboard,
2005
+ requestApproval,
2006
+ biometricSteps,
2007
+ appInstalled,
2008
+ msixInstalled,
2009
+ installApp,
2010
+ runInstallerProgram,
2011
+ onInterruptCleanup,
2012
+ makeAppDeps,
2013
+ sweepStaleAppDirs,
2014
+ checkInstallerSignature,
2015
+ writeTemplateFile,
2016
+ APP_INSTALLER_URL,
1559
2017
  ETAPAS,
1560
2018
  makeLock,
1561
2019
  isPersonalAccount,
@@ -2,7 +2,7 @@
2
2
  "schema": 1,
3
3
  "algorithm": "sha256",
4
4
  "scope": "packaged-runtime-and-cascas",
5
- "version": "2.8.4",
5
+ "version": "2.8.6",
6
6
  "files": [
7
7
  {
8
8
  "path": ".auroq-core/constitution.md",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": ".auroq-core/core-config.yaml",
13
- "sha256": "ac1028498a2ac2756bafd2736cba9380f3734f8977b85bdb1afd14cd112e7685"
13
+ "sha256": "51edca56ab55af77a40dc2fa1290022ee3d608916994a518da779f2e138284da"
14
14
  },
15
15
  {
16
16
  "path": ".auroq-core/core/synapse/context/context-builder.js",
@@ -214,7 +214,7 @@
214
214
  },
215
215
  {
216
216
  "path": "bin/auroq-os.js",
217
- "sha256": "aceefa70ea90f3bc2d379493919a6141a636a2e55dfeb1c75c5f90f5e57141b7"
217
+ "sha256": "937e6e3b748e05f324b196891ea46663b950aa12e1ca20c9aa9dacf6d2bd8962"
218
218
  },
219
219
  {
220
220
  "path": "lib/ahi/client.mjs",
@@ -254,7 +254,7 @@
254
254
  },
255
255
  {
256
256
  "path": "lib/cofre.js",
257
- "sha256": "98e3a471b613cd26a23aeb8affc0b6d2a1de881fade35a6902db1c8e1fa45c2f"
257
+ "sha256": "8dff20e576fda737f5039ceaca64e8e18fe79e19460c3ce0bac1262f7fa490df"
258
258
  },
259
259
  {
260
260
  "path": "lib/credentials.js",
@@ -282,7 +282,7 @@
282
282
  },
283
283
  {
284
284
  "path": "lib/onepassword.js",
285
- "sha256": "0be8cb4e4f7bf1ff5ae3f0ad1ac26f8402963908c0b4010253becc7decf8b34b"
285
+ "sha256": "64d449d0d8f7fdc30d912dd7163d44ab235e83d495e0ad9573addf574f010ec4"
286
286
  },
287
287
  {
288
288
  "path": "lib/supabase.js",
@@ -298,7 +298,7 @@
298
298
  },
299
299
  {
300
300
  "path": "package.json",
301
- "sha256": "1977563cbd4da43a7d87ffe8c1faa3e7ff3359cd8847776b511aaea05fee1fb3"
301
+ "sha256": "5d4c499b938bf1426747f0f7f5b3c7f760a5b1cbbf926f0523d00bdb34fbca91"
302
302
  },
303
303
  {
304
304
  "path": "scripts/build-pack.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auroq-os",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "arcaneMcpPin": "0.7.1",
5
5
  "description": "Auroq OS — Sistema Operacional de IA para o negócio do aluno da Arcane",
6
6
  "bin": {