terminal-smart-cli 0.97.47 → 0.97.49

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 CHANGED
@@ -2535,6 +2535,7 @@ async function verificarCmd(args) {
2535
2535
 
2536
2536
  // 1) arquivo de critérios (JSON: array ou {criteria:[...]}) OU 2) critérios inline via flags
2537
2537
  let criteria = [];
2538
+ if (rawArgs.includes('--qualidade') || rawArgs.includes('--quality')) criteria.push({ type:'project_text_quality', label:en ? 'text files preserve UTF-8' : 'textos e HTML preservam UTF-8' });
2538
2539
  const file = (args || []).find(a => !a.startsWith('-') && /\.json$/i.test(a));
2539
2540
  if (file) {
2540
2541
  try { const j = JSON.parse(_fs.readFileSync(file, 'utf8')); criteria = Array.isArray(j) ? j : (j.criteria || []); }
@@ -2547,12 +2548,7 @@ async function verificarCmd(args) {
2547
2548
  const url = _val(['--url']); if (url) criteria.push({ type: 'http', url, status: Number(_val(['--status'])) || 200, ...(cont != null && !arq ? { contains: cont } : {}) });
2548
2549
  const porta = _val(['--porta', '--port']); if (porta) criteria.push({ type: 'port', port: Number(porta) });
2549
2550
  }
2550
- if (!criteria.length) {
2551
- console.log(ui.infoLine(en
2552
- ? 'usage: ts verificar criterios.json OR ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'
2553
- : 'uso: ts verificar criterios.json OU ts verificar --cmd "npm test" --url http://localhost:3000/health --status 200 --porta 3000 --existe dist/app.js'));
2554
- return;
2555
- }
2551
+ if (!criteria.length) criteria=verify.deriveFromStack(cwd);
2556
2552
 
2557
2553
  if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
2558
2554
  // remoteExec: liga o verificador ldd remoto à conexão SSH ativa (binário PW mora na VPS).
@@ -3069,6 +3065,17 @@ async function cloudCmd(args) {
3069
3065
  const sub = String(args[0] || 'status').toLowerCase();
3070
3066
  const rest = args.slice(1).filter(a => !a.startsWith('-'));
3071
3067
  const call = (path, opts = {}) => api('/api/cloud/' + path, { token, ...opts });
3068
+ const mutatesWebText = cmd => /(?:>|>>|\btee\b|\bsed\b|\bperl\b|\bcp\b|\bmv\b|\bbase64\b)[\s\S]{0,500}(?:\.html?|\.css|\.jsx?|\.tsx?|\.json|\.md|\/workspace)/i.test(String(cmd || ''))
3069
+ || /(?:\.html?|\.css|\.jsx?|\.tsx?|\.json|\.md|\/workspace)[\s\S]{0,500}(?:>|>>|\btee\b|\bsed\b|\bperl\b|\bcp\b|\bmv\b|\bbase64\b)/i.test(String(cmd || ''));
3070
+ const verifyCloudText = async cmd => {
3071
+ if (!mutatesWebText(cmd)) return true;
3072
+ const check=await call('exec',{method:'POST',body:{confirmed:true,cmd:'cd /workspace && ts verificar --qualidade --json'},timeoutMs:120000});
3073
+ if (check.ok) return true;
3074
+ const evidence=String(check.out || check.error || '').replace(/\s+/g,' ').slice(0,500);
3075
+ console.log(ui.errLine((en ? 'Cloud quality gate failed: ' : 'Gate de qualidade da Cloud Box falhou: ') + evidence));
3076
+ process.exitCode=1;
3077
+ return false;
3078
+ };
3072
3079
  const cloudSlug = require('../lib/cloud-slug');
3073
3080
  const chooseSlug = async (provided) => {
3074
3081
  let value = String(provided || '').trim();
@@ -3154,12 +3161,14 @@ async function cloudCmd(args) {
3154
3161
  if (process.stdout.isTTY) {
3155
3162
  try {
3156
3163
  await sse('/api/cloud/exec-stream', { token, body: { confirmed: true, cmd, timeoutMs: 300000 }, timeoutMs: 310000, onEvent: (e) => { if (e.chunk) process.stdout.write(e.chunk); } });
3164
+ await verifyCloudText(cmd);
3157
3165
  return;
3158
3166
  } catch (e) { if (e.code === 'auth') { console.log(ui.errLine(T.need_login)); return; } }
3159
3167
  }
3160
3168
  const r = await call('exec', { method: 'POST', body: { confirmed: true, cmd }, timeoutMs: 300000 });
3161
3169
  if (r.out) process.stdout.write(r.out);
3162
3170
  if (!r.ok && r.error) console.log(ui.errLine(r.error));
3171
+ if (r.ok) await verifyCloudText(cmd);
3163
3172
  return;
3164
3173
  }
3165
3174
 
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ const fs=require('fs');
3
+ const path=require('path');
4
+
5
+ const TEXT_EXT=new Set(['.html','.htm','.css','.js','.mjs','.cjs','.ts','.tsx','.jsx','.json','.md','.txt','.svg','.xml','.yml','.yaml']);
6
+ const SKIP=new Set(['node_modules','.git','.cache','.idea','.vscode','coverage']);
7
+ const MOJIBAKE=[
8
+ { regex:/\uFFFD/g, label:'caractere de substituição Unicode (�)' },
9
+ { regex:/Ã[\u0080-\u00BF]/g, label:'UTF-8 interpretado como Latin-1 (Ã…)' },
10
+ { regex:/Â[\u0080-\u00BF\u00A0]/g, label:'UTF-8 interpretado como Latin-1 (Â…)' },
11
+ { regex:/â(?:€|€™|€œ|€|€“|€”|€¦)/g, label:'pontuação UTF-8 corrompida (â…)' },
12
+ ];
13
+
14
+ function inspectText(value,{html=false}={}) {
15
+ const source=String(value || ''); const defects=[];
16
+ for(const item of MOJIBAKE){ const hits=source.match(item.regex); if(hits?.length) defects.push(`${item.label}: ${hits.length} ocorrência(s)`); }
17
+ if(html && /<!doctype html|<html[\s>]/i.test(source)
18
+ && !/<meta\s+[^>]*charset\s*=\s*["']?utf-8\b/i.test(source)
19
+ && !/<meta\s+[^>]*http-equiv\s*=\s*["']content-type["'][^>]*charset\s*=\s*["']?utf-8\b/i.test(source)) defects.push('HTML completo sem <meta charset="utf-8">');
20
+ return {ok:defects.length===0,defects};
21
+ }
22
+
23
+ function scanProject(root,{maxFiles=1500,maxBytes=2*1024*1024}={}) {
24
+ const defects=[]; let scanned=0;
25
+ const walk=dir=>{
26
+ if(scanned>=maxFiles) return;
27
+ let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true});}catch(_){return;}
28
+ for(const entry of entries){
29
+ if(scanned>=maxFiles) break;
30
+ if(entry.isDirectory()){ if(!SKIP.has(entry.name)) walk(path.join(dir,entry.name)); continue; }
31
+ const file=path.join(dir,entry.name); const ext=path.extname(entry.name).toLowerCase();
32
+ if(!TEXT_EXT.has(ext)) continue;
33
+ let stat; try{stat=fs.statSync(file);}catch(_){continue;}
34
+ if(stat.size>maxBytes) continue;
35
+ let source; try{source=fs.readFileSync(file,'utf8');}catch(_){continue;}
36
+ scanned++;
37
+ const result=inspectText(source,{html:ext==='.html'||ext==='.htm'});
38
+ for(const defect of result.defects) defects.push(`${path.relative(root,file)}: ${defect}`);
39
+ }
40
+ };
41
+ walk(path.resolve(root));
42
+ return {ok:defects.length===0,defects,scanned,truncated:scanned>=maxFiles};
43
+ }
44
+
45
+ module.exports={inspectText,scanProject};
package/lib/verify.js CHANGED
@@ -73,6 +73,10 @@ function vPort(c) {
73
73
  sock.connect(port, host);
74
74
  });
75
75
  }
76
+ function vProjectTextQuality(c,cwd) {
77
+ const result=require('./content-quality').scanProject(cwd,{maxFiles:c.max_files||1500});
78
+ return {ok:result.ok,detail:result.ok ? `${result.scanned} arquivo(s) de texto com UTF-8 íntegro` : `${result.defects.length} problema(s) de codificação`,evidence:result.defects.slice(0,8).join(' | ')};
79
+ }
76
80
 
77
81
  // Analisa a saída de `ldd`: quais libs NÃO resolvem. Separado pra ser testável sem ldd real.
78
82
  // "arquivo está no lugar" ≠ "roda": foi o buraco do deploy que só checou `file` (arquitetura).
@@ -111,6 +115,7 @@ const VERIFIERS = {
111
115
  http: (c) => vHttp(c),
112
116
  port: (c) => vPort(c),
113
117
  ldd: (c, cwd, opts) => vLdd(c, cwd, opts),
118
+ project_text_quality: (c,cwd) => vProjectTextQuality(c,cwd),
114
119
  };
115
120
  const VERIFIER_TYPES = Object.keys(VERIFIERS);
116
121
 
@@ -132,6 +137,7 @@ function _labelFor(c) {
132
137
  case 'http': return (c.method || 'GET') + ' ' + c.url + ' → ' + (c.status || 200);
133
138
  case 'port': return 'porta ' + c.port + ' escutando';
134
139
  case 'ldd': return 'libs de ' + c.path + ' resolvem' + (c.remote ? ' (VPS)' : '');
140
+ case 'project_text_quality': return 'textos e HTML sem caracteres corrompidos';
135
141
  default: return c.type || '?';
136
142
  }
137
143
  }
@@ -148,7 +154,7 @@ async function runAll(criteria, opts = {}) {
148
154
  // ── COMPILADOR de critérios (TaskSpec): o LLM/usuário PROPÕE, o harness VALIDA ────
149
155
  // Aceita só tipos conhecidos com os campos obrigatórios preenchidos; descarta o resto
150
156
  // (o modelo não impõe critério malformado). Determinístico. Retorna { criteria, dropped }.
151
- const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'], ldd: ['path'] };
157
+ const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'], ldd: ['path'], project_text_quality:[] };
152
158
  function compileCriteria(raw) {
153
159
  const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.criteria) ? raw.criteria : []);
154
160
  const criteria = [], dropped = [];
@@ -168,6 +174,7 @@ function compileCriteria(raw) {
168
174
  function deriveFromStack(cwd) {
169
175
  let st; try { st = require('./stack').detectStack(cwd); } catch (_) { return []; }
170
176
  const out = [];
177
+ out.push({type:'project_text_quality',label:'textos e HTML preservam UTF-8'});
171
178
  for (const t of (st.test || [])) { if (/test/i.test(t)) out.push({ type: 'command', cmd: t, exit: 0, label: 'testes passam (' + t + ')' }); }
172
179
  return out;
173
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.47",
3
+ "version": "0.97.49",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"