@rclr-labs/dsh-plugin-qa 0.0.0-stage → 0.1.0

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
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # @rclr-labs/dsh-plugin-qa
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **“Este plugin instala e boota num perfil limpo?”** — respondido executando, não opinando.
4
+
5
+ O DSH isola falha de plugin: um plugin que quebra no carregamento **não derruba mais o
6
+ boot** — o processo sai 0 e apenas avisa (`N entries did not activate`). Por isso
7
+ “instalou” nunca significou “funciona”, e nem CI nem o usuário percebem a diferença.
8
+ Num catálogo real, **93% dos plugins falhavam na instalação**. Este pacote é o nosso
9
+ `npm run verify` empacotado para terceiros: ele instala a spec num `DSH_HOME`
10
+ descartável, **boota de verdade** e lê os avisos de ativação.
11
+
12
+ ## O que ele faz
13
+
14
+ Para cada spec (diretório local, tarball `.tgz` ou nome no registry):
15
+
16
+ 1. cria um perfil descartável (template `headless`, `DSH_HOME` temporário — o seu perfil real não é tocado);
17
+ 2. instala com o **`dsh plugin add` real** (não é simulação de `node_modules`);
18
+ 3. boota e parseia a saída de ativação;
19
+ 4. descarta o perfil (inclusive em falha) e devolve um relatório estruturado:
20
+
21
+ ```json
22
+ {
23
+ "spec": "packages/meu-plugin",
24
+ "nome": "dsh-meu-plugin",
25
+ "versao": "1.2.0",
26
+ "instala": true,
27
+ "boota": false,
28
+ "entradasNaoAtivadas": [
29
+ { "id": "meu-plugin", "pacote": "dsh-meu-plugin", "motivo": "failed to import", "detalhe": "..." }
30
+ ],
31
+ "camadas": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless", "dsh-meu-plugin"],
32
+ "erro": "1 entrada(s) não ativaram",
33
+ "duracaoMs": 908
34
+ }
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ ```bash
40
+ # no checkout deste repo
41
+ node packages/plugin-qa/bin/plugin-qa.mjs check packages/meu-plugin
42
+
43
+ # instalado globalmente
44
+ npm i -g @rclr-labs/dsh-plugin-qa
45
+ plugin-qa check packages/meu-plugin
46
+ plugin-qa check dsh-context --strict
47
+ plugin-qa check packages/a dist/b.tgz --json # várias specs, resultado agregado
48
+ ```
49
+
50
+ | opção | efeito |
51
+ |---|---|
52
+ | `--json` | imprime o selo agregado em JSON (consumível por CI/agente) |
53
+ | `--strict` | exit 1 se **qualquer** spec não instalar ou não bootar |
54
+
55
+ Exit codes: `0` = verificação concluída (sem `--strict`, mesmo reprovado); `1` = `--strict`
56
+ e alguma spec reprovada; `2` = erro de uso/infraestrutura.
57
+
58
+ ## Como plugin (ferramenta para o agente)
59
+
60
+ ```bash
61
+ dsh plugin --profile <perfil> add @rclr-labs/dsh-plugin-qa
62
+ ```
63
+
64
+ O pacote traz `cordis.patch.yml` no `files` e registra a ferramenta **`plugin_qa`**
65
+ (`spec` obrigatória, `strict` opcional). O agente passa a poder responder sozinho
66
+ “esse plugin instala e boota?” com a mesma prova que o CLI produz.
67
+
68
+ ## O selo
69
+
70
+ O resultado agregado é o selo:
71
+
72
+ ```json
73
+ { "selo": "aprovado", "ok": true, "total": 2, "aprovados": 2, "reprovados": 0, "resultados": [ ... ] }
74
+ ```
75
+
76
+ - **aprovado** — toda spec `instala: true` **e** `boota: true` (nenhuma entrada ficou de fora);
77
+ - **reprovado** — qualquer spec que não instale, ou que instale mas deixe alguma entrada sem ativar.
78
+
79
+ `boota` exige que a árvore tenha montado **e** que zero entradas tenham ficado de fora.
80
+ O código de saída do boot não é usado como sinal: o `headless` sem tarefa sai 1 por uso
81
+ (“a task is required”) depois de montar a árvore — o sinal é a fase de montagem.
82
+
83
+ ## Verificação deste pacote (no checkout)
84
+
85
+ ```bash
86
+ node packages/plugin-qa/test/plugin-qa.test.mjs # o teste que falha sem o pacote
87
+ node test-support/compat.mjs --spec=packages/plugin-qa
88
+ node test-support/profile.mjs --demo --spec=packages/plugin-qa
89
+ node packages/plugin-qa/bin/plugin-qa.mjs check packages/locale-pt-br # dogfooding
90
+ ```
91
+
92
+ ## Limitações honestas
93
+
94
+ - **Não julga qualidade.** Não lê o código, não avalia intenção, segurança, licença nem
95
+ desempenho — responde só “instala e boota num perfil limpo?”. Não substitui revisão de código.
96
+ - **Não é análise estática.** É uma prova de execução: exige o CLI do DSH instalado
97
+ (`DSH_BIN` ou o app Desktop) e uma instalação real da spec (registry/rede quando for o caso).
98
+ - **Um perfil por spec, em sequência.** Determinístico e limpo, mas não paralelo; ~1s por spec
99
+ local depois do primeiro boot.
100
+ - **O harness de perfil vive fora do pacote.** O núcleo importa
101
+ `test-support/profile.mjs` do checkout (`../../../test-support/profile.mjs`), então o
102
+ `verify` roda **no checkout**. Um pacote instalado isoladamente ativa como plugin (a linha
103
+ compõe), mas o CLI/tool exige o checkout do `dsh-plugins` até que o harness seja embutido.
104
+ - **Specs locais resolvem a partir da raiz do checkout**, como em `profile.addSpec`.
105
+ - Um perfil limpo do template `headless` **não** é o perfil do usuário: um plugin que boota
106
+ aqui ainda pode conflitar com as outras camadas que você instalou.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * plugin-qa — "este plugin instala e boota num perfil limpo?"
4
+ *
5
+ * plugin-qa check <spec> [<spec>...] [--json] [--strict]
6
+ *
7
+ * A spec é aceita em qualquer forma que o `dsh plugin add` aceite: diretório
8
+ * local, `.tgz` empacotado ou nome no registry. Cada spec roda num DSH_HOME
9
+ * descartável e é sempre limpa — inclusive em falha.
10
+ *
11
+ * `--json` imprime o selo agregado (consumível por CI/agente).
12
+ * `--strict` devolve exit 1 quando alguma spec não instala ou não boota.
13
+ *
14
+ * Por que existe: o DSH isola a falha do plugin — o boot sai 0 e só avisa — então
15
+ * "instalou" nunca foi o mesmo que "funciona". Este é o nosso `npm run verify`
16
+ * empacotado para terceiros.
17
+ */
18
+
19
+ import { verificar } from '../lib/core.mjs'
20
+
21
+ const argv = process.argv.slice(2)
22
+ const flag = (nome) => argv.includes(`--${nome}`)
23
+ const comando = argv.find((a) => !a.startsWith('-')) ?? null
24
+ const specs = argv.filter((a) => !a.startsWith('-') && a !== comando)
25
+
26
+ const uso = () => {
27
+ console.log(`
28
+ plugin-qa — este plugin instala e boota num perfil limpo?
29
+
30
+ uso:
31
+ plugin-qa check <spec> [<spec>...] [--json] [--strict]
32
+
33
+ spec:
34
+ diretório local (ex.: packages/meu-plugin), tarball (.tgz) ou nome no registry
35
+
36
+ opções:
37
+ --json imprime o selo agregado em JSON
38
+ --strict exit 1 se alguma spec não instalar ou não bootar
39
+
40
+ exemplos:
41
+ plugin-qa check packages/meu-plugin
42
+ plugin-qa check dsh-context --strict
43
+ plugin-qa check packages/a dist/b.tgz --json
44
+ `)
45
+ }
46
+
47
+ if (flag('help')) {
48
+ uso()
49
+ process.exit(0)
50
+ }
51
+
52
+ if (!comando) {
53
+ uso()
54
+ process.exit(2)
55
+ }
56
+
57
+ if (comando !== 'check') {
58
+ console.error(` ✗ comando desconhecido: ${comando} (use: check)`)
59
+ process.exit(2)
60
+ }
61
+
62
+ if (!specs.length) {
63
+ console.error(' ✗ falta pelo menos uma spec. Ex.: plugin-qa check packages/meu-plugin')
64
+ process.exit(2)
65
+ }
66
+
67
+ let selo
68
+ try {
69
+ selo = await verificar(specs, {
70
+ bin: process.env.DSH_BIN,
71
+ timeoutMs: Number(process.env.QA_TIMEOUT_MS ?? 300_000),
72
+ bootTimeoutMs: Number(process.env.QA_BOOT_TIMEOUT_MS ?? 60_000),
73
+ })
74
+ } catch (err) {
75
+ // Só chega aqui um erro de infraestrutura (ex.: harness de perfil ausente).
76
+ console.error(` ✗ ${err?.message ?? err}`)
77
+ if (flag('json')) console.log(JSON.stringify({ selo: 'reprovado', ok: false, erro: String(err?.message ?? err), resultados: [] }, null, 2))
78
+ process.exit(2)
79
+ }
80
+
81
+ if (flag('json')) {
82
+ console.log(JSON.stringify(selo, null, 2))
83
+ } else {
84
+ console.log(`\n plugin-qa — "instala e boota num perfil limpo?"\n`)
85
+ for (const r of selo.resultados) {
86
+ const aprovado = r.instala && r.boota
87
+ console.log(` ${aprovado ? '✓' : '✗'} ${r.spec}`)
88
+ console.log(` nome: ${r.nome ?? '—'}${r.versao ? `@${r.versao}` : ''}`)
89
+ console.log(` instala: ${r.instala ? 'sim' : 'NÃO'}`)
90
+ console.log(` boota: ${r.boota ? 'sim' : 'NÃO'}`)
91
+ console.log(` camadas: ${r.camadas.length}`)
92
+ if (r.entradasNaoAtivadas.length) {
93
+ console.log(` não ativou:`)
94
+ for (const e of r.entradasNaoAtivadas) console.log(` - ${e.id} (${e.pacote}): ${e.motivo}`)
95
+ }
96
+ if (r.erro) console.log(` erro: ${r.erro}`)
97
+ console.log(` tempo: ${r.duracaoMs}ms`)
98
+ console.log()
99
+ }
100
+ console.log(` selo: ${selo.selo} (${selo.aprovados}/${selo.total} aprovados)\n`)
101
+ if (selo.selo === 'reprovado' && !flag('strict')) {
102
+ console.log(' use --strict para transformar o selo em código de saída.\n')
103
+ }
104
+ }
105
+
106
+ const exit = flag('strict') && !selo.ok ? 1 : 0
107
+ process.exit(exit)
@@ -0,0 +1,8 @@
1
+ # O plugin-qa registra a própria linha na árvore do perfil.
2
+ #
3
+ # Sem este arquivo no `files` do package.json, o pacote instalado do registry não
4
+ # ativa nenhuma camada e o DSH RECUSA a instalação — foi o defeito real do
5
+ # locale-pt-BR em 02/10/2026.
6
+ - insert:
7
+ - id: plugin-qa
8
+ name: '@rclr-labs/dsh-plugin-qa'
package/lib/core.mjs ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Núcleo do plugin-qa — pura, sem dependência do runtime do DSH.
3
+ *
4
+ * Responde a UMA pergunta, com prova executável: **esta spec instala e boota num
5
+ * perfil limpo?** É a pergunta que o ecossistema não respondia — 93% dos plugins
6
+ * de um catálogo falhavam na instalação, e o boot do DSH isola a falha (sai 0 e
7
+ * só avisa) — então nem CI nem o usuário percebem.
8
+ *
9
+ * Desenho:
10
+ * 1. `test-support/profile.mjs` cria o perfil descartável (DSH_HOME temporário);
11
+ * NADA é escrito fora do tmp e o perfil é sempre descartado.
12
+ * 2. a spec é instalada pelo próprio `dsh plugin add` (aceita diretório, `.tgz`
13
+ * ou nome no registry) — é o caminho REAL de instalação, não uma simulação.
14
+ * 3. o boot é exercitado e a saída é lida de verdade: o DSH emite
15
+ * `warning: N entries did not activate` + `<id> (<pacote>): <motivo>`.
16
+ * Como o boot sem tarefa sai 1 por uso ("a task is required"), o código de
17
+ * saída NÃO é o sinal — o sinal é a árvore ter montado e nenhuma entrada
18
+ * ter ficado de fora.
19
+ *
20
+ * Reuso deliberado: o harness de perfil vive FORA do pacote
21
+ * (`test-support/profile.mjs`). O `verify` roda no checkout do dsh-plugins; um
22
+ * pacote publicado não embute o harness (limitação documentada no README).
23
+ */
24
+
25
+ import fs from 'node:fs'
26
+ import path from 'node:path'
27
+ import { spawn } from 'node:child_process'
28
+ import { fileURLToPath, pathToFileURL } from 'node:url'
29
+
30
+ const AQUI = path.dirname(fileURLToPath(import.meta.url))
31
+
32
+ /** `test-support/profile.mjs` do checkout (três níveis acima de lib/). */
33
+ export const HELPER_PROFILE = path.resolve(AQUI, '..', '..', '..', 'test-support', 'profile.mjs')
34
+ /** Raiz do checkout — a MESMA base que `profile.addSpec` usa para resolver specs locais. */
35
+ export const RAIZ_CHECKOUT = path.resolve(path.dirname(HELPER_PROFILE), '..')
36
+
37
+ /** Sinais de que o DSH terminou de montar a árvore (o plugin pode estar quebrado, mas montou). */
38
+ const MONTAGEM = /a task is required|did not activate/
39
+ /** `dsh: warning: 2 entries did not activate` */
40
+ const AVISO = /warning:\s*(\d+)\s+entr(?:y|ies) did not activate/
41
+
42
+ const primeiraLinha = (s) => String(s ?? '').split('\n').find((l) => l.trim())?.trim() ?? ''
43
+ const ultimaLinha = (s) => String(s ?? '').trim().split('\n').filter(Boolean).pop()?.trim() ?? ''
44
+
45
+ /**
46
+ * Lê os avisos de ativação do boot.
47
+ * Formato: `<id> (<pacote>): <motivo>`, com linhas de continuação indentadas.
48
+ */
49
+ export function parseNaoAtivadas(saida) {
50
+ const texto = String(saida ?? '')
51
+ const declarado = Number(texto.match(AVISO)?.[1] ?? 0)
52
+ const linhas = texto.split('\n')
53
+ const entradas = []
54
+
55
+ for (let i = 0; i < linhas.length; i++) {
56
+ const m = linhas[i].match(/^(\S+)\s+\(([^)]+)\):\s*(.+)$/)
57
+ if (!m) continue
58
+ const [, id, pacote, motivo] = m
59
+ const detalhe = [motivo]
60
+ for (let j = i + 1; j < linhas.length; j++) {
61
+ if (!/^\s{2,}\S/.test(linhas[j])) break
62
+ detalhe.push(linhas[j])
63
+ }
64
+ entradas.push({ id, pacote, motivo, detalhe: detalhe.join('\n').trimEnd() })
65
+ }
66
+ return { declarado, entradas }
67
+ }
68
+
69
+ /** Deriva o nome npm de uma spec (`dsh-x`, `@escopo/nome@1.2.3`) — sem tocar na rede. */
70
+ export function nomeDeSpecNpm(spec) {
71
+ if (!spec || spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('file:') || spec.endsWith('.tgz')) return null
72
+ if (spec.startsWith('@')) {
73
+ const barra = spec.indexOf('/')
74
+ if (barra === -1) return spec
75
+ const arroba = spec.indexOf('@', barra)
76
+ return arroba === -1 ? spec : spec.slice(0, arroba)
77
+ }
78
+ const arroba = spec.indexOf('@')
79
+ return arroba === -1 ? spec : spec.slice(0, arroba)
80
+ }
81
+
82
+ /** Diretório local correspondente à spec, na mesma base do `profile.addSpec`. */
83
+ function caminhoLocal(spec) {
84
+ for (const base of [RAIZ_CHECKOUT, process.cwd()]) {
85
+ const abs = path.resolve(base, spec)
86
+ try {
87
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs
88
+ } catch {
89
+ /* segue */
90
+ }
91
+ }
92
+ return null
93
+ }
94
+
95
+ function lerManifesto(dir) {
96
+ try {
97
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
98
+ } catch {
99
+ return null
100
+ }
101
+ }
102
+
103
+ function resolverNome(spec, novas) {
104
+ const local = caminhoLocal(spec)
105
+ if (local) {
106
+ const pkg = lerManifesto(local)
107
+ if (pkg?.name) return pkg.name
108
+ }
109
+ if (novas.length) return novas[0]
110
+ return nomeDeSpecNpm(spec)
111
+ }
112
+
113
+ function lerVersao(perfil, spec, nome) {
114
+ if (nome) {
115
+ const pkg = lerManifesto(path.join(perfil.dir, 'node_modules', ...nome.split('/')))
116
+ if (pkg?.version) return pkg.version
117
+ }
118
+ const local = caminhoLocal(spec)
119
+ return local ? (lerManifesto(local)?.version ?? null) : null
120
+ }
121
+
122
+ /** Importa o harness de perfil do checkout, com erro explícito se ele não existir. */
123
+ export async function carregarHelper() {
124
+ if (!fs.existsSync(HELPER_PROFILE)) {
125
+ throw new Error(
126
+ `o núcleo do plugin-qa reutiliza test-support/profile.mjs do checkout (${HELPER_PROFILE}) e ele não existe. ` +
127
+ `Rode a partir do checkout do dsh-plugins; o pacote publicado ainda não embute o harness.`,
128
+ )
129
+ }
130
+ return import(pathToFileURL(HELPER_PROFILE).href)
131
+ }
132
+
133
+ /**
134
+ * Boota o perfil e devolve a saída crua.
135
+ *
136
+ * `stdio[0] = 'ignore'` é essencial: o headless lê a tarefa do stdin e, com um
137
+ * pipe aberto que nunca fecha, ficaria pendurado até o timeout — foi assim que um
138
+ * teste existente passou a medir timeout como se fosse "boot exit 0".
139
+ */
140
+ export function bootar(bin, nomePerfil, env, { timeoutMs = 60_000 } = {}) {
141
+ return new Promise((resolve) => {
142
+ let finalizado = false
143
+ let timer = null
144
+ let saida = ''
145
+ const encerrar = (r) => {
146
+ if (finalizado) return
147
+ finalizado = true
148
+ if (timer) clearTimeout(timer)
149
+ resolve(r)
150
+ }
151
+
152
+ let child
153
+ try {
154
+ child = spawn(bin, ['--profile', nomePerfil], { env, stdio: ['ignore', 'pipe', 'pipe'] })
155
+ } catch (err) {
156
+ resolve({ code: null, saida: '', timeout: false, erro: String(err?.message ?? err) })
157
+ return
158
+ }
159
+
160
+ timer = setTimeout(() => {
161
+ encerrar({ code: null, saida, timeout: true, erro: `o boot não terminou em ${timeoutMs}ms` })
162
+ try {
163
+ child.kill('SIGKILL')
164
+ } catch {
165
+ /* já morreu */
166
+ }
167
+ }, timeoutMs)
168
+
169
+ child.stdout?.on('data', (d) => (saida += d))
170
+ child.stderr?.on('data', (d) => (saida += d))
171
+ child.on('error', (err) => encerrar({ code: null, saida, timeout: false, erro: String(err?.message ?? err) }))
172
+ child.on('close', (code) => encerrar({ code, saida, timeout: false, erro: null }))
173
+ })
174
+ }
175
+
176
+ /**
177
+ * Verifica UMA spec num perfil limpo e descartável.
178
+ *
179
+ * Relatório: `{ spec, nome, versao, instala, boota, entradasNaoAtivadas, camadas, erro, duracaoMs }`.
180
+ * - `instala` — a spec entrou em `dsh.profile.bundles` (a instalação real aceitou o pacote);
181
+ * - `boota` — a árvore montou e NENHUMA entrada ficou de fora;
182
+ * - `erro` — o motivo, quando instalar ou bootar não puderam ser confirmados.
183
+ */
184
+ export async function checkSpec(spec, { bin, timeoutMs = 300_000, bootTimeoutMs = 60_000 } = {}) {
185
+ const t0 = Date.now()
186
+ const relatorio = {
187
+ spec,
188
+ nome: null,
189
+ versao: null,
190
+ instala: false,
191
+ boota: false,
192
+ entradasNaoAtivadas: [],
193
+ camadas: [],
194
+ erro: null,
195
+ }
196
+
197
+ let perfil
198
+ try {
199
+ const { createProfile } = await carregarHelper()
200
+ perfil = await createProfile({ bin })
201
+ try {
202
+ await perfil.init()
203
+
204
+ const antes = perfil.bundles()
205
+ await perfil.addSpec(spec, { timeoutMs })
206
+ const depois = perfil.bundles()
207
+ const novas = depois.filter((b) => !antes.includes(b))
208
+
209
+ relatorio.nome = resolverNome(spec, novas)
210
+ relatorio.versao = lerVersao(perfil, spec, relatorio.nome)
211
+ relatorio.instala = Boolean(relatorio.nome) && depois.includes(relatorio.nome)
212
+
213
+ if (!relatorio.instala) {
214
+ relatorio.erro = `a instalação não registrou nenhuma camada em dsh.profile.bundles (bundles: ${depois.join(', ') || '—'})`
215
+ }
216
+
217
+ try {
218
+ relatorio.camadas = await perfil.layers()
219
+ } catch (err) {
220
+ relatorio.erro ??= `não consegui ler a composição: ${primeiraLinha(err?.message)}`
221
+ }
222
+
223
+ const boot = await bootar(perfil.bin, perfil.name, perfil.env, { timeoutMs: bootTimeoutMs })
224
+ const { declarado, entradas } = parseNaoAtivadas(boot.saida)
225
+ relatorio.entradasNaoAtivadas = entradas
226
+
227
+ if (boot.timeout) {
228
+ relatorio.boota = false
229
+ relatorio.erro ??= boot.erro
230
+ } else if (!MONTAGEM.test(boot.saida)) {
231
+ relatorio.boota = false
232
+ relatorio.erro ??= `o boot não chegou a montar a árvore: ${ultimaLinha(boot.saida) || 'sem saída'}`
233
+ } else if (declarado > entradas.length) {
234
+ // O DSH mudou o formato do aviso: melhor reprovar alto do que aprovar sem prova.
235
+ relatorio.boota = false
236
+ relatorio.erro ??= `o DSH avisou ${declarado} entrada(s) não ativada(s) e só consegui nomear ${entradas.length} — formato do aviso mudou`
237
+ } else {
238
+ relatorio.boota = entradas.length === 0
239
+ if (!relatorio.boota) relatorio.erro ??= `${entradas.length} entrada(s) não ativaram`
240
+ }
241
+ } finally {
242
+ perfil.dispose()
243
+ }
244
+ } catch (err) {
245
+ const detalhe = primeiraLinha(err?.message ?? err)
246
+ // Distingue "a spec não instala" de uma falha do próprio harness.
247
+ relatorio.erro = relatorio.instala ? detalhe : `não consegui instalar/verificar "${spec}": ${detalhe}`
248
+ }
249
+
250
+ relatorio.duracaoMs = Date.now() - t0
251
+ return relatorio
252
+ }
253
+
254
+ /** Agrega relatórios num "selo" consumível por pessoa, CI ou agente. */
255
+ export function agregar(resultados) {
256
+ const aprovados = resultados.filter((r) => r.instala && r.boota).length
257
+ const reprovados = resultados.length - aprovados
258
+ const ok = resultados.length > 0 && reprovados === 0
259
+ return {
260
+ selo: ok ? 'aprovado' : 'reprovado',
261
+ ok,
262
+ total: resultados.length,
263
+ aprovados,
264
+ reprovados,
265
+ resultados,
266
+ }
267
+ }
268
+
269
+ /** Verifica N specs, em sequência (um perfil descartável por spec). */
270
+ export async function verificar(specs, opcoes = {}) {
271
+ const resultados = []
272
+ for (const spec of specs) resultados.push(await checkSpec(spec, opcoes))
273
+ return agregar(resultados)
274
+ }
package/lib/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Metade DSH do plugin-qa: registra a ferramenta `plugin_qa` no agente.
3
+ *
4
+ * Fina de propósito — toda a lógica vive em `core.mjs` e no CLI, para que a
5
+ * resposta seja a mesma quando rodada por uma pessoa, por CI ou por um agente.
6
+ */
7
+
8
+ import path from 'node:path'
9
+ import { execFile } from 'node:child_process'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { defineTool } from '@deepseek-ai/dsh-tools'
12
+
13
+ export const name = 'plugin-qa'
14
+ export const inject = ['tools']
15
+
16
+ const AQUI = path.dirname(fileURLToPath(import.meta.url))
17
+ const CLI = path.join(AQUI, '..', 'bin', 'plugin-qa.mjs')
18
+
19
+ /** Roda o CLI do plugin-qa e devolve a saída como texto. */
20
+ function rodarCli(args) {
21
+ return new Promise((resolve) => {
22
+ execFile(process.execPath, [CLI, ...args], { maxBuffer: 32e6, timeout: 600_000 }, (err, stdout, stderr) => {
23
+ const saida = (stdout || '').trim() || (stderr || '').trim()
24
+ resolve(saida || `plugin-qa falhou: ${err?.message ?? 'erro desconhecido'}`)
25
+ })
26
+ })
27
+ }
28
+
29
+ export function apply(ctx) {
30
+ ctx.tools.register(
31
+ defineTool({
32
+ name: 'plugin_qa',
33
+ description:
34
+ 'Answer "does this plugin install and boot in a clean DeepSeek Harness profile?" by actually doing it: ' +
35
+ 'a disposable DSH_HOME, a real `dsh plugin add`, and a real boot whose activation warnings are parsed. ' +
36
+ 'The harness isolates a broken plugin (the boot exits 0 and only warns), so "it installed" never meant ' +
37
+ '"it works" — this tool reports install/boot separately, names every entry that did not activate, and ' +
38
+ 'returns a machine-readable verdict. Accepts a local directory, a .tgz tarball, or an npm name. ' +
39
+ 'It judges installability and activation only, not code quality or security.',
40
+ parameters: {
41
+ spec: {
42
+ type: 'string',
43
+ required: true,
44
+ description: 'Pacote a verificar: diretório local (ex.: packages/meu-plugin), tarball .tgz ou nome no registry',
45
+ },
46
+ strict: {
47
+ type: 'boolean',
48
+ description: 'Quando true, falha também em código de saída se a spec não instalar ou não bootar (útil em CI)',
49
+ },
50
+ },
51
+ output: {
52
+ schema: { type: 'string' },
53
+ render: (_args, value) => [{ type: 'text', text: value }],
54
+ },
55
+ async execute(args) {
56
+ return rodarCli(['check', args.spec, ...(args.strict ? ['--strict'] : [])])
57
+ },
58
+ }),
59
+ )
60
+ }
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@rclr-labs/dsh-plugin-qa",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "QA de plugins do DeepSeek Harness: verifica, num perfil limpo e descartável, se um pacote instala e boota de verdade — e devolve um selo consumível por CI",
6
+ "license": "MIT",
7
+ "author": "Rafael Nunes",
8
+ "homepage": "https://github.com/rclr-labs/dsh-plugins/tree/main/packages/plugin-qa",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/rclr-labs/dsh-plugins.git",
12
+ "directory": "packages/plugin-qa"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/rclr-labs/dsh-plugins/issues"
16
+ },
17
+ "keywords": [
18
+ "dsh-plugin",
19
+ "deepseek-harness",
20
+ "dsh",
21
+ "qa",
22
+ "verification",
23
+ "install-check",
24
+ "cordis-plugin"
25
+ ],
26
+ "engines": {
27
+ "node": ">=22.19"
28
+ },
29
+ "bin": {
30
+ "plugin-qa": "bin/plugin-qa.mjs"
31
+ },
32
+ "main": "lib/index.js",
33
+ "exports": {
34
+ ".": "./lib/index.js",
35
+ "./core": "./lib/core.mjs",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "bin/",
40
+ "lib/",
41
+ "cordis.patch.yml",
42
+ "README.md"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "qa": "node bin/plugin-qa.mjs",
49
+ "test": "node test/plugin-qa.test.mjs",
50
+ "verify": "node test/plugin-qa.test.mjs"
51
+ },
52
+ "dsh": {
53
+ "manifestVersion": 1,
54
+ "bundle": {
55
+ "patch": "./cordis.patch.yml"
56
+ }
57
+ },
58
+ "peerDependencies": {
59
+ "@deepseek-ai/cordis": "^4.0.4",
60
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.1"
61
+ },
62
+ "devDependencies": {
63
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2"
64
+ }
65
+ }