@pathbit/agentdevkit 0.0.10 → 0.0.12

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 (134) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +6 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +3 -0
  3. package/.pbai4devs/agents/README.md +19 -0
  4. package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
  5. package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
  6. package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
  7. package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
  8. package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
  9. package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
  10. package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
  11. package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
  12. package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
  13. package/.pbai4devs/agents/documentador/AGENT.md +19 -0
  14. package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
  15. package/.pbai4devs/agents/documentador/claude.yaml +5 -0
  16. package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
  17. package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
  18. package/.pbai4devs/agents/documentador/github.yaml +5 -0
  19. package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
  20. package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
  21. package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
  22. package/.pbai4devs/agents/implementador/AGENT.md +20 -0
  23. package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
  24. package/.pbai4devs/agents/implementador/claude.yaml +5 -0
  25. package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
  26. package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
  27. package/.pbai4devs/agents/implementador/github.yaml +5 -0
  28. package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
  29. package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
  30. package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
  31. package/.pbai4devs/agents/planejador/AGENT.md +19 -0
  32. package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
  33. package/.pbai4devs/agents/planejador/claude.yaml +5 -0
  34. package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
  35. package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
  36. package/.pbai4devs/agents/planejador/github.yaml +5 -0
  37. package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
  38. package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
  39. package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
  40. package/.pbai4devs/agents/validador/AGENT.md +19 -0
  41. package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
  42. package/.pbai4devs/agents/validador/claude.yaml +5 -0
  43. package/.pbai4devs/agents/validador/cursor.yaml +5 -0
  44. package/.pbai4devs/agents/validador/gemini.yaml +6 -0
  45. package/.pbai4devs/agents/validador/github.yaml +5 -0
  46. package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
  47. package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
  48. package/.pbai4devs/agents/validador/opencode.yaml +11 -0
  49. package/.pbai4devs/commands/README.md +13 -0
  50. package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
  51. package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
  52. package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
  53. package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
  54. package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
  55. package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
  56. package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
  57. package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
  58. package/.pbai4devs/external-skills/CATALOG.json +39 -1
  59. package/.pbai4devs/external-skills/README.md +13 -0
  60. package/.pbai4devs/rules/README.md +4 -0
  61. package/.pbai4devs/rules/documentacao/RULE.md +2 -0
  62. package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
  63. package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
  64. package/.pbai4devs/rules/seguranca/RULE.md +19 -0
  65. package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
  66. package/.pbai4devs/scripts/README.md +1 -0
  67. package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
  68. package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
  69. package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
  70. package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
  71. package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
  72. package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
  73. package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
  74. package/.pbai4devs/scripts/lib/README.md +4 -0
  75. package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
  76. package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
  77. package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
  78. package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
  79. package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
  80. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
  81. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
  82. package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
  83. package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
  84. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
  85. package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
  86. package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
  87. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
  88. package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
  89. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
  90. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
  91. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
  92. package/.pbai4devs/skills/README.md +10 -2
  93. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
  94. package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
  95. package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
  96. package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
  97. package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
  98. package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
  99. package/.pbai4devs/templates/README.md +2 -0
  100. package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
  101. package/.pbai4devs/tools/README.md +11 -3
  102. package/.pbai4devs/tools/gitleaks/README.md +22 -0
  103. package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
  104. package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
  105. package/.pbai4devs/tools/opengrep/README.md +22 -0
  106. package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
  107. package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
  108. package/.pbai4devs/tools/osv-scanner/README.md +23 -0
  109. package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
  110. package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
  111. package/CHANGELOG.md +17 -0
  112. package/README.en.md +3 -2
  113. package/README.es.md +3 -2
  114. package/README.md +8 -3
  115. package/docs/README.md +6 -3
  116. package/docs/guides/README.md +1 -0
  117. package/docs/guides/cli.md +2 -1
  118. package/docs/guides/installation.md +25 -8
  119. package/docs/guides/resources.md +48 -1
  120. package/docs/guides/security-references.md +97 -0
  121. package/docs/guides/security.md +32 -0
  122. package/docs/guides/troubleshooting.md +8 -5
  123. package/docs/maintainers/maintenance.md +12 -0
  124. package/docs/maintainers/packages.md +2 -2
  125. package/docs/product/design.md +10 -3
  126. package/docs/product/prd.md +5 -1
  127. package/docs/product/roadmap.md +4 -0
  128. package/docs/specs/README.md +4 -0
  129. package/docs/specs/additive-integration/README.md +9 -0
  130. package/docs/specs/additive-integration/SPEC.md +53 -0
  131. package/docs/specs/security-ecosystem/README.md +9 -0
  132. package/docs/specs/security-ecosystem/SPEC.md +53 -0
  133. package/package.json +1 -1
  134. package/src/install.mjs +139 -63
@@ -0,0 +1,192 @@
1
+ import { extractManagedBlock, managedBlockDigest, managedText, mergeManagedContent, stripManagedContent } from './managed-content.mjs';
2
+
3
+ // Localized editing of Gemini's root prompt; not a general TOML serializer.
4
+ const fail = message => { throw Object.assign(new Error(message), { code: 'E_MANAGED_BLOCK' }); };
5
+ const horizontal = character => character === ' ' || character === '\t';
6
+ const whitespace = character => horizontal(character) || character === '\n' || character === '\r';
7
+ const escapes = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
8
+
9
+ function scalar(code) {
10
+ if (code > 0x10ffff || code >= 0xd800 && code <= 0xdfff) fail('Escape Unicode TOML invalido.');
11
+ return String.fromCodePoint(code);
12
+ }
13
+
14
+ function stringAt(text, start, key = false) {
15
+ const quote = text[start];
16
+ if (!['"', "'"].includes(quote)) fail('O prompt TOML deve ser uma string.');
17
+ const multiline = text.slice(start, start + 3) === quote.repeat(3);
18
+ if (key && multiline) fail('Chave TOML multiline nao suportada.');
19
+ const basic = quote === '"';
20
+ let position = start + (multiline ? 3 : 1), value = '';
21
+ if (multiline) {
22
+ if (text.slice(position, position + 2) === '\r\n') position += 2;
23
+ else if (text[position] === '\n') position++;
24
+ }
25
+ while (position < text.length) {
26
+ const character = text[position];
27
+ if (character === quote) {
28
+ if (!multiline) return { start, end: position + 1, value };
29
+ let count = 1;
30
+ while (text[position + count] === quote) count++;
31
+ if (count >= 3) {
32
+ if (count > 5) fail('Delimitador TOML ambiguo.');
33
+ return { start, end: position + count, value: value + quote.repeat(count - 3) };
34
+ }
35
+ value += quote.repeat(count); position += count; continue;
36
+ }
37
+ if (basic && character === '\\') {
38
+ const escaped = text[++position];
39
+ if (multiline && whitespace(escaped)) {
40
+ let cursor = position, lineBreak = false;
41
+ while (whitespace(text[cursor])) {
42
+ if (text[cursor] === '\r') {
43
+ if (text[cursor + 1] !== '\n') fail('TOML exige LF ou CRLF.');
44
+ lineBreak = true; cursor += 2;
45
+ } else { if (text[cursor] === '\n') lineBreak = true; cursor++; }
46
+ }
47
+ if (!lineBreak) fail('Continuacao de string TOML invalida.');
48
+ position = cursor; continue;
49
+ }
50
+ if (Object.hasOwn(escapes, escaped)) { value += escapes[escaped]; position++; continue; }
51
+ if (escaped === 'u' || escaped === 'U') {
52
+ const width = escaped === 'u' ? 4 : 8, digits = text.slice(position + 1, position + 1 + width);
53
+ if (!new RegExp(`^[0-9a-fA-F]{${width}}$`).test(digits)) fail('Escape Unicode TOML invalido.');
54
+ value += scalar(Number.parseInt(digits, 16)); position += width + 1; continue;
55
+ }
56
+ fail('Escape TOML nao suportado.');
57
+ }
58
+ const code = text.charCodeAt(position);
59
+ if (code === 0x7f || code < 0x20 && character !== '\t' && !(multiline && ['\r', '\n'].includes(character))) fail('Controle invalido em string TOML.');
60
+ if (!multiline && ['\r', '\n'].includes(character)) fail('String TOML de uma linha interrompida.');
61
+ if (character === '\r' && text[position + 1] !== '\n') fail('TOML exige LF ou CRLF.');
62
+ value += character; position++;
63
+ }
64
+ fail('String TOML sem fechamento.');
65
+ }
66
+
67
+ function keyAt(text, start) {
68
+ let position = start;
69
+ const path = [];
70
+ while (true) {
71
+ while (horizontal(text[position])) position++;
72
+ if (['"', "'"].includes(text[position])) {
73
+ const part = stringAt(text, position, true); path.push(part.value); position = part.end;
74
+ } else {
75
+ const part = /^[A-Za-z0-9_-]+/.exec(text.slice(position));
76
+ if (!part) fail('Chave TOML nao suportada.');
77
+ path.push(part[0]); position += part[0].length;
78
+ }
79
+ while (horizontal(text[position])) position++;
80
+ if (text[position] !== '.') break;
81
+ position++;
82
+ }
83
+ return { path, end: position };
84
+ }
85
+
86
+ function valueEnd(text, start) {
87
+ if (['"', "'"].includes(text[start])) return stringAt(text, start).end;
88
+ const stack = [];
89
+ let position = start;
90
+ while (position < text.length) {
91
+ const character = text[position];
92
+ if (['"', "'"].includes(character)) { position = stringAt(text, position).end; continue; }
93
+ if (character === '#') {
94
+ if (!stack.length) break;
95
+ while (position < text.length && !['\r', '\n'].includes(text[position])) position++;
96
+ continue;
97
+ }
98
+ if (['\n', '\r'].includes(character) && !stack.length) break;
99
+ if (character === '[' || character === '{') stack.push(character);
100
+ if (character === ']' || character === '}') {
101
+ if (stack.pop() !== (character === ']' ? '[' : '{')) fail('Valor TOML desbalanceado.');
102
+ }
103
+ position++;
104
+ }
105
+ if (stack.length || !text.slice(start, position).trim()) fail('Valor TOML incompleto.');
106
+ return position;
107
+ }
108
+
109
+ function statementEnd(text, start) {
110
+ let position = start;
111
+ while (horizontal(text[position])) position++;
112
+ if (text[position] === '#') while (position < text.length && !['\r', '\n'].includes(text[position])) position++;
113
+ if (position < text.length && !['\r', '\n'].includes(text[position])) fail('Texto inesperado apos valor TOML.');
114
+ if (text[position] === '\r') {
115
+ if (text[position + 1] !== '\n') fail('TOML exige LF ou CRLF.');
116
+ return position + 2;
117
+ }
118
+ return text[position] === '\n' ? position + 1 : position;
119
+ }
120
+
121
+ function locatePrompt(input) {
122
+ let text;
123
+ try { text = managedText(input); }
124
+ catch (error) {
125
+ if (error.code === 'E_MANAGED_BLOCK') throw error;
126
+ fail('Texto TOML deve ser UTF-8 valido.');
127
+ }
128
+ for (const character of text) {
129
+ const code = character.codePointAt(0);
130
+ if (code >= 0xd800 && code <= 0xdfff) fail('Unicode TOML invalido.');
131
+ }
132
+ let position = text.startsWith('\ufeff') ? 1 : 0, root = true, insertion = text.length, prompt = null, reserved = false;
133
+ while (position < text.length) {
134
+ const lineStart = position;
135
+ while (horizontal(text[position])) position++;
136
+ if (position === text.length) break;
137
+ if (text[position] === '#' || ['\r', '\n'].includes(text[position])) { position = statementEnd(text, position); continue; }
138
+ if (text[position] === '[') {
139
+ const arrayTable = text[position + 1] === '[';
140
+ const header = keyAt(text, position + (arrayTable ? 2 : 1));
141
+ if (text.slice(header.end, header.end + (arrayTable ? 2 : 1)) !== (arrayTable ? ']]' : ']')) fail('Tabela TOML invalida.');
142
+ if (root) insertion = lineStart;
143
+ root = false;
144
+ if (header.path[0] === 'prompt') reserved = true;
145
+ position = statementEnd(text, header.end + (arrayTable ? 2 : 1)); continue;
146
+ }
147
+ const key = keyAt(text, position);
148
+ if (text[key.end] !== '=') fail('Atribuicao TOML invalida.');
149
+ position = key.end + 1;
150
+ while (horizontal(text[position])) position++;
151
+ if (root && key.path[0] === 'prompt') {
152
+ if (key.path.length !== 1 || prompt || reserved) fail('Prompt TOML duplicado ou ambiguo.');
153
+ prompt = stringAt(text, position);
154
+ position = prompt.end;
155
+ } else position = valueEnd(text, position);
156
+ position = statementEnd(text, position);
157
+ }
158
+ if (reserved) fail('Prompt TOML ocupa uma tabela; conteudo preservado.');
159
+ return { text, prompt, insertion };
160
+ }
161
+
162
+ function encode(value) {
163
+ return JSON.stringify(value).replace(/\x7f/g, '\\u007F');
164
+ }
165
+
166
+ export function extractTomlManagedBlock(input) {
167
+ const { prompt } = locatePrompt(input);
168
+ return prompt ? extractManagedBlock(prompt.value) : null;
169
+ }
170
+
171
+ export function tomlManagedBlockDigest(input) {
172
+ const { prompt } = locatePrompt(input);
173
+ return prompt ? managedBlockDigest(prompt.value) : null;
174
+ }
175
+
176
+ export function mergeTomlContent(input, body, options = {}) {
177
+ const { text, prompt, insertion } = locatePrompt(input);
178
+ const result = mergeManagedContent(prompt?.value ?? '', body, options);
179
+ if (prompt && !result.changed) return { ...result, text, changed: false };
180
+ const eol = text.match(/\r\n|\n/)?.[0] || '\n';
181
+ const separator = insertion && !/[\r\n]$/.test(text.slice(0, insertion)) ? eol : '';
182
+ const content = prompt ? text.slice(0, prompt.start) + encode(result.text) + text.slice(prompt.end)
183
+ : text.slice(0, insertion) + separator + `prompt = ${encode(result.text)}${eol}` + text.slice(insertion);
184
+ return { ...result, text: content, changed: content !== text };
185
+ }
186
+
187
+ export function stripTomlContent(input, expectedHash) {
188
+ const { text, prompt } = locatePrompt(input);
189
+ if (!prompt) return text;
190
+ const stripped = stripManagedContent(prompt.value, expectedHash);
191
+ return stripped === prompt.value ? text : text.slice(0, prompt.start) + encode(stripped) + text.slice(prompt.end);
192
+ }
@@ -0,0 +1,193 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmodSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { gunzipSync, inflateRawSync } from 'node:zlib';
5
+
6
+ const limit = 256 * 1024 * 1024;
7
+ const hosts = new Set(['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com', 'github-releases.githubusercontent.com']);
8
+ const fail = message => { throw new Error(message); };
9
+ export const nativeHash = bytes => createHash('sha256').update(bytes).digest('hex');
10
+
11
+ function safeName(value, directory = false) {
12
+ if (typeof value !== 'string' || !value || /[\\\x00-\x1f:]/.test(value) || isAbsolute(value)) fail('Caminho invalido no arquivo nativo.');
13
+ const normalized = directory ? value.replace(/\/$/, '') : value;
14
+ if (!normalized || normalized.split('/').some(part => !part || part === '.' || part === '..')) fail('Caminho invalido no arquivo nativo.');
15
+ return normalized;
16
+ }
17
+
18
+ export function validateNativeDownload(download, platforms) {
19
+ if (download?.provider !== 'github-release' || download.channel !== 'reviewed'
20
+ || !/^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/.test(download.repository || '')
21
+ || !/^\d+\.\d+\.\d+$/.test(download.version || '') || download.tag !== `v${download.version}`) fail('Release nativa deve usar origem e versao revisadas.');
22
+ if (!download.assets || typeof download.assets !== 'object' || Array.isArray(download.assets)
23
+ || Object.keys(download.assets).length !== platforms.length) fail('Assets nativos devem corresponder as plataformas.');
24
+ for (const platform of platforms) {
25
+ const asset = download.assets[platform];
26
+ if (!Object.hasOwn(download.assets, platform) || !asset || typeof asset !== 'object' || !['raw', 'tar.gz', 'zip'].includes(asset.format)
27
+ || !/^[a-f0-9]{64}$/.test(asset.sha256 || '') || !/^\w[\w.-]*$/.test(asset.file || '')) fail(`Asset nativo invalido: ${platform}`);
28
+ safeName(asset.binary);
29
+ }
30
+ return download;
31
+ }
32
+
33
+ export function nativeRelease(entry, platform) {
34
+ validateNativeDownload(entry.download, entry.platforms);
35
+ const asset = entry.download.assets[platform];
36
+ if (!asset) fail(`Plataforma nativa nao suportada: ${platform}`);
37
+ const { repository, version, tag } = entry.download;
38
+ return { name: entry.name, version, repository, platform, asset: { ...asset },
39
+ url: `https://github.com/${repository}/releases/download/${tag}/${asset.file}` };
40
+ }
41
+
42
+ export function validateNativeRelease(entry, release, platform) {
43
+ const expected = nativeRelease(entry, platform);
44
+ if (!release || JSON.stringify(release) !== JSON.stringify(expected)) fail('Release nativa diverge do pin revisado.');
45
+ return release;
46
+ }
47
+
48
+ export async function downloadNative(url, { fetcher = fetch, maxBytes = limit, timeoutMs = 60000 } = {}) {
49
+ let current = new URL(url);
50
+ if (current.hostname !== 'github.com') fail('Download nativo deve iniciar no GitHub oficial.');
51
+ for (let redirects = 0; redirects <= 5; redirects++) {
52
+ if (current.protocol !== 'https:' || current.username || current.password || !hosts.has(current.hostname)) fail('Host de download nativo nao permitido.');
53
+ const response = await fetcher(current.href, { redirect: 'manual', headers: { Accept: 'application/octet-stream' }, signal: AbortSignal.timeout(timeoutMs) });
54
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
55
+ const location = response.headers.get('location');
56
+ if (!location || redirects === 5) fail('Redirecionamento nativo invalido.');
57
+ current = new URL(location, current);
58
+ await response.body?.cancel();
59
+ continue;
60
+ }
61
+ if (!response.ok) fail(`Download nativo falhou: HTTP ${response.status}.`);
62
+ const declared = Number(response.headers.get('content-length'));
63
+ if (Number.isFinite(declared) && declared > maxBytes) fail('Download nativo excede o limite.');
64
+ const chunks = []; let size = 0;
65
+ for await (const chunk of response.body) {
66
+ size += chunk.byteLength;
67
+ if (size > maxBytes) fail('Download nativo excede o limite.');
68
+ chunks.push(Buffer.from(chunk));
69
+ }
70
+ if (!size) fail('Download nativo vazio.');
71
+ return Buffer.concat(chunks, size);
72
+ }
73
+ fail('Redirecionamentos nativos excedidos.');
74
+ }
75
+
76
+ function tarBytes(archive, binary) {
77
+ const data = gunzipSync(archive, { maxOutputLength: limit });
78
+ const names = new Set(); let selected, ended = false;
79
+ for (let offset = 0; offset + 512 <= data.length;) {
80
+ const header = data.subarray(offset, offset + 512);
81
+ if (header.every(value => value === 0)) {
82
+ if (data.length - offset < 1024 || !data.subarray(offset).every(value => value === 0)) fail('Final tar invalido.');
83
+ ended = true; break;
84
+ }
85
+ const text = (start, length) => header.subarray(start, start + length).toString('utf8').split('\0')[0];
86
+ const number = (start, length) => { const value = text(start, length).trim(); if (!/^[0-7]+$/.test(value)) fail('Cabecalho tar invalido.'); return parseInt(value, 8); };
87
+ const checksum = [...header].reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
88
+ if (number(148, 8) !== checksum) fail('Checksum do cabecalho tar invalido.');
89
+ const type = String.fromCharCode(header[156]), directory = type === '5';
90
+ if (!['0', '\0', '5'].includes(type)) fail('Links ou entradas especiais nao permitidos no tar.');
91
+ const name = safeName([text(345, 155), text(0, 100)].filter(Boolean).join('/'), directory);
92
+ const key = name.toLowerCase(); if (names.has(key)) fail('Entrada duplicada no tar.'); names.add(key);
93
+ const size = number(124, 12); offset += 512;
94
+ if (size > limit || offset + size > data.length || directory && size) fail('Tamanho tar invalido.');
95
+ if (!directory && name === binary) selected = Buffer.from(data.subarray(offset, offset + size));
96
+ offset += Math.ceil(size / 512) * 512;
97
+ }
98
+ if (!ended) fail('Arquivo tar incompleto.');
99
+ if (!selected?.length) fail('Executavel esperado ausente no tar.');
100
+ return selected;
101
+ }
102
+
103
+ function crc32(bytes) {
104
+ let crc = 0xffffffff;
105
+ for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); }
106
+ return (crc ^ 0xffffffff) >>> 0;
107
+ }
108
+
109
+ function zipBytes(data, binary) {
110
+ let end = -1;
111
+ for (let offset = data.length - 22; offset >= Math.max(0, data.length - 65557); offset--) {
112
+ if (data.readUInt32LE(offset) === 0x06054b50 && offset + 22 + data.readUInt16LE(offset + 20) === data.length) { end = offset; break; }
113
+ }
114
+ if (end < 0 || data.readUInt16LE(end + 4) || data.readUInt16LE(end + 6)) fail('Arquivo ZIP invalido ou multipartes.');
115
+ const count = data.readUInt16LE(end + 10), centralSize = data.readUInt32LE(end + 12), central = data.readUInt32LE(end + 16);
116
+ if (count === 0xffff || count !== data.readUInt16LE(end + 8) || central + centralSize !== end) fail('Diretorio ZIP invalido ou ZIP64.');
117
+ const names = new Set(); let selected, offset = central;
118
+ for (let index = 0; index < count; index++) {
119
+ if (offset + 46 > end || data.readUInt32LE(offset) !== 0x02014b50) fail('Entrada ZIP invalida.');
120
+ const flags = data.readUInt16LE(offset + 8), method = data.readUInt16LE(offset + 10), crc = data.readUInt32LE(offset + 16);
121
+ const compressed = data.readUInt32LE(offset + 20), size = data.readUInt32LE(offset + 24);
122
+ const nameLength = data.readUInt16LE(offset + 28), extra = data.readUInt16LE(offset + 30), comment = data.readUInt16LE(offset + 32);
123
+ const disk = data.readUInt16LE(offset + 34), attributes = data.readUInt32LE(offset + 38), local = data.readUInt32LE(offset + 42);
124
+ if (offset + 46 + nameLength + extra + comment > end || flags & 1 || disk || ![0, 8].includes(method) || size > limit || compressed > limit) fail('Formato ZIP nao permitido.');
125
+ const original = data.subarray(offset + 46, offset + 46 + nameLength).toString('utf8');
126
+ const directory = original.endsWith('/'), name = safeName(original, directory), mode = attributes >>> 16, type = mode & 0o170000;
127
+ if (![0, 0o100000, 0o040000].includes(type) || type === 0o040000 && !directory) fail('Link ou entrada especial no ZIP.');
128
+ const key = name.toLowerCase(); if (names.has(key)) fail('Entrada duplicada no ZIP.'); names.add(key);
129
+ if (local + 30 > central || data.readUInt32LE(local) !== 0x04034b50 || data.readUInt16LE(local + 6) !== flags || data.readUInt16LE(local + 8) !== method) fail('Cabecalho local ZIP divergente.');
130
+ const localNameLength = data.readUInt16LE(local + 26), localExtra = data.readUInt16LE(local + 28), start = local + 30 + localNameLength + localExtra;
131
+ if (start + compressed > central || data.subarray(local + 30, local + 30 + localNameLength).toString('utf8') !== original) fail('Caminho local ZIP divergente.');
132
+ if (!directory && name === binary) {
133
+ const packed = data.subarray(start, start + compressed);
134
+ selected = method === 0 ? Buffer.from(packed) : inflateRawSync(packed, { maxOutputLength: limit });
135
+ if (selected.length !== size || crc32(selected) !== crc) fail('Integridade do executavel ZIP invalida.');
136
+ }
137
+ offset += 46 + nameLength + extra + comment;
138
+ }
139
+ if (offset !== end || !selected?.length) fail('Executavel esperado ausente no ZIP.');
140
+ return selected;
141
+ }
142
+
143
+ export function extractNative(archive, asset) {
144
+ if (!Buffer.isBuffer(archive) || !archive.length || archive.length > limit) fail('Tamanho de arquivo nativo invalido.');
145
+ safeName(asset.binary);
146
+ if (nativeHash(archive) !== asset.sha256) fail('SHA-256 do download nativo diverge do pin revisado.');
147
+ if (asset.format === 'raw') return archive;
148
+ if (asset.format === 'tar.gz') return tarBytes(archive, asset.binary);
149
+ if (asset.format === 'zip') return zipBytes(archive, asset.binary);
150
+ fail('Formato nativo nao suportado.');
151
+ }
152
+
153
+ function checkedFile(prefix, relativePath) {
154
+ safeName(relativePath);
155
+ const base = resolve(prefix), file = resolve(base, relativePath), child = relative(base, file);
156
+ if (isAbsolute(child) || child === '..' || child.startsWith(`..${sep}`)) fail('Executavel fora da instalacao.');
157
+ if (!lstatSync(base).isDirectory() || lstatSync(base).isSymbolicLink()) fail('Raiz nativa deve ser um diretorio regular.');
158
+ let current = base;
159
+ for (const segment of child.split(sep)) {
160
+ current = join(current, segment);
161
+ try { if (lstatSync(current).isSymbolicLink()) fail('Link simbolico na instalacao nativa.'); }
162
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
163
+ }
164
+ return file;
165
+ }
166
+
167
+ export function nativeExecutable(entry, platform) { return `bin/${entry.name}${platform.startsWith('win32-') ? '.exe' : ''}`; }
168
+
169
+ export async function installNative(prefix, entry, platform, { download = downloadNative } = {}) {
170
+ const release = nativeRelease(entry, platform), archive = await download(release.url), bytes = extractNative(archive, release.asset);
171
+ const executable = nativeExecutable(entry, platform), target = checkedFile(prefix, executable);
172
+ mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, bytes, { flag: 'wx', mode: 0o755 }); chmodSync(target, 0o755);
173
+ const receipt = { name: entry.name, version: release.version, platform, executable, archiveSha256: release.asset.sha256, binarySha256: nativeHash(bytes) };
174
+ writeFileSync(checkedFile(prefix, 'native.json'), JSON.stringify(receipt, null, 2) + '\n', { flag: 'wx' });
175
+ return receipt;
176
+ }
177
+
178
+ export function checkNative(prefix, entry, platform) {
179
+ const file = checkedFile(prefix, 'native.json');
180
+ if (!lstatSync(file).isFile()) fail('Recibo nativo invalido.');
181
+ const receipt = JSON.parse(readFileSync(file, 'utf8'));
182
+ const release = nativeRelease(entry, platform);
183
+ if (receipt.name !== entry.name || receipt.version !== release.version || receipt.platform !== platform
184
+ || receipt.executable !== nativeExecutable(entry, platform) || receipt.archiveSha256 !== release.asset.sha256
185
+ || !/^[a-f0-9]{64}$/.test(receipt.binarySha256 || '')) fail('Recibo nativo diverge do catalogo revisado.');
186
+ const executable = checkedFile(prefix, receipt.executable);
187
+ if (!lstatSync(executable).isFile() || nativeHash(readFileSync(executable)) !== receipt.binarySha256) fail('Executavel nativo alterado.');
188
+ return { ...receipt, file: executable };
189
+ }
190
+
191
+ export function assertNativeVersion(output, version) {
192
+ if (!new RegExp(`(?:^|[^\\w.])v?${version.replaceAll('.', '\\.')}(?=$|[^\\w.+-])`).test(output)) fail('Probe nativo informou outra versao.');
193
+ }
@@ -54,14 +54,20 @@ Locks ativos, de outro host ou sem metadados verificáveis são preservados.
54
54
  Não aceita grupos. Um lock ausente não cria arquivos.
55
55
 
56
56
  O requisito comum é Node.js 24+ com npm. O gerenciador usa o Node atual.
57
- Downloads usam npm oficial com scripts de instalação desativados e integridade
58
- conferida.
57
+ Pacotes npm usam o registro oficial com scripts de instalação desativados.
58
+ Scanners nativos usam assets oficiais do GitHub com versão e SHA-256 revisados.
59
+ `update` segue `latest` no npm e o pin do catálogo nos scanners nativos.
60
+ Os dois caminhos conferem integridade antes de executar e preservam a versão
61
+ anterior se a atualização falhar.
59
62
  Permissões de binários POSIX são aplicadas somente aos caminhos declarados
60
63
  em `executables` no `TOOL.json`, antes do probe.
61
64
  Atualizações usam staging e preservam a versão anterior em caso de falha.
62
65
  Atalhos locais recusam diretórios e arquivos simbólicos antes de escrever.
63
66
  O preparo valida os CLIs. Navegador, permissões desktop e FFmpeg dependem da
64
67
  tarefa; ele não inicia aplicações nem concede permissões automaticamente.
68
+ No grupo `security`, a base OSV também é separada do CLI. Use o comando
69
+ [preparar-seguranca](../../commands/preparar-seguranca/COMMAND.md) para orientar
70
+ instalação, atualização explícita dos dados e análise offline.
65
71
 
66
72
  Testes: `node --test .pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs`.
67
73
  Após instalar `skills`, `just-bash`, `opensrc`, `webreel` e `portless`, execute
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { hostname } from 'node:os';
8
8
  import { projectRoot, requireNode, isMain } from '../lib/runtime.mjs';
9
+ import { validateNativeDownload, nativeRelease, validateNativeRelease, installNative, checkNative, assertNativeVersion } from '../lib/native-release.mjs';
9
10
 
10
11
  const registry = 'https://registry.npmjs.org/';
11
12
  const versionPattern = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/;
@@ -29,9 +30,13 @@ export function validateToolConfig(name, value) {
29
30
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || value?.name !== name) fail(`Nome de ferramenta invalido: ${name}`);
30
31
  if (typeof value.description !== 'string' || !value.description.trim() || value.description.includes('\0')) fail(`Descricao ausente ou invalida: ${name}`);
31
32
  const download = value.download;
32
- if (download?.provider !== 'npm' || download.registry !== registry || download.channel !== 'latest') fail(`Download nao suportado: ${name}`);
33
- if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(download.package || '') || !/^[\w.-]+\/[\w.-]+$/.test(download.repository || '')) fail(`Origem invalida: ${name}`);
33
+ if (!['npm', 'github-release'].includes(download?.provider)) fail(`Download nao suportado: ${name}`);
34
+ if (download.provider === 'npm') {
35
+ if (download.registry !== registry || download.channel !== 'latest') fail(`Download nao suportado: ${name}`);
36
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(download.package || '') || !/^[\w.-]+\/[\w.-]+$/.test(download.repository || '')) fail(`Origem invalida: ${name}`);
37
+ }
34
38
  if (!/^[a-z][a-z0-9-]*$/.test(value.group || '') || !Array.isArray(value.platforms) || !value.platforms.length || new Set(value.platforms).size !== value.platforms.length || value.platforms.some(item => !/^(?:darwin|linux|win32)-(?:arm64|x64)$/.test(item))) fail(`Grupo/plataformas invalidos: ${name}`);
39
+ if (download.provider === 'github-release') validateNativeDownload(download, value.platforms);
35
40
  if (!Array.isArray(value.probe) || !value.probe.length || value.probe.some(item => typeof item !== 'string' || item.includes('\0')) || (value.probeInput !== undefined && (typeof value.probeInput !== 'string' || value.probeInput.includes('\0')))) fail(`Verificacao invalida: ${name}`);
36
41
  if (value.binary !== undefined && (typeof value.binary !== 'string' || !/^[\w.-]+$/.test(value.binary))) fail(`Executavel invalido: ${name}`);
37
42
  if (value.executables !== undefined) {
@@ -72,7 +77,7 @@ export function toolActions(name, entry) {
72
77
  validateToolConfig(name, entry);
73
78
  const descriptions = {
74
79
  install: 'Instala ou reutiliza uma instalacao saudavel.',
75
- update: 'Consulta latest e atualiza com rollback em caso de falha.',
80
+ update: entry.download.provider === 'github-release' ? 'Aplica a versao revisada do catalogo com rollback.' : 'Consulta latest e atualiza com rollback em caso de falha.',
76
81
  check: 'Executa o probe local sem instalar nem consultar latest.',
77
82
  info: 'Mostra configuracao, plataforma, runtime e versao registrada.',
78
83
  run: 'Executa o CLI instalado com os argumentos informados.',
@@ -128,7 +133,8 @@ export function loadCatalog(root = projectRoot) {
128
133
  return catalog;
129
134
  }
130
135
 
131
- export function validateRelease(name, entry, release) {
136
+ export function validateRelease(name, entry, release, platform = platformKey()) {
137
+ if (entry.download.provider === 'github-release') return validateNativeRelease(entry, release, platform);
132
138
  const expected = entry.download.package;
133
139
  const repository = typeof release.repository === 'string' ? release.repository : release.repository?.url;
134
140
  if (release.name !== expected || !versionPattern.test(release.version || '')) fail(`Nome/versao invalido no registro: ${name}`);
@@ -165,6 +171,9 @@ export function binaryInvocation(prefix, name, args = [], binaryName = name.spli
165
171
 
166
172
  export function toolEnvironment(entry, local, runtime) {
167
173
  const environment = Object.assign(Object.create(null), process.env);
174
+ if (entry.download?.provider === 'github-release') {
175
+ for (const key of Object.keys(environment)) if (/token|secret|password|(?:^|_)auth(?:_|$)|^(?:semgrep|opengrep)_/i.test(key)) delete environment[key];
176
+ }
168
177
  const set = (key, value) => {
169
178
  if (process.platform === 'win32') for (const existing of Object.keys(environment)) if (existing.toUpperCase() === key.toUpperCase()) delete environment[existing];
170
179
  environment[key] = value;
@@ -184,10 +193,13 @@ export function toolEnvironment(entry, local, runtime) {
184
193
  }
185
194
 
186
195
  function execute(prefix, entry, args, capture = false, input, environment, runtime) {
187
- const [file, options] = binaryInvocation(prefix, entry.download.package, args, entry.binary || entry.name, runtime.file);
196
+ const native = entry.download.provider === 'github-release';
197
+ const [file, options] = native ? [checkNative(prefix, entry, runtime.platform || platformKey()).file, args]
198
+ : binaryInvocation(prefix, entry.download.package, args, entry.binary || entry.name, runtime.file);
188
199
  const result = spawnSync(file, options, { stdio: capture ? 'pipe' : 'inherit', cwd: capture ? prefix : undefined, input, env: environment, encoding: 'utf8', timeout: capture ? 30000 : undefined, windowsHide: true });
189
200
  if (result.error) throw result.error;
190
201
  if (capture && result.status !== 0) fail(`${entry.name}: ${result.stderr?.trim().slice(0, 400) || result.stdout?.trim().slice(0, 400) || `falha ${result.status}`}`, result.status ?? 1);
202
+ if (capture && native) assertNativeVersion(`${result.stdout || ''}\n${result.stderr || ''}`, entry.download.version);
191
203
  return result.status ?? 1;
192
204
  }
193
205
 
@@ -215,11 +227,13 @@ export class ToolManager {
215
227
  this.catalog = catalog ?? loadCatalog(this.root);
216
228
  for (const [name, entry] of Object.entries(this.catalog)) validateToolConfig(name, entry);
217
229
  this.fetchRelease = fetchRelease || (async (name, entry) => {
230
+ if (entry.download.provider === 'github-release') return nativeRelease(entry, this.target);
218
231
  const response = await fetch(`${entry.download.registry}${encodeURIComponent(entry.download.package)}/${entry.download.channel}`, { signal: AbortSignal.timeout(20000) });
219
232
  if (!response.ok) fail(`Registro indisponivel: HTTP ${response.status}`);
220
233
  return response.json();
221
234
  });
222
- this.installPackage = install || ((prefix, packageName, version, entry, runtime) => {
235
+ this.installPackage = install || (async (prefix, packageName, version, entry, runtime) => {
236
+ if (entry.download.provider === 'github-release') return installNative(prefix, entry, this.target);
223
237
  const environment = toolEnvironment(entry, this.local(entry.name), runtime);
224
238
  const result = spawnSync(runtime.file, [this.npm(runtime, environment), 'install', '--prefix', prefix, '--cache', join(this.home, '.cache/npm'), '--registry', entry.download.registry, '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund', '--save-exact', `${packageName}@${version}`], {
225
239
  cwd: prefix, encoding: 'utf8', timeout: 180000,
@@ -236,7 +250,7 @@ export class ToolManager {
236
250
  const entry = this.entry(name);
237
251
  if (!entry.platforms.includes(this.target)) fail(`Plataforma nao suportada por ${name}: ${this.target}`);
238
252
  requireNode(24, this.nodeVersion);
239
- return { file: process.execPath, version: process.version };
253
+ return { file: process.execPath, version: process.version, platform: this.target };
240
254
  }
241
255
  preflight(names) {
242
256
  const issues = [];
@@ -298,8 +312,13 @@ export class ToolManager {
298
312
  }
299
313
  check(name) {
300
314
  const entry = this.entry(name); const runtime = this.runtime(name); const info = this.current(name);
301
- const pkg = json(join(info.prefix, 'node_modules', entry.download.package, 'package.json'));
302
- if (pkg.name !== entry.download.package || pkg.version !== info.version) fail(`Versao local divergente: ${name}`);
315
+ if (entry.download.provider === 'github-release') {
316
+ const receipt = checkNative(info.prefix, entry, this.target);
317
+ if (receipt.version !== info.version || receipt.binarySha256 !== info.binarySha256) fail(`Versao local divergente: ${name}`);
318
+ } else {
319
+ const pkg = json(join(info.prefix, 'node_modules', entry.download.package, 'package.json'));
320
+ if (pkg.name !== entry.download.package || pkg.version !== info.version) fail(`Versao local divergente: ${name}`);
321
+ }
303
322
  execute(info.prefix, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, this.local(name), runtime), runtime);
304
323
  return info;
305
324
  }
@@ -399,7 +418,7 @@ export class ToolManager {
399
418
  renameSync(storagePath(this.root, temporary, 'file'), storagePath(this.root, join(local, 'CURRENT.json'), 'file'));
400
419
  }
401
420
  async updateLocked(name) {
402
- const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry));
421
+ const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry), this.target);
403
422
  const { local } = this.storage(name);
404
423
  const target = storagePath(this.root, join(local, 'versions', `${release.version}-${this.target}`), 'directory');
405
424
  let installed;
@@ -409,14 +428,21 @@ export class ToolManager {
409
428
  const stage = mkdtempSync(join(local, '.stage-'));
410
429
  const temporary = storagePath(this.root, join(local, `CURRENT.${randomUUID()}.json`), 'file'); let previous; let moved = false;
411
430
  try {
412
- this.installPackage(stage, entry.download.package, release.version, entry, runtime);
431
+ if (entry.download.provider === 'github-release') {
432
+ for (const location of Object.values(entry.envPaths || {})) mkdirSync(storagePath(this.root, join(local, location), 'directory'), { recursive: true });
433
+ }
434
+ await this.installPackage(stage, entry.download.package, release.version, entry, runtime);
413
435
  this.storage(name); storagePath(this.root, stage, 'directory');
414
- const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
415
- if (pkg.name !== entry.download.package || pkg.version !== release.version || lock.packages?.[`node_modules/${entry.download.package}`]?.integrity !== release.dist.integrity) fail('Pacote instalado nao corresponde ao registro.');
416
- prepareExecutables(stage, entry, this.target);
436
+ let receipt;
437
+ if (entry.download.provider === 'github-release') receipt = checkNative(stage, entry, this.target);
438
+ else {
439
+ const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
440
+ if (pkg.name !== entry.download.package || pkg.version !== release.version || lock.packages?.[`node_modules/${entry.download.package}`]?.integrity !== release.dist.integrity) fail('Pacote instalado nao corresponde ao registro.');
441
+ prepareExecutables(stage, entry, this.target);
442
+ }
417
443
  execute(stage, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, local, runtime), runtime);
418
444
  this.storage(name); storagePath(this.root, target, 'directory');
419
- const info = { name, package: entry.download.package, version: release.version, platform: this.target, node: runtime.version, nodePath: runtime.file, source: entry.source, tarball: release.dist.tarball, integrity: release.dist.integrity, checkedAt: new Date().toISOString() };
445
+ const info = { name, ...(receipt ? { provider: 'github-release', binarySha256: receipt.binarySha256, archiveSha256: receipt.archiveSha256 } : { package: entry.download.package }), version: release.version, platform: this.target, node: runtime.version, nodePath: runtime.file, source: entry.source, tarball: receipt ? release.url : release.dist.tarball, integrity: receipt ? `sha256-${Buffer.from(receipt.archiveSha256, 'hex').toString('base64')}` : release.dist.integrity, checkedAt: new Date().toISOString() };
420
446
  writeFileSync(storagePath(this.root, temporary, 'file'), `${JSON.stringify(info, null, 2)}\n`, { flag: 'wx' });
421
447
  this.launchers(name);
422
448
  if (existsSync(target)) { previous = storagePath(this.root, `${target}.previous-${randomUUID()}`, 'directory'); renameSync(storagePath(this.root, target, 'directory'), previous); }
@@ -425,7 +451,11 @@ export class ToolManager {
425
451
  return { ...info, layout: 'local', prefix: target, changed: true };
426
452
  } finally { rmSync(storagePath(this.root, stage, 'directory'), { recursive: true, force: true }); rmSync(storagePath(this.root, temporary, 'file'), { force: true }); }
427
453
  }
428
- run(name, args) { const entry = this.entry(name); const runtime = this.runtime(name); return execute(this.current(name).prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime); }
454
+ run(name, args) {
455
+ const entry = this.entry(name), runtime = this.runtime(name), current = this.current(name);
456
+ if (entry.download.provider === 'github-release' && checkNative(current.prefix, entry, this.target).binarySha256 !== current.binarySha256) fail(`Recibo local divergente: ${name}`);
457
+ return execute(current.prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime);
458
+ }
429
459
  }
430
460
 
431
461
  const help = 'Uso: setup [all|grupo|nome]; [install|update|check|info] [base|all|grupo|nome]; list; unlock <nome>; run <nome> [argumentos]; --tool <nome> <acao> [argumentos].';
@@ -8,6 +8,7 @@ import { hostname } from 'node:os';
8
8
  import { ToolManager, platformKey, validateRelease, validateToolConfig, loadCatalog, main, toolEnvironment, binaryInvocation, toolActions, toolWrapper } from '../SCRIPT.mjs';
9
9
  import { requireNode } from '../../lib/runtime.mjs';
10
10
  import './bootstrap.test.mjs';
11
+ import './native-release.test.mjs';
11
12
 
12
13
  const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
13
14
  const tool = (name = 'test-tool', group = 'base') => ({ name, description: 'Ferramenta da fixture.', group, source: `https://github.com/owner/${name}`, download: { provider: 'npm', package: name, registry: 'https://registry.npmjs.org/', repository: `owner/${name}`, channel: 'latest' }, platforms: [platformKey()], scripts: { entry: 'SCRIPT.mjs' }, probe: ['--version'] });
@@ -44,6 +45,7 @@ function fixture(t, catalog = { 'test-tool': tool() }, options = {}) {
44
45
  copyFileSync(join(project, '.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs'), join(common, 'SCRIPT.mjs'));
45
46
  mkdirSync(join(root, '.pbai4devs/scripts/lib'), { recursive: true });
46
47
  copyFileSync(join(project, '.pbai4devs/scripts/lib/runtime.mjs'), join(root, '.pbai4devs/scripts/lib/runtime.mjs'));
48
+ copyFileSync(join(project, '.pbai4devs/scripts/lib/native-release.mjs'), join(root, '.pbai4devs/scripts/lib/native-release.mjs'));
47
49
  const state = { version: '1.0.0', installs: 0, fetches: 0, fail: false, tamper: false, broken: false, noBin: false };
48
50
  const manager = new ToolManager({ root, catalog, fetchRelease: async name => { state.fetches++; return release(state.version, name); }, install(prefix, name, version) { state.installs++; if (state.fail) throw new Error('download de prova falhou'); packageFiles(prefix, name, version, state); }, ...options });
49
51
  return { root, manager, state, local: join(root, '.pbai4devs/tools/test-tool/.local') };