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:
| type | Widget | Como chega ao sidecar |
|---|---|---|
| string | Campo de texto | a própria string |
| secret | Campo de senha, com botão Mostrar/Ocultar | a própria string |
| boolean | Checkbox | "true" ou "false" |
| number | Entrada numérica | o número em texto decimal, ex.: "30" |
[
{
"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 temdefaultcomo string também para boolean e número. - Defaults + valores do usuário. O objeto entregue é o
defaultde cada configuração com os valores preenchidos por cima. - Duas vias, mesmo conteúdo: a variável
ADE_PLUGIN_SETTINGS(JSON) e o camposettingsda mensagemhello. - 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
hellonovo.
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
helloe oADE_PLUGIN_SETTINGScarregam todos os valores; umlogcom 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
descriptionda configuração. - Valide o formato cedo (por exemplo, o prefixo esperado do token) e responda com um
errorclaro, 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.
| hello.settings.runNow | Trava | O que o sidecar faz |
|---|---|---|
| "true" | livre | Grava a trava e executa a ação. |
| "true" | consumida | Ignora (replay de restart, crash ou reabertura do app). |
| "false" | consumida | Rearma a trava (livre). Não executa. |
| "false" | livre | Nada. |
{
"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 — 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
descriptionque, para executar de novo, é preciso desmarcar, salvar e marcar de novo. - Variante com token: troque o boolean por uma configuração
string(por exemplorunToken) e execute quando o valor for diferente do último token gravado emstate.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
helloseguidos não correrem em paralelo sobre o mesmo estado.
Mais sobre logs, pasta de dados e idempotência em boas práticas.