Menu
5/9 Configurações e segredosTodas as páginas

Tutoriais · Plugins

Configurações e segredos

Os quatro tipos de configuração, como os valores chegam ao sidecar e o padrão de ação “faça agora” com boolean e trava anti-repetição.

Nesta página

Os quatro tipos

O Jarvis ADE gera o formulário do plugin a partir de contributes.settings. Existem apenas quatro tipos; o type só escolhe o widget:

Tipos de configuração
typeWidgetComo chega ao sidecar
stringCampo de textoa própria string
secretCampo de senha, com botão Mostrar/Ocultara própria string
booleanCheckbox"true" ou "false"
numberEntrada numéricao número em texto decimal, ex.: "30"
contributes.settings
[
  {
    "key": "apiKey",
    "label": "API key",
    "type": "secret",
    "required": true,
    "description": "Chave de API do serviço.",
    "placeholder": "pk_..."
  },
  {
    "key": "workspaceId",
    "label": "Workspace",
    "type": "string",
    "description": "Deixe vazio para detectar automaticamente."
  },
  {
    "key": "verbose",
    "label": "Log detalhado",
    "type": "boolean",
    "default": "false"
  },
  {
    "key": "timeoutSeconds",
    "label": "Timeout (segundos)",
    "type": "number",
    "default": "30"
  }
]

Como os valores chegam ao sidecar

  • Tudo é string. O app guarda e entrega um objeto plano Record<string, string>. O manifesto tem default como string também para boolean e número.
  • Defaults + valores do usuário. O objeto entregue é o default de cada configuração com os valores preenchidos por cima.
  • Duas vias, mesmo conteúdo: a variável ADE_PLUGIN_SETTINGS (JSON) e o campo settings da mensagem hello.
  • Salvar reinicia o sidecar. O botão Salvar do formulário envia todos os campos de uma vez (não há salvamento por campo) e o app faz um restart gracioso: o processo novo nasce com as configurações novas e recebe um hello novo.
lendo as configurações
const settings = JSON.parse(process.env.ADE_PLUGIN_SETTINGS ?? '{}')

const apiKey = settings.apiKey // string ('' se o usuário não preencheu)
const verbose = settings.verbose === 'true' // boolean chega como 'true' | 'false'
const timeout = Number.parseFloat(settings.timeoutSeconds) // number chega como texto decimal; '' → NaN
if (!Number.isFinite(timeout)) {
  // o formulário não impede um campo numérico vazio: valide você mesmo
}

Configurações obrigatórias

Com required: true, enquanto o valor (já contando o default) estiver vazio — ou só com espaços — o sidecar não é iniciado: a integração fica em missing_settings e a tela mostra faltando: <chaves>. Assim que o usuário salva o que faltava, o sidecar sobe. Aproveite: um plugin sem a chave de API nem chega a rodar, e você não precisa tratar esse caso no código.

Um boolean obrigatório com default: "false" já conta como preenchido ("false" não é vazio).

Segredos

secret muda só o widget (campo de senha). Segundo o guia de plugins do Jarvis ADE, os valores ficam no estado do app, sem uma camada de criptografia à parte — trate-os como credenciais locais do usuário.

  • Nunca registre segredos. O hello e o ADE_PLUGIN_SETTINGS carregam todos os valores; um log com o objeto inteiro aparece no painel de log da integração e é guardado.
  • Peça o menor escopo possível ao serviço externo (um token só de leitura, um token por workspace) e diga isso na description da configuração.
  • Valide o formato cedo (por exemplo, o prefixo esperado do token) e responda com um error claro, sem repetir o valor.

Padrão: “faça agora” com um boolean e uma trava

Às vezes o plugin precisa de um gatilho do usuário: “reindexar agora”, “exportar”, “sincronizar tudo”. Como não há botões, o gatilho é uma configuração boolean: o usuário marca, clica em Salvar, o app reinicia o sidecar e o hello chega com settings.runNow === "true".

A solução é uma trava persistida em ADE_PLUGIN_DATA_DIR (a pasta sobrevive aos restarts): o sidecar age quando runNow está marcado e a trava está livre, grava a trava e só então executa. Quando o usuário desmarca e salva, o hello chega com "false" e a trava é rearmada.

Comportamento da trava
hello.settings.runNowTravaO que o sidecar faz
"true"livreGrava a trava e executa a ação.
"true"consumidaIgnora (replay de restart, crash ou reabertura do app).
"false"consumidaRearma a trava (livre). Não executa.
"false"livreNada.
ade.plugin.json
{
  "id": "acao-sob-demanda",
  "name": "Ação sob demanda",
  "version": "0.1.0",
  "description": "Executa uma ação quando o usuário marca a configuração 'Executar agora' e salva.",
  "license": "MIT",
  "contributes": {
    "settings": [
      {
        "key": "runNow",
        "label": "Executar agora",
        "type": "boolean",
        "default": "false",
        "description": "Marque e clique em Salvar para executar. Para executar de novo, desmarque, salve e repita."
      }
    ],
    "integrations": [
      {
        "id": "acao",
        "name": "Ação sob demanda",
        "command": "node",
        "args": ["integrations/acao.mjs"],
        "events": ["mission.finished"]
      }
    ]
  }
}
integrations/acao.mjs
// integrations/acao.mjs — ação "faça agora" com trava persistida (sem dependências).
import { createInterface } from 'node:readline'
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
import { join } from 'node:path'

const dataDir = process.env.ADE_PLUGIN_DATA_DIR ?? '.ade-data'

const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`)
const log = (level, message) => send({ type: 'log', level, message })

async function loadState() {
  try {
    return JSON.parse(await readFile(join(dataDir, 'state.json'), 'utf8'))
  } catch {
    return {}
  }
}

/** Escrita atômica: um crash no meio nunca deixa um state.json truncado. */
async function saveState(state) {
  await mkdir(dataDir, { recursive: true })
  const tmp = join(dataDir, 'state.json.tmp')
  await writeFile(tmp, JSON.stringify(state), 'utf8')
  await rename(tmp, join(dataDir, 'state.json'))
}

async function runAction() {
  log('info', 'executando a ação pedida pelo usuário')
  // ... o trabalho de verdade entra aqui ...
}

/** Roda a CADA hello — inclusive nos replays de restart e de crash com backoff. */
async function onHello(settings) {
  const state = await loadState()

  if (settings.runNow !== 'true') {
    // Desmarcado: rearma a trava para o próximo "Executar agora".
    if (state.consumed) await saveState({ ...state, consumed: false })
    return
  }
  if (state.consumed) {
    log('info', 'runNow continua marcado, mas a ação já rodou — ignorando o replay do hello')
    return
  }
  // Grava a trava ANTES de agir: um crash no meio não repete a ação em loop.
  await saveState({ ...state, consumed: true, lastRunAt: Date.now() })
  await runAction()
}

send({ type: 'ready', name: 'acao-sob-demanda' })

const rl = createInterface({ input: process.stdin })
let queue = Promise.resolve() // uma linha por vez, na ordem em que chegaram

rl.on('line', (line) => {
  queue = queue
    .then(async () => {
      let event
      try {
        event = JSON.parse(line)
      } catch {
        log('warn', 'linha ilegível')
        return
      }
      if (event.type === 'hello') await onHello(event.settings ?? {})
      else if (event.type === 'shutdown') process.exit(0)
    })
    .catch((err) => log('error', `falha: ${err instanceof Error ? err.message : String(err)}`))
})

rl.on('close', () => {
  void queue.then(() => process.exit(0))
})

Decisões de projeto

  • Trava antes da ação (como no exemplo) = no máximo uma execução: se o processo cair no meio, a ação não repete. Se a ação for idempotente e perdê-la for pior que repeti-la, grave a trava depois de concluir.
  • A caixa continua marcada depois da ação. Diga na description que, para executar de novo, é preciso desmarcar, salvar e marcar de novo.
  • Variante com token: troque o boolean por uma configuração string (por exemplo runToken) e execute quando o valor for diferente do último token gravado em state.json. O usuário dispara alterando o texto — sem “desmarcar e marcar”.
  • Escrita atômica do state.json (arquivo temporário + rename): um crash no meio da escrita nunca deixa o arquivo truncado.
  • Uma linha por vez. O exemplo encadeia o processamento das linhas numa fila de promises, para dois hello seguidos não correrem em paralelo sobre o mesmo estado.

Mais sobre logs, pasta de dados e idempotência em boas práticas.