terminal-smart-cli 0.63.0 → 0.65.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 +56 -16
- package/lib/i18n.js +1 -0
- package/lib/stack.js +51 -0
- package/lib/verify.js +104 -0
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -682,33 +682,29 @@ async function hooksCmd(words) {
|
|
|
682
682
|
async function initCmd() {
|
|
683
683
|
const _fs = require('fs'), _p = require('path');
|
|
684
684
|
const cwd = process.cwd();
|
|
685
|
+
const auto = FLAGS.has('--auto') || FLAGS.has('--stack');
|
|
685
686
|
const dest = _p.join(cwd, '.ts-memoria.md');
|
|
686
|
-
const has = (f) => _fs.existsSync(_p.join(cwd, f));
|
|
687
|
-
const readJson = (f) => { try { return JSON.parse(_fs.readFileSync(_p.join(cwd, f), 'utf8')); } catch (_) { return null; } };
|
|
688
687
|
const en = cfg.lang === 'en';
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
tipo = 'Node.js' + (pkg.dependencies && (pkg.dependencies.react || pkg.dependencies.next) ? ' (React/Next)' : '');
|
|
693
|
-
const s = pkg.scripts || {};
|
|
694
|
-
if (s.build) build.push('npm run build'); if (s.dev || s.start) run.push('npm run ' + (s.dev ? 'dev' : 'start')); if (s.test) test.push('npm test');
|
|
695
|
-
} else if (has('pubspec.yaml')) { tipo = 'Flutter/Dart'; build = ['flutter build']; run = ['flutter run']; test = ['flutter test']; }
|
|
696
|
-
else if (has('requirements.txt') || has('pyproject.toml')) { tipo = 'Python'; run = ['python main.py']; test = ['pytest']; }
|
|
697
|
-
else if (has('go.mod')) { tipo = 'Go'; build = ['go build ./...']; run = ['go run .']; test = ['go test ./...']; }
|
|
698
|
-
else if (has('Cargo.toml')) { tipo = 'Rust'; build = ['cargo build']; run = ['cargo run']; test = ['cargo test']; }
|
|
699
|
-
else if (has('index.html')) { tipo = en ? 'Static web (HTML/JS)' : 'Web estático (HTML/JS)'; run = ['python -m http.server']; }
|
|
688
|
+
const st = require('../lib/stack').detectStack(cwd);
|
|
689
|
+
let { tipo, build, run, test } = st;
|
|
690
|
+
if (tipo === 'desconhecido' && en) tipo = 'unknown';
|
|
700
691
|
const top = _fs.readdirSync(cwd).filter(n => !n.startsWith('.') && n !== 'node_modules').slice(0, 25).join(', ');
|
|
701
692
|
const nl = (a) => a.length ? a.map(x => '`' + x + '`').join(' · ') : (en ? '(fill in)' : '(preencher)');
|
|
693
|
+
// Seção de convenções: com --auto vem PREENCHIDA pelo detector (determinístico); senão fica em branco.
|
|
694
|
+
const convBlock = (auto && st.conv.length)
|
|
695
|
+
? st.conv.map(c => '- ' + c).join('\n') + '\n- ' + (en ? '(add project-specific pitfalls / how to deploy)' : '(acrescente armadilhas específicas / como fazer deploy)')
|
|
696
|
+
: '- ' + (en ? '(add what the agent should always know: architecture, pitfalls, how to deploy)' : '(anote o que o agente sempre deve saber: arquitetura, pegadinhas, como fazer deploy)');
|
|
702
697
|
const body = (en
|
|
703
|
-
? `# Project memory (ts)\n\n> Auto-generated by \`ts init\`. Edit freely — the ts agent reads this every session.\n\n- **Type:** ${tipo}\n- **Build:** ${nl(build)}\n- **Run:** ${nl(run)}\n- **Test:** ${nl(test)}\n- **Top-level:** ${top}\n\n## Conventions / gotchas\n
|
|
704
|
-
: `# Memória do projeto (ts)\n\n> Gerado por \`ts init\`. Edite à vontade — o agente ts lê isto toda sessão.\n\n- **Tipo:** ${tipo}\n- **Build:** ${nl(build)}\n- **Rodar:** ${nl(run)}\n- **Testar:** ${nl(test)}\n- **Raiz:** ${top}\n\n## Convenções / armadilhas\n
|
|
698
|
+
? `# Project memory (ts)\n\n> Auto-generated by \`ts init${auto ? ' --auto' : ''}\`. Edit freely — the ts agent reads this every session.\n\n- **Type:** ${tipo}\n- **Build:** ${nl(build)}\n- **Run:** ${nl(run)}\n- **Test:** ${nl(test)}\n- **Top-level:** ${top}\n\n## Conventions / gotchas\n${convBlock}\n`
|
|
699
|
+
: `# Memória do projeto (ts)\n\n> Gerado por \`ts init${auto ? ' --auto' : ''}\`. Edite à vontade — o agente ts lê isto toda sessão.\n\n- **Tipo:** ${tipo}\n- **Build:** ${nl(build)}\n- **Rodar:** ${nl(run)}\n- **Testar:** ${nl(test)}\n- **Raiz:** ${top}\n\n## Convenções / armadilhas\n${convBlock}\n`);
|
|
705
700
|
if (_fs.existsSync(dest)) {
|
|
706
701
|
const ok = await ui.ask(C.warn('▲ ') + (en ? '.ts-memoria.md already exists. Overwrite? (backup kept) [s/N] ' : '.ts-memoria.md já existe. Sobrescrever? (com backup) [s/N] '));
|
|
707
702
|
if (!['s', 'sim', 'y', 'yes'].includes(String(ok).trim().toLowerCase())) { console.log(' ' + C.dim(en ? 'cancelled.' : 'cancelado.')); return; }
|
|
708
703
|
let r; try { r = await require('../lib/tools').execute('escrever_arquivo', { caminho: dest, conteudo: body }, { baseDir: cwd }); } catch (_) {}
|
|
709
704
|
if (!r || r.erro) { try { _fs.writeFileSync(dest, body); } catch (e) { console.error(ui.errLine((en ? 'Failed to write .ts-memoria.md: ' : 'Falha ao escrever .ts-memoria.md: ') + e.message)); return; } }
|
|
710
705
|
} else { _fs.writeFileSync(dest, body); }
|
|
711
|
-
|
|
706
|
+
const detN = auto ? st.conv.length : 0;
|
|
707
|
+
console.log(' ' + C.ok('✔ ') + (en ? 'Created ' : 'Criado ') + C.bold('.ts-memoria.md') + C.dim(' — ' + tipo + (detN ? (en ? ` · ${detN} convention(s) detected` : ` · ${detN} convenção(ões) detectada(s)`) : '')) + '\n ' + C.dim(auto ? (en ? 'Detected your stack automatically. Add project-specific notes and it loads every session.' : 'Detectei sua stack sozinho. Acrescente notas específicas — carrega toda sessão.') : (en ? 'Tip: `ts init --auto` detects your stack and fills the conventions.' : 'Dica: `ts init --auto` detecta a stack e preenche as convenções.')));
|
|
712
708
|
}
|
|
713
709
|
|
|
714
710
|
// ── git worktrees: isola uma run numa cópia descartável do repo ──
|
|
@@ -1675,6 +1671,49 @@ function buscarCmd(args) {
|
|
|
1675
1671
|
], { title: 'ts buscar' }));
|
|
1676
1672
|
}
|
|
1677
1673
|
|
|
1674
|
+
// ── ts verificar — PROVA determinística de que a tarefa terminou (verificadores) ──
|
|
1675
|
+
async function verificarCmd(args) {
|
|
1676
|
+
const en = (cfg.lang === 'en');
|
|
1677
|
+
const _fs = require('fs');
|
|
1678
|
+
const verify = require('../lib/verify');
|
|
1679
|
+
const _val = (names) => { const i = rawArgs.findIndex(a => names.includes(a)); return i >= 0 ? rawArgs[i + 1] : null; };
|
|
1680
|
+
const cwd = process.cwd();
|
|
1681
|
+
|
|
1682
|
+
// 1) arquivo de critérios (JSON: array ou {criteria:[...]}) OU 2) critérios inline via flags
|
|
1683
|
+
let criteria = [];
|
|
1684
|
+
const file = (args || []).find(a => !a.startsWith('-') && /\.json$/i.test(a));
|
|
1685
|
+
if (file) {
|
|
1686
|
+
try { const j = JSON.parse(_fs.readFileSync(file, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
|
|
1687
|
+
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); }
|
|
1688
|
+
} else {
|
|
1689
|
+
const cmd = _val(['--cmd', '--comando']); if (cmd) criteria.push({ type: 'command', cmd, exit: Number(_val(['--exit', '--codigo'])) || 0 });
|
|
1690
|
+
const ex = _val(['--existe', '--exists']); if (ex) criteria.push({ type: 'file_exists', path: ex });
|
|
1691
|
+
const arq = _val(['--arquivo', '--file']); const cont = _val(['--contem', '--contains']);
|
|
1692
|
+
if (arq && cont != null) criteria.push({ type: 'file_contains', path: arq, text: cont });
|
|
1693
|
+
const url = _val(['--url']); if (url) criteria.push({ type: 'http', url, status: Number(_val(['--status'])) || 200, ...(cont != null && !arq ? { contains: cont } : {}) });
|
|
1694
|
+
const porta = _val(['--porta', '--port']); if (porta) criteria.push({ type: 'port', port: Number(porta) });
|
|
1695
|
+
}
|
|
1696
|
+
if (!criteria.length) {
|
|
1697
|
+
console.log(ui.infoLine(en
|
|
1698
|
+
? 'usage: ts verificar criterios.json OR ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'
|
|
1699
|
+
: 'uso: ts verificar criterios.json OU ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'));
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
|
|
1704
|
+
const rep = await verify.runAll(criteria, { cwd });
|
|
1705
|
+
if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
|
|
1706
|
+
|
|
1707
|
+
for (const r of rep.results) {
|
|
1708
|
+
const mark = r.ok ? C.ok('✔') : C.err('✗');
|
|
1709
|
+
console.log(' ' + mark + ' ' + C.bold(r.label || r.type) + C.dim(' ' + (r.detail || '')) + (r.errorClass ? C.dim(' [' + r.errorClass + ']') : ''));
|
|
1710
|
+
if (!r.ok && r.evidence) console.log(' ' + C.dim('┄ ' + String(r.evidence).slice(0, 120)));
|
|
1711
|
+
}
|
|
1712
|
+
console.log('\n' + (rep.allOk ? ui.okLine : ui.errLine)(
|
|
1713
|
+
(rep.allOk ? (en ? 'VERIFIED' : 'VERIFICADO') : (en ? 'NOT verified' : 'NÃO verificado')) + C.dim(` ${rep.passed}/${rep.total} ` + (en ? 'passed' : 'ok'))));
|
|
1714
|
+
process.exit(rep.allOk ? 0 : 1);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1678
1717
|
// ── ts perfil — personas com COFRE DE MEMÓRIA isolado (Mempalace/Hermes) ──────
|
|
1679
1718
|
function perfilCmd(args) {
|
|
1680
1719
|
const en = (cfg.lang === 'en');
|
|
@@ -1937,6 +1976,7 @@ function recallCmd(args) {
|
|
|
1937
1976
|
case 'indexar': case 'index': return indexarCmd();
|
|
1938
1977
|
case 'buscar': case 'search': case 'procurar': return buscarCmd(POS.slice(1));
|
|
1939
1978
|
case 'perfil': case 'persona': case 'profile': return perfilCmd(POS.slice(1));
|
|
1979
|
+
case 'verificar': case 'verify': case 'provar': return verificarCmd(POS.slice(1));
|
|
1940
1980
|
case 'meta': case 'missao': case 'mission': return metaCmd();
|
|
1941
1981
|
case 'runs': return runsCmd();
|
|
1942
1982
|
case 'status': return statusCmd(POS[1]);
|
package/lib/i18n.js
CHANGED
|
@@ -45,6 +45,7 @@ const STR = {
|
|
|
45
45
|
['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
|
|
46
46
|
['ts meta --status', 'estado da missão deste diretório'],
|
|
47
47
|
['ts eval suite.json', 'avalia o agente numa suíte de casos (juiz de IA + nota)'],
|
|
48
|
+
['ts verificar --cmd "npm test" --url ... --porta N', 'PROVA determinística de conclusão (build/http/porta/arquivo); exit 1 pra CI'],
|
|
48
49
|
['ts acp', 'servidor Agent Client Protocol (conecta o ts a editores tipo Zed)'],
|
|
49
50
|
['ts mcp', 'servidores MCP: as ferramentas deles entram no agente'],
|
|
50
51
|
] },
|
package/lib/stack.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Detecta a STACK de um projeto de forma DETERMINÍSTICA (zero IA): tipo, comandos
|
|
2
|
+
// (build/run/test) e CONVENÇÕES (o que o agente sempre deve saber). Lê config files +
|
|
3
|
+
// deps do package.json. Puro/testável. Usado por `ts init --auto` (e reutilizável pelo meta).
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
function detectStack(cwd) {
|
|
9
|
+
const dir = cwd || process.cwd();
|
|
10
|
+
const has = (f) => { try { return fs.existsSync(path.join(dir, f)); } catch (_) { return false; } };
|
|
11
|
+
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { return null; } };
|
|
12
|
+
let tipo = 'desconhecido', build = [], run = [], test = [];
|
|
13
|
+
const conv = [];
|
|
14
|
+
const pkg = has('package.json') && readJson('package.json');
|
|
15
|
+
if (pkg) {
|
|
16
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
17
|
+
const dep = (n) => Object.prototype.hasOwnProperty.call(deps, n);
|
|
18
|
+
tipo = dep('next') ? 'Next.js' : dep('nuxt') ? 'Nuxt' : dep('@remix-run/react') ? 'Remix' : dep('react') ? 'React' : dep('vue') ? 'Vue' : dep('svelte') ? 'Svelte' : dep('@angular/core') ? 'Angular' : (dep('express') || dep('fastify') || dep('koa')) ? 'Node backend' : 'Node.js';
|
|
19
|
+
const s = pkg.scripts || {};
|
|
20
|
+
if (s.build) build.push('npm run build'); if (s.dev || s.start) run.push('npm run ' + (s.dev ? 'dev' : 'start')); if (s.test) test.push('npm test'); if (s.lint) test.push('npm run lint');
|
|
21
|
+
if (has('tsconfig.json') || dep('typescript')) conv.push('**TypeScript** — mantenha os tipos corretos; rode o type-check (tsc/`npm run build`) antes de concluir. Não use `any` à toa.');
|
|
22
|
+
if (dep('next')) conv.push('**Next.js** — respeite o roteamento do framework (App Router em `app/` ou Pages em `pages/`); não reinvente SSR/rotas/data-fetching.');
|
|
23
|
+
if (dep('vite') || has('vite.config.js') || has('vite.config.ts')) conv.push('**Vite** — dev/build pelo Vite; imports com alias respeitam `vite.config`.');
|
|
24
|
+
if (dep('tailwindcss') || has('tailwind.config.js') || has('tailwind.config.ts')) conv.push('**Tailwind CSS** — estilize com classes utilitárias; evite CSS solto/inline fora do padrão do projeto.');
|
|
25
|
+
if (dep('@prisma/client') || dep('prisma') || has('prisma/schema.prisma')) conv.push('**Prisma** — altere `schema.prisma` + `prisma migrate`; NÃO edite SQL/tabela na mão.');
|
|
26
|
+
if (dep('drizzle-orm')) conv.push('**Drizzle ORM** — schema em código + migrations; não edite o banco direto.');
|
|
27
|
+
if (dep('jest') || dep('vitest') || dep('mocha')) conv.push('**Testes** (' + (dep('vitest') ? 'vitest' : dep('jest') ? 'jest' : 'mocha') + ') — rode e mantenha verdes; toda mudança de lógica precisa de teste.');
|
|
28
|
+
if (has('.eslintrc') || has('.eslintrc.js') || has('.eslintrc.json') || has('eslint.config.js') || dep('eslint')) conv.push('**ESLint** — rode o lint antes de concluir; siga as regras do projeto.');
|
|
29
|
+
if (has('.prettierrc') || has('.prettierrc.json') || dep('prettier')) conv.push('**Prettier** — mantenha a formatação (não reformate arquivos inteiros à toa).');
|
|
30
|
+
if (pkg.workspaces || has('pnpm-workspace.yaml') || has('turbo.json') || has('lerna.json')) conv.push('**Monorepo** — trabalhe no pacote/workspace certo; não misture dependências entre pacotes.');
|
|
31
|
+
} else if (has('pubspec.yaml')) { tipo = 'Flutter/Dart'; build = ['flutter build']; run = ['flutter run']; test = ['flutter test']; conv.push('**Flutter** — widgets + state management do projeto; `flutter analyze` limpo antes de concluir.'); }
|
|
32
|
+
else if (has('requirements.txt') || has('pyproject.toml')) {
|
|
33
|
+
tipo = 'Python'; run = ['python main.py']; test = ['pytest'];
|
|
34
|
+
const req = has('requirements.txt') ? (() => { try { return fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf8').toLowerCase(); } catch (_) { return ''; } })() : '';
|
|
35
|
+
if (/django/.test(req)) { tipo = 'Python/Django'; run = ['python manage.py runserver']; conv.push('**Django** — use models/migrations (`makemigrations`+`migrate`); respeite apps e settings.'); }
|
|
36
|
+
else if (/fastapi/.test(req)) { tipo = 'Python/FastAPI'; run = ['uvicorn main:app --reload']; conv.push('**FastAPI** — rotas com Pydantic; mantenha os schemas tipados.'); }
|
|
37
|
+
else if (/flask/.test(req)) { tipo = 'Python/Flask'; run = ['flask run']; }
|
|
38
|
+
conv.push('**Python** — use o venv do projeto; não instale global. `pytest` verde antes de concluir.');
|
|
39
|
+
}
|
|
40
|
+
else if (has('go.mod')) { tipo = 'Go'; build = ['go build ./...']; run = ['go run .']; test = ['go test ./...']; conv.push('**Go** — `go build`/`go vet`/`go test ./...` limpos; erros tratados explicitamente.'); }
|
|
41
|
+
else if (has('Cargo.toml')) { tipo = 'Rust'; build = ['cargo build']; run = ['cargo run']; test = ['cargo test']; conv.push('**Rust** — `cargo clippy` e `cargo test` limpos antes de concluir.'); }
|
|
42
|
+
else if (has('composer.json')) { tipo = 'PHP'; run = ['php -S localhost:8000']; conv.push('**PHP/Composer** — `composer install`; siga o autoload PSR do projeto.'); }
|
|
43
|
+
else if (has('index.html')) { tipo = 'Web estático (HTML/JS)'; run = ['python -m http.server']; }
|
|
44
|
+
// sinais transversais (independem da linguagem)
|
|
45
|
+
if (has('Dockerfile') || has('docker-compose.yml') || has('docker-compose.yaml') || has('compose.yaml')) conv.push('**Docker** — mudanças de ambiente/infra passam pelo Docker; rebuild da imagem quando alterar deps.');
|
|
46
|
+
if (has('.env.example') || has('.env.sample')) conv.push('**Config via .env** — NUNCA commite segredos; use `.env.example` como referência das variáveis.');
|
|
47
|
+
if (has('.git')) conv.push('**Git** — commits pequenos e descritivos; não faça `push --force` na branch principal.');
|
|
48
|
+
return { tipo, build, run, test, conv };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { detectStack };
|
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 };
|