@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.
- package/.pbai4devs/COMPATIBILIDADE.md +6 -0
- package/.pbai4devs/INSTRUCTIONS.md +3 -0
- package/.pbai4devs/agents/README.md +19 -0
- package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
- package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
- package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
- package/.pbai4devs/agents/documentador/AGENT.md +19 -0
- package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/documentador/claude.yaml +5 -0
- package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
- package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
- package/.pbai4devs/agents/documentador/github.yaml +5 -0
- package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
- package/.pbai4devs/agents/implementador/AGENT.md +20 -0
- package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/implementador/claude.yaml +5 -0
- package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
- package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
- package/.pbai4devs/agents/implementador/github.yaml +5 -0
- package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
- package/.pbai4devs/agents/planejador/AGENT.md +19 -0
- package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/planejador/claude.yaml +5 -0
- package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
- package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
- package/.pbai4devs/agents/planejador/github.yaml +5 -0
- package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
- package/.pbai4devs/agents/validador/AGENT.md +19 -0
- package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/validador/claude.yaml +5 -0
- package/.pbai4devs/agents/validador/cursor.yaml +5 -0
- package/.pbai4devs/agents/validador/gemini.yaml +6 -0
- package/.pbai4devs/agents/validador/github.yaml +5 -0
- package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/validador/opencode.yaml +11 -0
- package/.pbai4devs/commands/README.md +13 -0
- package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
- package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
- package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
- package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
- package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
- package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
- package/.pbai4devs/external-skills/CATALOG.json +39 -1
- package/.pbai4devs/external-skills/README.md +13 -0
- package/.pbai4devs/rules/README.md +4 -0
- package/.pbai4devs/rules/documentacao/RULE.md +2 -0
- package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
- package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/seguranca/RULE.md +19 -0
- package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
- package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
- package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
- package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
- package/.pbai4devs/scripts/lib/README.md +4 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
- package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
- package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
- package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
- package/.pbai4devs/skills/README.md +10 -2
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
- package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
- package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
- package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
- package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
- package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
- package/.pbai4devs/templates/README.md +2 -0
- package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
- package/.pbai4devs/tools/README.md +11 -3
- package/.pbai4devs/tools/gitleaks/README.md +22 -0
- package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
- package/.pbai4devs/tools/opengrep/README.md +22 -0
- package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
- package/.pbai4devs/tools/osv-scanner/README.md +23 -0
- package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
- package/CHANGELOG.md +17 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +8 -3
- package/docs/README.md +6 -3
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +25 -8
- package/docs/guides/resources.md +48 -1
- package/docs/guides/security-references.md +97 -0
- package/docs/guides/security.md +32 -0
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +12 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +5 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +4 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/docs/specs/security-ecosystem/README.md +9 -0
- package/docs/specs/security-ecosystem/SPEC.md +53 -0
- package/package.json +1 -1
- 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
|
-
|
|
58
|
-
|
|
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 (
|
|
33
|
-
if (
|
|
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
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
if (
|
|
416
|
-
|
|
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) {
|
|
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') };
|