terminal-smart-cli 0.64.0 → 0.66.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/bin/ts.js +58 -1
- package/lib/i18n.js +2 -0
- package/lib/meta.js +20 -5
- package/lib/verify.js +104 -0
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -1183,6 +1183,14 @@ async function metaCmd() {
|
|
|
1183
1183
|
const intervaloMin = flagNum('--intervalo', 5); // min de espera entre janelas ao aguardar crédito
|
|
1184
1184
|
// Anti-loop-infinito: se ninguém definiu teto, cai num limite de janelas seguro.
|
|
1185
1185
|
const maxJanelasEff = maxJanelas || ((budgetTotal || maxHoras) ? 0 : 20);
|
|
1186
|
+
// CRITÉRIOS EXECUTÁVEIS (verify): --criterios <arquivo.json> = a PROVA de que a missão
|
|
1187
|
+
// cumpriu o pedido (não basta compilar/abrir). Falhou → missão sai "NÃO verificada".
|
|
1188
|
+
let criteria = [];
|
|
1189
|
+
const _critFile = (() => { const i = rawArgs.findIndex(a => a === '--criterios' || a === '--criteria'); return i >= 0 ? rawArgs[i + 1] : null; })();
|
|
1190
|
+
if (_critFile) {
|
|
1191
|
+
try { const j = JSON.parse(require('fs').readFileSync(_critFile, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
|
|
1192
|
+
catch (e) { console.error(ui.errLine((cfg.lang === 'en' ? 'criteria file: ' : 'arquivo de critérios: ') + (e.message || e))); process.exit(2); }
|
|
1193
|
+
}
|
|
1186
1194
|
|
|
1187
1195
|
const existing = metaMod.load(dir);
|
|
1188
1196
|
if (FLAGS.has('--status')) {
|
|
@@ -1229,7 +1237,7 @@ async function metaCmd() {
|
|
|
1229
1237
|
const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
|
|
1230
1238
|
try {
|
|
1231
1239
|
st = await metaMod.run(goalArg, {
|
|
1232
|
-
token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch,
|
|
1240
|
+
token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria,
|
|
1233
1241
|
onAlert: ({ type, text }) => {
|
|
1234
1242
|
sp.stop();
|
|
1235
1243
|
const ic = type === 'human' ? C.warn('🙋') : type === 'escalate' ? C.indigo('🧠') : type === 'stagnated' ? C.err('🛑') : type === 'design' ? C.cyan('🎨') : type === 'retry' || type === 'conn' ? C.warn('📡') : C.warn('⏱');
|
|
@@ -1304,6 +1312,11 @@ async function metaCmd() {
|
|
|
1304
1312
|
const _vtag = (_vl.ok ? C.ok : C.warn)(` · ${_vl.icon} ${_vl.txt}`);
|
|
1305
1313
|
console.log((_vl.ok ? ui.okLine : ui.infoLine)(C.bold(T.meta_done) + C.dim(` · ${st.rounds.length} rodada(s) · ${st.creditsSpent} créditos · ${secs} min` + (st.escalations ? ` · ${st.escalations} escalonamento(s)` : '')) + _vtag));
|
|
1306
1314
|
if (!_vl.ok) console.log(' ' + C.dim((cfg.lang === 'en' ? 'Heads up: the deliverable was produced but NOT fully proven — check it before relying on it.' : 'Atenção: o entregável foi produzido mas NÃO foi totalmente provado — confira antes de confiar.') + (st.runNote ? ' (' + String(st.runNote).trim() + ')' : '')));
|
|
1315
|
+
// Evidence Ledger da missão: mostra cada critério executável provado/falho.
|
|
1316
|
+
if (st.verifyReport && st.verifyReport.total) {
|
|
1317
|
+
console.log(' ' + C.dim((cfg.lang === 'en' ? 'Criteria: ' : 'Critérios: ') + st.verifyReport.passed + '/' + st.verifyReport.total));
|
|
1318
|
+
for (const r of st.verifyReport.results) console.log(' ' + (r.ok ? C.ok('✔') : C.err('✗')) + ' ' + C.dim((r.label || '') + (r.ok ? '' : ' — ' + (r.detail || ''))));
|
|
1319
|
+
}
|
|
1307
1320
|
metaMod.notify(token, T.meta_notify_done(st.goal, done, st.creditsSpent) + (_vl.ok ? '' : `\n⚠️ ${_vl.txt}`));
|
|
1308
1321
|
} else if (st.pause_reason === 'awaiting_human' && st.humanRequest) {
|
|
1309
1322
|
console.log(ui.infoLine(C.bold('🙋 Preciso de você: ') + st.humanRequest.motivo));
|
|
@@ -1671,6 +1684,49 @@ function buscarCmd(args) {
|
|
|
1671
1684
|
], { title: 'ts buscar' }));
|
|
1672
1685
|
}
|
|
1673
1686
|
|
|
1687
|
+
// ── ts verificar — PROVA determinística de que a tarefa terminou (verificadores) ──
|
|
1688
|
+
async function verificarCmd(args) {
|
|
1689
|
+
const en = (cfg.lang === 'en');
|
|
1690
|
+
const _fs = require('fs');
|
|
1691
|
+
const verify = require('../lib/verify');
|
|
1692
|
+
const _val = (names) => { const i = rawArgs.findIndex(a => names.includes(a)); return i >= 0 ? rawArgs[i + 1] : null; };
|
|
1693
|
+
const cwd = process.cwd();
|
|
1694
|
+
|
|
1695
|
+
// 1) arquivo de critérios (JSON: array ou {criteria:[...]}) OU 2) critérios inline via flags
|
|
1696
|
+
let criteria = [];
|
|
1697
|
+
const file = (args || []).find(a => !a.startsWith('-') && /\.json$/i.test(a));
|
|
1698
|
+
if (file) {
|
|
1699
|
+
try { const j = JSON.parse(_fs.readFileSync(file, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
|
|
1700
|
+
catch (e) { console.log(ui.errLine((en ? 'could not read criteria file: ' : 'não deu pra ler o arquivo de critérios: ') + (e.message || e))); process.exit(2); }
|
|
1701
|
+
} else {
|
|
1702
|
+
const cmd = _val(['--cmd', '--comando']); if (cmd) criteria.push({ type: 'command', cmd, exit: Number(_val(['--exit', '--codigo'])) || 0 });
|
|
1703
|
+
const ex = _val(['--existe', '--exists']); if (ex) criteria.push({ type: 'file_exists', path: ex });
|
|
1704
|
+
const arq = _val(['--arquivo', '--file']); const cont = _val(['--contem', '--contains']);
|
|
1705
|
+
if (arq && cont != null) criteria.push({ type: 'file_contains', path: arq, text: cont });
|
|
1706
|
+
const url = _val(['--url']); if (url) criteria.push({ type: 'http', url, status: Number(_val(['--status'])) || 200, ...(cont != null && !arq ? { contains: cont } : {}) });
|
|
1707
|
+
const porta = _val(['--porta', '--port']); if (porta) criteria.push({ type: 'port', port: Number(porta) });
|
|
1708
|
+
}
|
|
1709
|
+
if (!criteria.length) {
|
|
1710
|
+
console.log(ui.infoLine(en
|
|
1711
|
+
? 'usage: ts verificar criterios.json OR ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'
|
|
1712
|
+
: 'uso: ts verificar criterios.json OU ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'));
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
|
|
1717
|
+
const rep = await verify.runAll(criteria, { cwd });
|
|
1718
|
+
if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
|
|
1719
|
+
|
|
1720
|
+
for (const r of rep.results) {
|
|
1721
|
+
const mark = r.ok ? C.ok('✔') : C.err('✗');
|
|
1722
|
+
console.log(' ' + mark + ' ' + C.bold(r.label || r.type) + C.dim(' ' + (r.detail || '')) + (r.errorClass ? C.dim(' [' + r.errorClass + ']') : ''));
|
|
1723
|
+
if (!r.ok && r.evidence) console.log(' ' + C.dim('┄ ' + String(r.evidence).slice(0, 120)));
|
|
1724
|
+
}
|
|
1725
|
+
console.log('\n' + (rep.allOk ? ui.okLine : ui.errLine)(
|
|
1726
|
+
(rep.allOk ? (en ? 'VERIFIED' : 'VERIFICADO') : (en ? 'NOT verified' : 'NÃO verificado')) + C.dim(` ${rep.passed}/${rep.total} ` + (en ? 'passed' : 'ok'))));
|
|
1727
|
+
process.exit(rep.allOk ? 0 : 1);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1674
1730
|
// ── ts perfil — personas com COFRE DE MEMÓRIA isolado (Mempalace/Hermes) ──────
|
|
1675
1731
|
function perfilCmd(args) {
|
|
1676
1732
|
const en = (cfg.lang === 'en');
|
|
@@ -1933,6 +1989,7 @@ function recallCmd(args) {
|
|
|
1933
1989
|
case 'indexar': case 'index': return indexarCmd();
|
|
1934
1990
|
case 'buscar': case 'search': case 'procurar': return buscarCmd(POS.slice(1));
|
|
1935
1991
|
case 'perfil': case 'persona': case 'profile': return perfilCmd(POS.slice(1));
|
|
1992
|
+
case 'verificar': case 'verify': case 'provar': return verificarCmd(POS.slice(1));
|
|
1936
1993
|
case 'meta': case 'missao': case 'mission': return metaCmd();
|
|
1937
1994
|
case 'runs': return runsCmd();
|
|
1938
1995
|
case 'status': return statusCmd(POS[1]);
|
package/lib/i18n.js
CHANGED
|
@@ -43,8 +43,10 @@ const STR = {
|
|
|
43
43
|
['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
|
|
44
44
|
['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
|
|
45
45
|
['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
|
|
46
|
+
['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
|
|
46
47
|
['ts meta --status', 'estado da missão deste diretório'],
|
|
47
48
|
['ts eval suite.json', 'avalia o agente numa suíte de casos (juiz de IA + nota)'],
|
|
49
|
+
['ts verificar --cmd "npm test" --url ... --porta N', 'PROVA determinística de conclusão (build/http/porta/arquivo); exit 1 pra CI'],
|
|
48
50
|
['ts acp', 'servidor Agent Client Protocol (conecta o ts a editores tipo Zed)'],
|
|
49
51
|
['ts mcp', 'servidores MCP: as ferramentas deles entram no agente'],
|
|
50
52
|
] },
|
package/lib/meta.js
CHANGED
|
@@ -1001,6 +1001,20 @@ async function makeChecklist(goal, token) {
|
|
|
1001
1001
|
* onStep/askApprove/onRemote (repassados pro agente)
|
|
1002
1002
|
* Retorna o estado final {status: done|paused, ...}.
|
|
1003
1003
|
*/
|
|
1004
|
+
// CRITÉRIOS EXECUTÁVEIS (verify): a prova FINAL da missão. Rodados quando os gates de
|
|
1005
|
+
// build/run passam, ANTES de declarar 'verified'. Se algum critério do usuário falha, a
|
|
1006
|
+
// missão NÃO é verificada (honestidade — não basta compilar/abrir; tem que CUMPRIR o pedido).
|
|
1007
|
+
// Sem critérios → nada a provar (retorna true). runAll nunca lança.
|
|
1008
|
+
async function _checkCriteria(st, dir) {
|
|
1009
|
+
if (!st.criteria || !st.criteria.length) return true;
|
|
1010
|
+
try {
|
|
1011
|
+
const rep = await require('./verify').runAll(st.criteria, { cwd: dir });
|
|
1012
|
+
st.verifyReport = { passed: rep.passed, total: rep.total, allOk: rep.allOk, results: rep.results.map(r => ({ ok: r.ok, label: r.label, detail: r.detail })) };
|
|
1013
|
+
if (!rep.allOk) st.runNote = (st.runNote || '') + ` critérios: ${rep.passed}/${rep.total} passaram · falharam: ${rep.results.filter(r => !r.ok).map(r => r.label).slice(0, 3).join(' | ')}`;
|
|
1014
|
+
return rep.allOk;
|
|
1015
|
+
} catch (_) { return true; }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1004
1018
|
async function run(goal, opts = {}) {
|
|
1005
1019
|
const { token, lang = 'pt', yes = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true } = opts;
|
|
1006
1020
|
const onChecklist = opts.onChecklist || (() => {});
|
|
@@ -1043,7 +1057,7 @@ async function run(goal, opts = {}) {
|
|
|
1043
1057
|
}
|
|
1044
1058
|
const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
|
|
1045
1059
|
st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
|
|
1046
|
-
budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, created_at: new Date().toISOString() };
|
|
1060
|
+
budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: Array.isArray(opts.criteria) ? opts.criteria : [], created_at: new Date().toISOString() };
|
|
1047
1061
|
save(st, dir);
|
|
1048
1062
|
} else {
|
|
1049
1063
|
st.status = 'running';
|
|
@@ -1126,9 +1140,9 @@ async function run(goal, opts = {}) {
|
|
|
1126
1140
|
// Aprovado: impecável OU só com nitpicks cosméticos (não vale queimar rodada cara).
|
|
1127
1141
|
st.visualOk = true;
|
|
1128
1142
|
if (vg.severity === 'ressalvas') { st.runNote = (st.runNote || '') + ' visual aprovado com ressalvas cosméticas (sem defeito estrutural)'; onAlert({ type: 'design', text: `👁 ts: visual APROVADO — sobrou só nitpick cosmético (sem defeito estrutural). Não vou gastar polimento caro à toa.` }); }
|
|
1129
|
-
st.verification = 'verified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
|
|
1143
|
+
st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
|
|
1130
1144
|
}
|
|
1131
|
-
} else { st.visualOk = true; st.verification = 'verified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st; }
|
|
1145
|
+
} else { st.visualOk = true; st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified'; st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st; }
|
|
1132
1146
|
} else {
|
|
1133
1147
|
// CRASHOU ao abrir → trata como erro pro executor corrigir (loop igual ao build)
|
|
1134
1148
|
st.buildFixes = (st.buildFixes || 0) + 1;
|
|
@@ -1143,7 +1157,8 @@ async function run(goal, opts = {}) {
|
|
|
1143
1157
|
let wg = { ok: true }; try { wg = await webRunGate(b, { token, eye, eyeGate: !!eye && (st.visualFixes || 0) < MAX_VISUAL_FIXES }); } catch (_) {}
|
|
1144
1158
|
st.creditsSpent += wg.credits || 0;
|
|
1145
1159
|
if (wg.ok) {
|
|
1146
|
-
st.buildVerified = true; st.runVerified = true; st.visualOk = true;
|
|
1160
|
+
st.buildVerified = true; st.runVerified = true; st.visualOk = true;
|
|
1161
|
+
st.verification = (await _checkCriteria(st, dir)) ? 'verified' : 'unverified';
|
|
1147
1162
|
if (wg.shot) st.webShot = wg.shot;
|
|
1148
1163
|
st.status = 'done'; st.finished_at = new Date().toISOString(); save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
|
|
1149
1164
|
} else {
|
|
@@ -1381,4 +1396,4 @@ function verificationLabel(ver, lang) {
|
|
|
1381
1396
|
}
|
|
1382
1397
|
}
|
|
1383
1398
|
|
|
1384
|
-
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel };
|
|
1399
|
+
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel, _checkCriteria };
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// VERIFICADORES POR DOMÍNIO — a PROVA determinística de que uma tarefa terminou.
|
|
2
|
+
// Insight das análises externas (GPT): "o modelo propõe; o harness observa e COMPROVA".
|
|
3
|
+
// Cada critério é EXECUTÁVEL e checado por código (não pela narrativa do modelo nem por um
|
|
4
|
+
// LLM-judge). É a base do TaskSpec (critérios executáveis) e do Evidence Ledger, e o motor
|
|
5
|
+
// do comando `ts verificar` (o "Prove que terminou"). Usa o classificador de erro do core.
|
|
6
|
+
'use strict';
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const net = require('net');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const core = require('./core');
|
|
12
|
+
|
|
13
|
+
// ── verificadores individuais (cada um devolve { ok, detail }) ───────────────
|
|
14
|
+
function _abs(cwd, p) { return path.isAbsolute(p) ? p : path.join(cwd || process.cwd(), p); }
|
|
15
|
+
|
|
16
|
+
function vCommand(c, cwd) {
|
|
17
|
+
const want = c.exit != null ? Number(c.exit) : 0;
|
|
18
|
+
try {
|
|
19
|
+
const out = execSync(c.cmd, { cwd, encoding: 'utf8', timeout: (c.timeout_s || 120) * 1000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
20
|
+
const ok = want === 0;
|
|
21
|
+
return { ok, detail: ok ? 'exit 0' : `exit 0 mas esperava ${want}`, evidence: String(out).slice(-200).replace(/\s+/g, ' ') };
|
|
22
|
+
} catch (e) {
|
|
23
|
+
const code = typeof e.status === 'number' ? e.status : 1;
|
|
24
|
+
const ok = code === want;
|
|
25
|
+
const cls = core.classifyError(e.stderr || e.stdout || e.message);
|
|
26
|
+
return { ok, detail: ok ? `exit ${code} (esperado)` : `exit ${code} (esperava ${want})`, errorClass: ok ? null : cls, evidence: String(e.stderr || e.stdout || e.message || '').slice(-200).replace(/\s+/g, ' ') };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function vFileExists(c, cwd) { const p = _abs(cwd, c.path); const ok = fs.existsSync(p); return { ok, detail: ok ? 'existe' : 'não existe: ' + c.path }; }
|
|
30
|
+
function vFileAbsent(c, cwd) { const p = _abs(cwd, c.path); const ok = !fs.existsSync(p); return { ok, detail: ok ? 'ausente (ok)' : 'existe (não deveria): ' + c.path }; }
|
|
31
|
+
function vFileContains(c, cwd) {
|
|
32
|
+
const p = _abs(cwd, c.path);
|
|
33
|
+
let src = null; try { src = fs.readFileSync(p, 'utf8'); } catch (_) { return { ok: false, detail: 'não deu pra ler: ' + c.path }; }
|
|
34
|
+
const hit = c.regex ? new RegExp(c.text, c.flags || '').test(src) : src.includes(c.text);
|
|
35
|
+
const want = c.absent ? !hit : hit;
|
|
36
|
+
return { ok: want, detail: want ? 'ok' : (c.absent ? `contém "${c.text}" (não deveria)` : `NÃO contém "${c.text}"`) };
|
|
37
|
+
}
|
|
38
|
+
async function vHttp(c) {
|
|
39
|
+
const want = c.status != null ? Number(c.status) : 200;
|
|
40
|
+
const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), (c.timeout_s || 15) * 1000);
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(c.url, { method: c.method || 'GET', signal: ctrl.signal, headers: c.headers || {}, ...(c.body ? { body: typeof c.body === 'string' ? c.body : JSON.stringify(c.body) } : {}) });
|
|
43
|
+
let body = ''; try { body = await res.text(); } catch (_) {}
|
|
44
|
+
const statusOk = res.status === want;
|
|
45
|
+
const containsOk = c.contains ? body.includes(c.contains) : true;
|
|
46
|
+
const ok = statusOk && containsOk;
|
|
47
|
+
return { ok, detail: ok ? `HTTP ${res.status}` : (!statusOk ? `HTTP ${res.status} (esperava ${want})` : `HTTP ${res.status} mas sem "${c.contains}"`), evidence: body.slice(0, 160).replace(/\s+/g, ' ') };
|
|
48
|
+
} catch (e) { return { ok: false, detail: 'sem resposta: ' + (e.message || e), errorClass: 'network' }; }
|
|
49
|
+
finally { clearTimeout(t); }
|
|
50
|
+
}
|
|
51
|
+
function vPort(c) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const host = c.host || '127.0.0.1'; const port = Number(c.port);
|
|
54
|
+
const sock = new net.Socket(); let done = false;
|
|
55
|
+
const finish = (ok, detail) => { if (done) return; done = true; try { sock.destroy(); } catch (_) {} resolve({ ok, detail }); };
|
|
56
|
+
sock.setTimeout((c.timeout_s || 5) * 1000);
|
|
57
|
+
sock.once('connect', () => finish(true, `porta ${port} escutando`));
|
|
58
|
+
sock.once('timeout', () => finish(false, `porta ${port} sem resposta (timeout)`));
|
|
59
|
+
sock.once('error', () => finish(false, `porta ${port} fechada/recusada`));
|
|
60
|
+
sock.connect(port, host);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const VERIFIERS = {
|
|
65
|
+
command: (c, cwd) => vCommand(c, cwd),
|
|
66
|
+
file_exists: (c, cwd) => vFileExists(c, cwd),
|
|
67
|
+
file_absent: (c, cwd) => vFileAbsent(c, cwd),
|
|
68
|
+
file_contains: (c, cwd) => vFileContains(c, cwd),
|
|
69
|
+
http: (c) => vHttp(c),
|
|
70
|
+
port: (c) => vPort(c),
|
|
71
|
+
};
|
|
72
|
+
const VERIFIER_TYPES = Object.keys(VERIFIERS);
|
|
73
|
+
|
|
74
|
+
// Roda UM critério. Sempre resolve { ok, type, detail, ... } — nunca lança.
|
|
75
|
+
async function runCriterion(c, opts = {}) {
|
|
76
|
+
const cwd = opts.cwd || process.cwd();
|
|
77
|
+
const type = c && c.type;
|
|
78
|
+
const fn = VERIFIERS[type];
|
|
79
|
+
if (!fn) return { ok: false, type: type || '?', detail: 'tipo de critério desconhecido: ' + type, unknown: true };
|
|
80
|
+
try { const r = await fn(c, cwd); return Object.assign({ type, label: c.label || _labelFor(c) }, r); }
|
|
81
|
+
catch (e) { return { ok: false, type, label: c.label || _labelFor(c), detail: 'erro no verificador: ' + (e.message || e) }; }
|
|
82
|
+
}
|
|
83
|
+
function _labelFor(c) {
|
|
84
|
+
switch (c.type) {
|
|
85
|
+
case 'command': return '`' + String(c.cmd).slice(0, 50) + '` → exit ' + (c.exit != null ? c.exit : 0);
|
|
86
|
+
case 'file_exists': return 'existe ' + c.path;
|
|
87
|
+
case 'file_absent': return 'ausente ' + c.path;
|
|
88
|
+
case 'file_contains': return c.path + (c.absent ? ' SEM ' : ' contém ') + '"' + String(c.text).slice(0, 30) + '"';
|
|
89
|
+
case 'http': return (c.method || 'GET') + ' ' + c.url + ' → ' + (c.status || 200);
|
|
90
|
+
case 'port': return 'porta ' + c.port + ' escutando';
|
|
91
|
+
default: return c.type || '?';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Roda uma LISTA de critérios. Retorna resumo + resultados (o Evidence Ledger mínimo).
|
|
96
|
+
async function runAll(criteria, opts = {}) {
|
|
97
|
+
const list = Array.isArray(criteria) ? criteria : (criteria && Array.isArray(criteria.criteria) ? criteria.criteria : []);
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const c of list) results.push(await runCriterion(c, opts));
|
|
100
|
+
const passed = results.filter(r => r.ok).length;
|
|
101
|
+
return { allOk: results.length > 0 && passed === results.length, passed, total: results.length, results };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { runCriterion, runAll, VERIFIERS, VERIFIER_TYPES, _labelFor };
|