@mocoto/mahoraga 0.14.6 → 0.14.8

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.
Files changed (33) hide show
  1. package/README.md +165 -511
  2. package/dist/analysts/css/analysts/analyst-css.js +10 -1
  3. package/dist/analysts/detectors/detector-bugs-ml.js +81 -19
  4. package/dist/analysts/js-ts/registrar.js +1 -1
  5. package/dist/analysts/react/analysts/analyst-react-hooks.js +109 -90
  6. package/dist/analysts/react/detectors/detector-react-best-practices.js +100 -6
  7. package/dist/cli/commands/command-perf.js +1 -1
  8. package/dist/cli/commands/command-update.js +3 -2
  9. package/dist/core/config/auto/auto-fix-config.js +9 -4
  10. package/dist/core/config/config.js +1 -1
  11. package/dist/core/messages/en/core/formatters-messages.js +1 -0
  12. package/dist/core/messages/ja/core/formatters-messages.js +1 -0
  13. package/dist/core/messages/pt/core/formatters-messages.js +1 -0
  14. package/dist/core/messages/shared/icons.js +3 -3
  15. package/dist/core/messages/zh/core/formatters-messages.js +1 -0
  16. package/dist/core/parsing/langs/json.js +2 -2
  17. package/dist/core/parsing/langs/typescript.js +33 -7
  18. package/dist/core/schema/version.js +1 -1
  19. package/dist/core/utils/exec-safe.js +47 -25
  20. package/dist/lib/api.js +14 -0
  21. package/dist/shared/data-processing/occurrences.js +3 -0
  22. package/dist/shared/formatters/code-min-formatter.js +1 -1
  23. package/dist/shared/formatters/formatters/commons.js +19 -5
  24. package/dist/shared/formatters/formatters/shell.js +128 -9
  25. package/dist/shared/helpers/ast-visitor-utils.js +3 -0
  26. package/dist/shared/helpers/imports.js +1 -1
  27. package/dist/shared/helpers/reader-report.js +13 -0
  28. package/dist/shared/helpers/structure.js +2 -2
  29. package/dist/types/guardian/result.js +38 -0
  30. package/dist/types/reports/processing.js +12 -0
  31. package/dist/types/shared/validation.js +111 -0
  32. package/dist/vulnerabilities/scanner.js +7 -2
  33. package/package.json +11 -18
@@ -163,11 +163,11 @@ export const ICONES_ZELADOR = {
163
163
  export function getIcone(categoria, nome) {
164
164
  switch (categoria) {
165
165
  case 'nivel':
166
- return ICONES_NIVEL[nome];
166
+ return ICONES_NIVEL[nome] ?? ICONES_NIVEL.info;
167
167
  case 'status':
168
- return ICONES_STATUS[nome];
168
+ return ICONES_STATUS[nome] ?? ICONES_STATUS.pendente;
169
169
  case 'acao':
170
- return ICONES_ACAO[nome];
170
+ return ICONES_ACAO[nome] ?? '[*]';
171
171
  default:
172
172
  return '[*]';
173
173
  }
@@ -13,6 +13,7 @@ export const FormattersMensagens = {
13
13
  };
14
14
  export const ExecSafeMensagens = {
15
15
  safeModeBloqueado: 'SAFE_MODE下命令执行已禁用。设置 SUKUNA_ALLOW_EXEC=1 以允许。',
16
+ binarioNaoPermitido: (bin) => `二进制文件 '${bin}' 不在允许列表中。`,
16
17
  };
17
18
  export const WorkerExecutorMensagens = {
18
19
  timeoutTecnica: (nome, ms) => `超时: 技术 '${nome}' 已超过 ${ms}ms`,
@@ -6,9 +6,9 @@ export function parseComJson(codigo) {
6
6
  try {
7
7
  const parsed = JSON.parse(codigo);
8
8
  const info = {
9
- type: Array.isArray(parsed) ? 'array' : typeof parsed,
9
+ type: parsed === null ? 'null' : (Array.isArray(parsed) ? 'array' : typeof parsed),
10
10
  };
11
- if (typeof parsed === 'object' && !Array.isArray(parsed)) {
11
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
12
12
  const keys = Object.keys(parsed);
13
13
  info.keys = keys;
14
14
  info.depth = estimateDepth(parsed, 0);
@@ -1,15 +1,41 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import { createRequire } from 'module';
2
+ import { parse as babelParse } from '@babel/parser';
3
3
  import { getCurrentParsingFile, logCore, wrapMinimal } from '../utils.js';
4
- const localRequire = createRequire(import.meta.url);
5
- /** Analisa código TypeScript usando parser nativo TSC */
4
+ /**
5
+ * Analisa código TypeScript/TSX.
6
+ * Nota de migração (TS 7 / compilador nativo em Go): o compilador TypeScript
7
+ * não expõe mais a API programática (ts.createSourceFile etc.). Usamos o
8
+ * @babel/parser (já dependência do projeto) para obter o AST de forma estável
9
+ * e compatível com o TS 7.
10
+ */
6
11
  export function parseComTypeScript(codigo, tsx = false) {
7
12
  try {
8
- const ts = localRequire('typescript');
9
- const sf = ts.createSourceFile(tsx ? 'file.tsx' : 'file.ts', codigo, ts.ScriptTarget.Latest, false, tsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
13
+ // Lista de plugins alinhada aos demais parsers do projeto (babel 8).
14
+ // Tipada como string[] para acomodar plugins que deixaram de figurar no
15
+ // enum ParserPlugin do babel 8 (passaram a ser comportamento padrão).
16
+ const plugins = [
17
+ 'decorators-legacy', 'classProperties', 'classPrivateProperties',
18
+ 'classPrivateMethods', 'optionalChaining', 'nullishCoalescingOperator',
19
+ 'topLevelAwait', 'importAttributes'
20
+ ];
21
+ if (tsx) {
22
+ plugins.unshift('jsx');
23
+ }
24
+ plugins.unshift('typescript');
25
+ const ast = babelParse(codigo, {
26
+ sourceType: 'unambiguous',
27
+ errorRecovery: true,
28
+ plugins: plugins
29
+ });
30
+ if (ast.errors && ast.errors.length > 0) {
31
+ for (const err of ast.errors) {
32
+ logCore.erroTs(err.message ?? String(err), getCurrentParsingFile());
33
+ }
34
+ return null;
35
+ }
10
36
  return wrapMinimal(tsx ? 'tsx-tsc' : 'ts-tsc', {
11
- kind: sf.kind,
12
- statements: sf.statements.length
37
+ kind: 'SourceFile',
38
+ statements: ast.program.body.length
13
39
  });
14
40
  }
15
41
  catch (erro) {
@@ -40,7 +40,7 @@ export function criarSchemaMetadata(versao = VERSAO_ATUAL, descricaoPersonalizad
40
40
  export function validarSchema(relatorio) {
41
41
  const erros = [];
42
42
  // Verificar estrutura básica
43
- if (typeof relatorio !== 'object') {
43
+ if (typeof relatorio !== 'object' || relatorio === null) {
44
44
  erros.push('Relatório deve ser um objeto');
45
45
  return {
46
46
  valido: false,
@@ -1,19 +1,32 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import { execSync } from 'node:child_process';
2
+ import { execFileSync } from 'node:child_process';
3
+ import path from 'node:path';
3
4
  import { config } from '../config/index.js';
4
5
  import { getMessages } from '../messages/index.js';
5
- // Expressão regular para detectar shell metacharacters perigosos
6
- // que permitiriam injeção de comandos adicionais.
7
- // Bloqueia: ; | & ` $() ${} <> \n
8
- const SHELL_INJECTION_RE = /[;|&`]|\$[({]|\${|>|<|\\n/;
9
6
  /**
10
- * Valida que o comando não contém caracteres de injeção shell.
11
- * @param cmd - Comando a validar
12
- * @throws Error se o comando contiver padrões perigosos
7
+ * Allowlist de binários que o CLI pode executar externamente.
8
+ * Apenas comandos essenciais para operação do ferramenta são permitidos.
9
+ * Isso substitui a abordagem anterior de blacklist de caracteres shell,
10
+ * eliminando o vetor de injeção por shell intermediário.
13
11
  */
14
- function validarComando(cmd) {
15
- if (SHELL_INJECTION_RE.test(cmd)) {
16
- throw new Error(`Comando rejeitado por segurança: contém caracteres de injeção shell.`);
12
+ const BINARIOS_PERMITIDOS = new Set([
13
+ 'git',
14
+ 'npm',
15
+ 'npx',
16
+ 'node',
17
+ 'pnpm',
18
+ 'yarn',
19
+ 'corepack',
20
+ ]);
21
+ /**
22
+ * Valida que o binário está na allowlist.
23
+ * @param bin - Caminho ou nome do binário
24
+ * @throws Error se o binário não for permitido
25
+ */
26
+ function validarBinario(bin) {
27
+ const nome = path.basename(bin);
28
+ if (!BINARIOS_PERMITIDOS.has(nome)) {
29
+ throw new Error(getMessages().ExecSafeMensagens.binarioNaoPermitido(bin));
17
30
  }
18
31
  }
19
32
  function verificarSafeMode() {
@@ -22,28 +35,37 @@ function verificarSafeMode() {
22
35
  }
23
36
  }
24
37
  /**
25
- * Executa um comando shell de forma segura.
38
+ * Executa um binário de forma segura sem shell intermediário.
39
+ *
26
40
  * Bloqueia a execução quando SAFE_MODE está ativo e ALLOW_EXEC não está habilitado.
27
- * Também valida o comando contra injeção shell mesmo quando a execução é permitida.
28
- * @param cmd - Comando shell a executar
29
- * @param opts - Opções do execSync do Node.js
41
+ * Valida o binário contra uma allowlist de comandos permitidos.
42
+ * Força `shell: false` para eliminar o risco de injeção de comandos.
43
+ *
44
+ * @param bin - Binário a executar (ex: 'git', 'npm')
45
+ * @param args - Argumentos do binário (array de strings)
46
+ * @param opts - Opções adicionais do child_process
30
47
  * @returns Buffer ou string com a saída do comando
31
- * @throws Error se SAFE_MODE estiver ativo sem ALLOW_EXEC ou se o comando for inválido
48
+ * @throws Error se SAFE_MODE estiver ativo sem ALLOW_EXEC,
49
+ * se o binário não estiver na allowlist,
50
+ * ou se a execução falhar
32
51
  */
33
- export function executarShellSeguro(cmd, opts = {}) {
34
- validarComando(cmd);
52
+ export function executarShellSeguro(bin, args = [], opts = {}) {
53
+ validarBinario(bin);
35
54
  verificarSafeMode();
36
- return execSync(cmd, opts);
55
+ return execFileSync(bin, args, { ...opts, shell: false });
37
56
  }
38
57
  /**
39
58
  * Versão assíncrona do `executarShellSeguro`.
40
59
  * Wrapper async que delega ao sync, útil em contextos que já utilizam await.
41
- * @param cmd - Comando shell a executar
42
- * @param opts - Opções do execSync do Node.js
60
+ *
61
+ * @param bin - Binário a executar (ex: 'git', 'npm')
62
+ * @param args - Argumentos do binário (array de strings)
63
+ * @param opts - Opções adicionais do child_process
43
64
  * @returns Promise resolvida com Buffer ou string da saída do comando
44
- * @throws Error se SAFE_MODE estiver ativo sem ALLOW_EXEC
65
+ * @throws Error se SAFE_MODE estiver ativo sem ALLOW_EXEC,
66
+ * se o binário não estiver na allowlist,
67
+ * ou se a execução falhar
45
68
  */
46
- export function executarShellSeguroAsync(cmd, opts = {}) {
47
- // wrapper assíncrono que delega ao sync (usado onde já há await)
48
- return executarShellSeguro(cmd, opts);
69
+ export function executarShellSeguroAsync(bin, args = [], opts = {}) {
70
+ return executarShellSeguro(bin, args, opts);
49
71
  }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ async function request(path) {
3
+ const response = await fetch(path);
4
+ if (!response.ok) {
5
+ const body = await response.json();
6
+ throw new Error(body.erro ?? `HTTP ${response.status}`);
7
+ }
8
+ return response.json();
9
+ }
10
+ export const api = {
11
+ getAppInfo() {
12
+ return request('/api/github/app-info');
13
+ },
14
+ };
@@ -9,6 +9,9 @@
9
9
  * A chave de deduplicação é composta por: relPath|linha|tipo|mensagem.
10
10
  */
11
11
  export function dedupeOcorrencias(arr) {
12
+ if (!arr || !Array.isArray(arr)) {
13
+ return [];
14
+ }
12
15
  const seen = new Map();
13
16
  for (const ocorrencia of arr) {
14
17
  const key = `${ocorrencia.relPath ?? ''}|${String(ocorrencia.linha ?? '')}|${ocorrencia.tipo ?? ''}|${ocorrencia.mensagem ?? ''}`;
@@ -10,7 +10,7 @@ export function formatarCodeMinimo(code, opts) {
10
10
  const formatted = normalizarNewlinesFinais(removerEspacosFinaisPorLinha(normalized));
11
11
  return {
12
12
  ok: true,
13
- parser: opts?.parser,
13
+ parser: (opts?.parser ?? 'code'),
14
14
  formatted,
15
15
  changed: formatted !== normalizarNewlinesFinais(normalized),
16
16
  };
@@ -93,13 +93,19 @@ const REPEATABLE_TO_SINGLE = new Set([',', '.', '!', '?', ';']);
93
93
  const REPEATABLE_KEEP_UP_TO_3 = new Set(['.', '!', '?']);
94
94
  const MULTI_PUNCT_RE = /([,\.!?:;_\-])\1+/g;
95
95
  const SPACE_BEFORE_PUNCT_RE = /\s+([,.:;!?])/g;
96
- const NO_SPACE_AFTER_NON_DOT_PUNCT_RE = /([,:;!?])([^\s\)\]\}\[!,;:?])/g;
96
+ const NO_SPACE_AFTER_NON_DOT_PUNCT_RE = /([,:;!?])([^\s\)\]\}\[!,;:?"'])/g;
97
97
  const SPACE_BEFORE_ELLIPSIS_RE = /([^0-9])(\.{3})/g;
98
98
  const PROTECTED_PATTERNS = [/https?:\/\/[^\s"'<>]+/gi, /ftp:\/\/[^\s"'<>]+/gi, /file:\/\/[^\s"'<>]+/gi, /www\.[^\s"'<>]+/gi, /[a-zA-Z0-9](?:\.[a-zA-Z0-9-]+)+\.[a-zA-Z]{2,}/g, /(?:^|(?<=\s|["'=(\[,]))\.\//g, /(?:^|(?<=\s|["'=(\[,]))\.\.\//g, /(?<=[/\\])[\w-]+\.[\w]+/g, /\d+\.\d+(?:\.\d+)*/g, /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, /[a-f0-9]{7,}\.[a-f0-9]+/gi, /[\w-]+\.[\w-]+\.[\w]+/g,
99
99
  // Proteger paths e nomes de arquivo com extensão
100
100
  /(?:^|[\s"'=(\[,])([\/\\][\w-]+(?:[\/\\][\w-]+)*[\/\\]?[\w-]+\.[a-zA-Z0-9]+)/g, /(?:^|[\s"'=(\[,])([\w-]+(?:[\/\\][\w-]+)+\.[a-zA-Z0-9]+)/g,
101
101
  // Proteger imports/require paths
102
- /(?:from\s+|require\(|import\s+)(["'])([^"']+\.[a-zA-Z0-9]+)\1/g];
102
+ /(?:from\s+|require\(|import\s+)(["'])([^"']+\.[a-zA-Z0-9]+)\1/g,
103
+ // Proteger expansões de parâmetro shell ${...} - operadores :- := :? :+ e slicing não devem ser tocados
104
+ /\$\{[^}]*\}/g,
105
+ // Proteger terminadores de case shell: ;; ;;& ;&
106
+ /;;[;&]?|;&/g,
107
+ // Proteger expressões de teste shell [[ ... ]] e (( ... ))
108
+ /\[\[[^\]]*\]\]|\(\([^)]*\)\)/g];
103
109
  function protectSequences(code) {
104
110
  const placeholders = [];
105
111
  let result = code;
@@ -299,14 +305,22 @@ function fixSpacingAroundPunct(code) {
299
305
  if (shouldSkipLineForPunctuation(line, idx, lines)) {
300
306
  return line;
301
307
  }
302
- const t1 = line.replace(SPACE_BEFORE_PUNCT_RE, '$1');
303
- const t2 = t1.replace(NO_SPACE_AFTER_NON_DOT_PUNCT_RE, (m, p1, _p2, off) => fixNonDotPunct(m, p1, _p2, off, t1));
308
+ // Proteger espaço antes de ! quando é operador de negação shell:
309
+ // [[ ! preservar o espaço, não remover
310
+ // Also protect (( ! and case ! patterns
311
+ const shellNegationProtected = line
312
+ .replace(/(\[\[)\s+(!)/g, '$1\x00SHL_NEG\x00')
313
+ .replace(/(\(\()\s+(!)/g, '$1\x00SHL_NEG\x00');
314
+ const t1 = shellNegationProtected.replace(SPACE_BEFORE_PUNCT_RE, '$1');
315
+ // Restaurar proteção de negação shell
316
+ const t1restored = t1.replace(/\x00SHL_NEG\x00/g, ' !');
317
+ const t2 = t1restored.replace(NO_SPACE_AFTER_NON_DOT_PUNCT_RE, (m, p1, _p2, off) => fixNonDotPunct(m, p1, _p2, off, t1restored));
304
318
  const t3 = t2.replace(NO_SPACE_AFTER_DOT_RE, (m, off) => fixDotPunct(m, off, t2));
305
319
  const t4 = t3.replace(SPACE_BEFORE_ELLIPSIS_RE, '$1 $2');
306
320
  if (t4 !== line) {
307
321
  changed = true;
308
322
  const countBefore = (line.match(SPACE_BEFORE_PUNCT_RE) ?? []).length;
309
- const countNonDot = (t1.match(NO_SPACE_AFTER_NON_DOT_PUNCT_RE) ?? []).length;
323
+ const countNonDot = (t1restored.match(NO_SPACE_AFTER_NON_DOT_PUNCT_RE) ?? []).length;
310
324
  const countDot = (t2.match(NO_SPACE_AFTER_DOT_RE) ?? []).length;
311
325
  const countEllipsis = (t3.match(SPACE_BEFORE_ELLIPSIS_RE) ?? []).length;
312
326
  totalChanges += countBefore + countNonDot + countDot + countEllipsis;
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // @mahoraga-disable ml-deep-nesting
3
+ import { execSync } from 'node:child_process';
3
4
  import { corrigirPontuacaoECodigo, limitarLinhasEmBranco, normalizarFimDeLinha, normalizarNewlinesFinais, removerBom } from './commons.js';
4
5
  function parseShellLine(raw, ctx) {
5
6
  const trimmed = raw.trimEnd();
@@ -129,6 +130,82 @@ function parseShellLine(raw, ctx) {
129
130
  }
130
131
  };
131
132
  }
133
+ /**
134
+ * Verifica se um índice dentro de uma linha está entre aspas (single ou double).
135
+ */
136
+ function isIndexInQuotes(line, index) {
137
+ let inSingle = false;
138
+ let inDouble = false;
139
+ let escaped = false;
140
+ for (let i = 0; i < index && i < line.length; i++) {
141
+ const ch = line[i];
142
+ if (escaped) {
143
+ escaped = false;
144
+ continue;
145
+ }
146
+ if (ch === '\\') {
147
+ escaped = true;
148
+ continue;
149
+ }
150
+ if (inSingle) {
151
+ if (ch === "'")
152
+ inSingle = false;
153
+ continue;
154
+ }
155
+ if (inDouble) {
156
+ if (ch === '"')
157
+ inDouble = false;
158
+ continue;
159
+ }
160
+ if (ch === "'") {
161
+ inSingle = true;
162
+ continue;
163
+ }
164
+ if (ch === '"') {
165
+ inDouble = true;
166
+ continue;
167
+ }
168
+ }
169
+ return inSingle || inDouble;
170
+ }
171
+ /**
172
+ * Verifica se um caractere pipe (|) está em posição de pipe shell real
173
+ * (fora de aspas, não dentro de string/regex literal).
174
+ */
175
+ function isRealShellPipe(line, index) {
176
+ if (isIndexInQuotes(line, index))
177
+ return false;
178
+ // Verificar se o | está precedido por $ ou ` (subshell) - isso é pipe real
179
+ const before = line.slice(0, index);
180
+ // Se | está dentro de expressão $() ou `` , é pipe dentro de subshell - ainda é pipe real
181
+ // Mas se está dentro de '...' ou "..." já foi filtrado acima
182
+ return true;
183
+ }
184
+ /**
185
+ * Aplica espaçamento em redirecionamento simples: >file → > file
186
+ * Mas preserva redirecionamentos atômicos (2>&1, &>, etc.)
187
+ */
188
+ function aplicarEspacamentoRedirecionamento(line) {
189
+ // Primeiro, proteger redirecionamentos atômicos com placeholders
190
+ const atomicos = [];
191
+ let result = line.replace(
192
+ // Captura operadores de redirecionamento atômicos (compostos por 2+ caracteres):
193
+ // &>, &>>, 2>, 2>>, 2>&1, 3>&-, |&, <<, <<-, <>, >|, >>, >&, <&
194
+ // NÃO captura > ou < simples (esses devem ganhar espaço)
195
+ /(?:\d+>&?\d*|&>>?|<<[-]?|\|&|<>|>[\|]|>>|>&|<&)/g, (match) => {
196
+ const id = atomicos.length;
197
+ atomicos.push(match);
198
+ return `\x00ATOMIC_REDIR_${id}\x00`;
199
+ });
200
+ // Agora aplicar espaçamento em redirecionamentos simples restantes
201
+ // cmd>file → cmd > file (espaço antes e depois)
202
+ result = result.replace(/(?<!\x00)([^\s><])([><])([^\s><])/g, '$1 $2 $3');
203
+ // Restaurar placeholders
204
+ result = result.replace(/\x00ATOMIC_REDIR_(\d+)\x00/g, (_m, id) => {
205
+ return atomicos[Number(id)] ?? _m;
206
+ });
207
+ return result;
208
+ }
132
209
  function corrigirEspacamentoShell(lines) {
133
210
  const out = [];
134
211
  let ctx = {
@@ -152,6 +229,11 @@ function corrigirEspacamentoShell(lines) {
152
229
  out.push(output);
153
230
  continue;
154
231
  }
232
+ // Shebang (#!): preservar intacto, sem espaços
233
+ if (/^\s*#!/.test(output)) {
234
+ out.push(output);
235
+ continue;
236
+ }
155
237
  // Comentários: garantir espaço após #
156
238
  if (/^\s*#\S/.test(output)) {
157
239
  out.push(output.replace(/^(\s*)#(\S)/, '$1# $2'));
@@ -163,23 +245,44 @@ function corrigirEspacamentoShell(lines) {
163
245
  out.push(output.replace(/^(\s*[a-zA-Z_][\w]*)\s+=\s/, '$1='));
164
246
  continue;
165
247
  }
166
- // Espaçamento em pipes: | → | (espaços em torno)
248
+ // Espaçamento em pipes reais do shell (fora de aspas/strings): cmd|cmdcmd | cmd
167
249
  if (/\S\|\S/.test(output)) {
168
- out.push(output.replace(/(\S)\|(\S)/g, '$1 | $2'));
169
- continue;
170
- }
171
- // Espaçamento em redirecionamentos: >file → > file
172
- if (/\S[><]\S/.test(output)) {
173
- out.push(output.replace(/(\S)([><])(\S)/g, '$1 $2 $3'));
174
- continue;
250
+ // Só aplicar espaços em pipes que são reais do shell (fora de aspas)
251
+ const withPipeSpaces = output.replace(/(\S)\|(\S)/g, (match, before, after, offset) => {
252
+ if (isRealShellPipe(output, offset + before.length)) {
253
+ return `${before} | ${after}`;
254
+ }
255
+ return match;
256
+ });
257
+ // Se houve mudança, continue; senão cai nos próximos handlers
258
+ if (withPipeSpaces !== output) {
259
+ out.push(aplicarEspacamentoRedirecionamento(withPipeSpaces));
260
+ continue;
261
+ }
175
262
  }
176
- out.push(output);
263
+ // Espaçamento em redirecionamentos: >file → > file (preservando atômicos)
264
+ out.push(aplicarEspacamentoRedirecionamento(output));
177
265
  }
178
266
  return out;
179
267
  }
268
+ /* ────────────────────────── Validação de sintaxe ────────────────────────── */
269
+ /**
270
+ * Valida a sintaxe shell usando bash -n.
271
+ * Retorna true se a sintaxe for válida, false caso contrário.
272
+ */
273
+ function validarSintaxeShell(code) {
274
+ try {
275
+ execSync('bash -n', { input: code, timeout: 5000, stdio: ['pipe', 'ignore', 'ignore'] });
276
+ return true;
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ }
180
282
  /* ────────────────────────── Public API ────────────────────────── */
181
283
  /**
182
284
  * Shell formatter com detecção de contexto para here-docs, quoting e continuação.
285
+ * Inclui gate de segurança: se bash -n falhar, retorna o código original sem formatação.
183
286
  */
184
287
  export function formatarShellMinimo(code) {
185
288
  const normalized = normalizarFimDeLinha(removerBom(code));
@@ -193,6 +296,22 @@ export function formatarShellMinimo(code) {
193
296
  const formatted = normalizarNewlinesFinais(comPontuacao);
194
297
  const baseline = normalizarNewlinesFinais(normalized);
195
298
  const anyChanged = formatted !== baseline;
299
+ // Gate de segurança: validar sintaxe com bash -n, reverter se falhar
300
+ if (anyChanged && lines.length > 1) {
301
+ try {
302
+ execSync('bash -n', { input: formatted, timeout: 5000, stdio: ['pipe', 'ignore', 'ignore'] });
303
+ }
304
+ catch {
305
+ // bash -n falhou → reverter para o código original
306
+ return {
307
+ ok: true,
308
+ parser: 'shell',
309
+ formatted: baseline,
310
+ changed: false,
311
+ reason: 'shell-syntax-error-revert'
312
+ };
313
+ }
314
+ }
196
315
  return {
197
316
  ok: true,
198
317
  parser: 'shell',
@@ -30,6 +30,9 @@ export class DedupeManager {
30
30
  export function runUniqueVisitor(ast, relPath, visitor, initialState = {}) {
31
31
  const manager = new DedupeManager(relPath);
32
32
  const state = { ...initialState, manager, relPath };
33
+ if (ast === null || ast === undefined) {
34
+ return [];
35
+ }
33
36
  const nodeToTraverse = ast.node ?? ast;
34
37
  if (!nodeToTraverse) {
35
38
  return [];
@@ -6,7 +6,7 @@
6
6
  * Não toca disco; apenas retorna o novo conteúdo.
7
7
  */
8
8
  import path from 'node:path';
9
- import { normalizePath } from './index.js';
9
+ import { normalizePath } from './path.js';
10
10
  /**
11
11
  * Normaliza o caminho de import para uma chave consistente (POSIX).
12
12
  */
@@ -13,6 +13,13 @@ export async function lerRelatorioVersionado(options) {
13
13
  try {
14
14
  // Ler arquivo
15
15
  const conteudo = await lerEstado(caminho);
16
+ // Verificar se arquivo existe
17
+ if (conteudo === null || conteudo === undefined) {
18
+ return {
19
+ sucesso: false,
20
+ erro: 'Arquivo não encontrado ou vazio',
21
+ };
22
+ }
16
23
  let relatorioFinal = conteudo;
17
24
  let migrado = false;
18
25
  // Validar schema se solicitado
@@ -92,6 +99,12 @@ export async function lerDadosRelatorio(caminho) {
92
99
  export async function verificarSchemaRelatorio(caminho) {
93
100
  try {
94
101
  const conteudo = await lerEstado(caminho);
102
+ if (conteudo === null || conteudo === undefined) {
103
+ return {
104
+ valido: false,
105
+ erros: ['Arquivo não encontrado ou vazio'],
106
+ };
107
+ }
95
108
  const validacao = validarSchema(conteudo);
96
109
  return {
97
110
  valido: validacao.valido,
@@ -268,9 +268,9 @@ export function normalizarRel(relPath) {
268
268
  export async function carregarConfigEstrategia(baseDir, overrides) {
269
269
  const caminho = path.join(baseDir, '.mahoraga', 'estrutura.json');
270
270
  const lido = await lerEstado(caminho);
271
- const cfgObj = !Array.isArray(lido) && typeof lido === 'object' ? lido : {};
271
+ const cfgObj = lido !== null && !Array.isArray(lido) && typeof lido === 'object' ? lido : {};
272
272
  const nomePreset = overrides?.preset ?? cfgObj['preset'];
273
- const preset = nomePreset && PRESETS[nomePreset].nome ? PRESETS[nomePreset] : undefined;
273
+ const preset = nomePreset && PRESETS[nomePreset]?.nome ? PRESETS[nomePreset] : undefined;
274
274
  const base = {
275
275
  raizCodigo: 'src',
276
276
  criarSubpastasPorEntidade: true,
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Type guard: verifica se um valor é um GuardianResult válido.
4
+ * Aceita null/undefined como válido (representa "sem resultado ainda").
5
+ */
6
+ export function isGuardianResult(value) {
7
+ if (value === null || value === undefined) {
8
+ return true;
9
+ }
10
+ if (typeof value !== 'object') {
11
+ return false;
12
+ }
13
+ const obj = value;
14
+ if (typeof obj.status !== 'string') {
15
+ return false;
16
+ }
17
+ const knownStatuses = ['ok', 'erro', 'alteracoes-detectadas', 'baseline-aceito', 'baseline-criado'];
18
+ return knownStatuses.includes(obj.status);
19
+ }
20
+ /**
21
+ * Converte/conforma um GuardianResult.
22
+ * - undefined → null
23
+ * - status 'baseline-aceito' → 'ok'
24
+ * - status desconhecido retorna null
25
+ */
26
+ export function converterResultadoGuardian(result) {
27
+ if (result === undefined) {
28
+ return null;
29
+ }
30
+ if (result.status === 'baseline-aceito') {
31
+ return { ...result, status: 'ok' };
32
+ }
33
+ const knownStatuses = ['ok', 'erro', 'alteracoes-detectadas'];
34
+ if (!knownStatuses.includes(result.status)) {
35
+ return null;
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Verifica se um valor é uma PendenciaProcessavel válida.
4
+ * Deve ter `arquivo` (string) e `motivo` (string).
5
+ */
6
+ export function isPendenciaProcessavel(value) {
7
+ if (value === null || value === undefined || typeof value !== 'object') {
8
+ return false;
9
+ }
10
+ const obj = value;
11
+ return typeof obj.arquivo === 'string' && typeof obj.motivo === 'string';
12
+ }