@pathbit/agentdevkit 0.0.0-stage → 0.0.6
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 +109 -0
- package/.pbai4devs/INSTRUCTIONS.md +87 -0
- package/.pbai4devs/README.md +53 -0
- package/.pbai4devs/agents/README.md +18 -0
- package/.pbai4devs/agents/revisor/AGENT.md +16 -0
- package/.pbai4devs/agents/revisor/antigravity.yaml +8 -0
- package/.pbai4devs/agents/revisor/claude.yaml +5 -0
- package/.pbai4devs/agents/revisor/cursor.yaml +5 -0
- package/.pbai4devs/agents/revisor/gemini.yaml +6 -0
- package/.pbai4devs/agents/revisor/github.yaml +5 -0
- package/.pbai4devs/agents/revisor/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/revisor/mimocode.yaml +10 -0
- package/.pbai4devs/agents/revisor/opencode.yaml +7 -0
- package/.pbai4devs/agents/revisor/zcode.yaml +5 -0
- package/.pbai4devs/commands/README.md +30 -0
- package/.pbai4devs/commands/atualizar-ferramentas/COMMAND.md +17 -0
- package/.pbai4devs/commands/diagnosticar/COMMAND.md +18 -0
- package/.pbai4devs/commands/instalar-ferramentas/COMMAND.md +21 -0
- package/.pbai4devs/commands/preparar-dev/COMMAND.md +14 -0
- package/.pbai4devs/commands/proximos-passos/COMMAND.md +9 -0
- package/.pbai4devs/commands/revisar-entrega/COMMAND.md +29 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +20 -0
- package/.pbai4devs/commands/status-roadmap/COMMAND.md +12 -0
- package/.pbai4devs/commands/testar-navegador/COMMAND.md +16 -0
- package/.pbai4devs/commands/verificar-agentes/COMMAND.md +19 -0
- package/.pbai4devs/estado/README.md +12 -0
- package/.pbai4devs/external-skills/CATALOG.json +121 -0
- package/.pbai4devs/external-skills/README.md +48 -0
- package/.pbai4devs/rules/README.md +18 -0
- package/.pbai4devs/rules/ambiente-local/RULE.md +33 -0
- package/.pbai4devs/rules/ambiente-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/documentacao/RULE.md +11 -0
- package/.pbai4devs/rules/documentacao/cursor.yaml +4 -0
- package/.pbai4devs/rules/estrutura/RULE.md +33 -0
- package/.pbai4devs/rules/estrutura/cursor.yaml +4 -0
- package/.pbai4devs/rules/git/RULE.md +18 -0
- package/.pbai4devs/rules/git/cursor.yaml +4 -0
- package/.pbai4devs/rules/proximos-passos/RULE.md +14 -0
- package/.pbai4devs/rules/qualidade/RULE.md +32 -0
- package/.pbai4devs/rules/qualidade/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +69 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs +41 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +30 -0
- package/.pbai4devs/scripts/executar-script/README.md +28 -0
- package/.pbai4devs/scripts/executar-script/SCRIPT.mjs +78 -0
- package/.pbai4devs/scripts/executar-script/tests/TEST.mjs +122 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/README.md +31 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs +182 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs +134 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +190 -0
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +161 -0
- package/.pbai4devs/scripts/lib/README.md +9 -0
- package/.pbai4devs/scripts/lib/adapters.mjs +135 -0
- package/.pbai4devs/scripts/lib/catalog.mjs +170 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +348 -0
- package/.pbai4devs/scripts/lib/npm.mjs +17 -0
- package/.pbai4devs/scripts/lib/resource-path.mjs +20 -0
- package/.pbai4devs/scripts/lib/runtime.mjs +28 -0
- package/.pbai4devs/scripts/lib/script-config.mjs +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md +34 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs +189 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +71 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +455 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/FIXTURE.html +20 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_BROWSER.mjs +55 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_DESKTOP.mjs +61 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_VERCEL.mjs +192 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +552 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/bootstrap.test.mjs +94 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash/input.txt +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash-overlay/input.txt +1 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/skills/agentdevkit-smoke/SKILL.md +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/index.html +6 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/webreel.config.json +10 -0
- package/.pbai4devs/scripts/revisar-entrega/README.md +36 -0
- package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +249 -0
- package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +249 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +41 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +473 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +290 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +869 -0
- package/.pbai4devs/scripts/testar-infraestrutura/SCRIPT.mjs +39 -0
- package/.pbai4devs/scripts/testar-infraestrutura/tests/TEST.mjs +29 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +54 -0
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +477 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.mjs +1 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.test.mjs.template +8 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/http-api.test.mjs.template +28 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/server.mjs +34 -0
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +305 -0
- package/.pbai4devs/scripts/testar-navegador/REFERENCE.md +49 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.conf +1 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.js +64 -0
- package/.pbai4devs/scripts/testar-navegador/tests/FAIL.js +8 -0
- package/.pbai4devs/scripts/testar-navegador/tests/SUCCESS.js +13 -0
- package/.pbai4devs/scripts/verificar-recursos/README.md +25 -0
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +209 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +310 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/ambiente-local.mjs +80 -0
- package/.pbai4devs/skills/README.md +24 -0
- package/.pbai4devs/skills/git/git-branch/SKILL.md +11 -0
- package/.pbai4devs/skills/git/git-commit/SKILL.md +16 -0
- package/.pbai4devs/skills/git/git-diff/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-fetch/SKILL.md +10 -0
- package/.pbai4devs/skills/git/git-log/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-merge/SKILL.md +13 -0
- package/.pbai4devs/skills/git/git-pull/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-push/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-status/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-worktree/SKILL.md +10 -0
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +37 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +35 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +34 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +26 -0
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +62 -0
- package/.pbai4devs/skills/projeto/criar-agente/SKILL.md +20 -0
- package/.pbai4devs/skills/projeto/criar-comando/SKILL.md +25 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +41 -0
- package/.pbai4devs/skills/projeto/criar-regra/SKILL.md +19 -0
- package/.pbai4devs/skills/projeto/criar-skill/SKILL.md +22 -0
- package/.pbai4devs/skills/projeto/executar-script/SKILL.md +29 -0
- package/.pbai4devs/skills/projeto/preparar-dev/SKILL.md +45 -0
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +56 -0
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +23 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +42 -0
- package/.pbai4devs/skills/projeto/usar-skills-externas/SKILL.md +28 -0
- package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +45 -0
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +33 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +26 -0
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +48 -0
- package/.pbai4devs/templates/README.md +65 -0
- package/.pbai4devs/templates/adr/TEMPLATE.md +21 -0
- package/.pbai4devs/templates/ambiente-local/Makefile.template +48 -0
- package/.pbai4devs/templates/ambiente-local/TEMPLATE.md +48 -0
- package/.pbai4devs/templates/ambiente-local/compose.local.yml.template +16 -0
- package/.pbai4devs/templates/ambiente-local/dockerignore.template +28 -0
- package/.pbai4devs/templates/changelog/TEMPLATE.md +17 -0
- package/.pbai4devs/templates/claude/TEMPLATE.md +8 -0
- package/.pbai4devs/templates/clients/ADAPTERS.json +306 -0
- package/.pbai4devs/templates/clients/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/comando/TEMPLATE.md +15 -0
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +46 -0
- package/.pbai4devs/templates/configuracao-raiz/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/configuracao-raiz/editorconfig.template +21 -0
- package/.pbai4devs/templates/configuracao-raiz/env.example.template +3 -0
- package/.pbai4devs/templates/configuracao-raiz/gitattributes.template +6 -0
- package/.pbai4devs/templates/contribuicao/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/design/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/entrypoints/AGENTS.md +19 -0
- package/.pbai4devs/templates/entrypoints/CLAUDE.md +7 -0
- package/.pbai4devs/templates/entrypoints/GEMINI.md +7 -0
- package/.pbai4devs/templates/entrypoints/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +42 -0
- package/.pbai4devs/templates/gitignore/agents.ignore +45 -0
- package/.pbai4devs/templates/gitignore/clients.json +20 -0
- package/.pbai4devs/templates/gitignore/local.ignore +10 -0
- package/.pbai4devs/templates/gitignore/shared.ignore +60 -0
- package/.pbai4devs/templates/instrucoes-agentes/TEMPLATE.md +25 -0
- package/.pbai4devs/templates/plano-implementacao/TEMPLATE.md +27 -0
- package/.pbai4devs/templates/prd/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/readme-pasta/TEMPLATE.md +16 -0
- package/.pbai4devs/templates/readme-projeto/TEMPLATE.md +34 -0
- package/.pbai4devs/templates/release/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/revisao-entrega/TEMPLATE.md +41 -0
- package/.pbai4devs/templates/roadmap/TEMPLATE.md +18 -0
- package/.pbai4devs/templates/seguranca/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/spec/TEMPLATE.md +61 -0
- package/.pbai4devs/templates/tasks/TEMPLATE.md +20 -0
- package/.pbai4devs/templates/ux-ui/TEMPLATE.md +26 -0
- package/.pbai4devs/tools/README.md +87 -0
- package/.pbai4devs/tools/agent-browser/README.md +26 -0
- package/.pbai4devs/tools/agent-browser/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-browser/TOOL.json +33 -0
- package/.pbai4devs/tools/agent-desktop/README.md +26 -0
- package/.pbai4devs/tools/agent-desktop/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-desktop/TOOL.json +30 -0
- package/.pbai4devs/tools/just-bash/README.md +23 -0
- package/.pbai4devs/tools/just-bash/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/just-bash/TOOL.json +27 -0
- package/.pbai4devs/tools/markdownlint-cli2/README.md +18 -0
- package/.pbai4devs/tools/markdownlint-cli2/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/markdownlint-cli2/TOOL.json +28 -0
- package/.pbai4devs/tools/opensrc/README.md +25 -0
- package/.pbai4devs/tools/opensrc/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opensrc/TOOL.json +29 -0
- package/.pbai4devs/tools/portless/README.md +21 -0
- package/.pbai4devs/tools/portless/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/portless/TOOL.json +30 -0
- package/.pbai4devs/tools/skills/README.md +24 -0
- package/.pbai4devs/tools/skills/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/skills/TOOL.json +33 -0
- package/.pbai4devs/tools/webreel/README.md +23 -0
- package/.pbai4devs/tools/webreel/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/webreel/TOOL.json +25 -0
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.es.md +91 -0
- package/README.md +94 -2
- package/bin/README.md +8 -0
- package/bin/agentdevkit.mjs +35 -0
- package/docs/README.md +81 -0
- package/docs/guides/README.md +14 -0
- package/docs/guides/cli.md +81 -0
- package/docs/guides/installation.md +86 -0
- package/docs/guides/resources.md +121 -0
- package/docs/guides/tools-and-testing.md +85 -0
- package/docs/guides/troubleshooting.md +151 -0
- package/docs/guides/workflow.md +130 -0
- package/docs/maintainers/README.md +12 -0
- package/docs/maintainers/community.md +49 -0
- package/docs/maintainers/development.md +54 -0
- package/docs/maintainers/maintenance.md +104 -0
- package/docs/maintainers/packages.md +87 -0
- package/docs/product/README.md +12 -0
- package/docs/product/design.md +104 -0
- package/docs/product/prd.md +70 -0
- package/docs/product/roadmap.md +38 -0
- package/docs/product/ux-ui.md +42 -0
- package/docs/specs/README.md +21 -0
- package/docs/specs/github-packages/SPEC.md +45 -0
- package/docs/specs/v0.0.1/README.md +10 -0
- package/docs/specs/v0.0.1/SPEC.md +57 -0
- package/docs/specs/v0.0.2/README.md +10 -0
- package/docs/specs/v0.0.2/SPEC.md +67 -0
- package/docs/specs/v0.0.3/README.md +10 -0
- package/docs/specs/v0.0.3/SPEC.md +64 -0
- package/docs/specs/v0.0.4/README.md +10 -0
- package/docs/specs/v0.0.4/SPEC.md +53 -0
- package/docs/specs/v0.0.5/README.md +9 -0
- package/docs/specs/v0.0.5/SPEC.md +42 -0
- package/docs/specs/v0.0.6/README.md +10 -0
- package/docs/specs/v0.0.6/SPEC.md +77 -0
- package/package.json +67 -4
- package/src/README.md +16 -0
- package/src/adapters/index.mjs +1 -0
- package/src/cli.mjs +162 -0
- package/src/config.mjs +14 -0
- package/src/i18n.mjs +42 -0
- package/src/install.mjs +373 -0
- package/src/next.mjs +79 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Índices derivados das fontes versionadas; não lê instalações ou caches locais.
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, posix, resolve } from 'node:path';
|
|
4
|
+
import { loadCatalog as loadTools, toolActions } from '../preparar-ferramentas/SCRIPT.mjs';
|
|
5
|
+
import { validateCatalog as validateExternalCatalog } from '../gerenciar-skills-externas/SCRIPT.mjs';
|
|
6
|
+
import { requireNode } from './runtime.mjs';
|
|
7
|
+
import { isGeneratedOutput } from './gitignore.mjs';
|
|
8
|
+
import { parseScriptConfig } from './script-config.mjs';
|
|
9
|
+
|
|
10
|
+
const kinds = { skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md', templates: 'TEMPLATE.md' };
|
|
11
|
+
const excluded = new Set(['.git', 'node_modules', '.local', '.cache', 'opencode-loop']);
|
|
12
|
+
const generated = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']);
|
|
13
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
14
|
+
const sorted = values => [...new Set(values)].sort(compare);
|
|
15
|
+
const fail = message => { throw new Error(message); };
|
|
16
|
+
const stat = file => { try { return lstatSync(file); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; } };
|
|
17
|
+
|
|
18
|
+
function scanner(root) {
|
|
19
|
+
const absolute = path => join(root, ...path.split('/'));
|
|
20
|
+
function safe(path, required = false) {
|
|
21
|
+
if (typeof path !== 'string' || !path || /^[A-Za-z]:/.test(path) || /[|\\\x00-\x1f\x7f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho de catalogo invalido: ${path}`);
|
|
22
|
+
let current = root;
|
|
23
|
+
for (const part of path.split('/')) {
|
|
24
|
+
if (stat(current)?.isDirectory() && readdirSync(current).some(name => name.toLowerCase() === part.toLowerCase() && name !== part)) fail(`Caminhos diferem somente por maiusculas/minusculas: ${path}`);
|
|
25
|
+
current = join(current, part);
|
|
26
|
+
if (stat(current)?.isSymbolicLink()) fail(`Link simbolico no catalogo: ${path}`);
|
|
27
|
+
}
|
|
28
|
+
if (required && !stat(current)) fail(`Referencia ausente no catalogo: ${path}`);
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
function walk(path) {
|
|
32
|
+
const info = stat(safe(path));
|
|
33
|
+
if (!info) return [];
|
|
34
|
+
if (!info.isDirectory()) fail(`Pasta de catalogo invalida: ${path}`);
|
|
35
|
+
const result = [];
|
|
36
|
+
for (const name of readdirSync(absolute(path)).sort(compare)) {
|
|
37
|
+
const child = `${path}/${name}`, item = stat(safe(child));
|
|
38
|
+
if (excluded.has(name.toLowerCase()) || isGeneratedOutput(child, item.isDirectory()) || (path.startsWith('.pbai4devs/tools/') && ['versions', 'bin'].includes(name))) continue;
|
|
39
|
+
if (item.isDirectory()) result.push(...walk(child));
|
|
40
|
+
else if (item.isFile()) result.push(child);
|
|
41
|
+
else fail(`Tipo de arquivo nao suportado no catalogo: ${child}`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
const read = path => readFileSync(safe(path, true), 'utf8').replace(/\r\n?/g, '\n');
|
|
46
|
+
const json = path => { try { return JSON.parse(read(path)); } catch (error) { fail(`JSON invalido: ${path}: ${error.message}`); } };
|
|
47
|
+
return { absolute, safe, walk, read, json };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function metadata(text, path, required = true, fields = ['name', 'description']) {
|
|
51
|
+
const lines = text.split('\n');
|
|
52
|
+
if (lines[0] !== '---') { if (required) fail(`Frontmatter YAML ausente: ${path}`); return {}; }
|
|
53
|
+
const end = lines.indexOf('---', 1);
|
|
54
|
+
if (end < 0) fail(`Frontmatter YAML sem fechamento: ${path}`);
|
|
55
|
+
const result = {};
|
|
56
|
+
for (let i = 1; i < end; i++) {
|
|
57
|
+
const match = lines[i].match(/^([A-Za-z][\w-]*):\s*(.*)$/);
|
|
58
|
+
if (!match || !fields.includes(match[1])) continue;
|
|
59
|
+
const key = match[1]; let value = match[2].trim();
|
|
60
|
+
if (Object.hasOwn(result, key)) fail(`Metadado duplicado ${key}: ${path}`);
|
|
61
|
+
if (/^[>|][-+]?$/.test(value)) {
|
|
62
|
+
const block = [];
|
|
63
|
+
while (i + 1 < end && (/^\s/.test(lines[i + 1]) || !lines[i + 1])) block.push(lines[++i].trim());
|
|
64
|
+
value = block.join(' ');
|
|
65
|
+
} else if (value.startsWith('"')) {
|
|
66
|
+
try { value = JSON.parse(value); } catch { fail(`Metadado invalido ${key}: ${path}`); }
|
|
67
|
+
} else if (value.startsWith("'")) {
|
|
68
|
+
if (!value.endsWith("'")) fail(`Metadado invalido ${key}: ${path}`);
|
|
69
|
+
value = value.slice(1, -1).replace(/''/g, "'");
|
|
70
|
+
} else if (/^[\[{]|^(?:null|true|false)$/.test(value)) fail(`Metadado deve ser texto ${key}: ${path}`);
|
|
71
|
+
if (typeof value !== 'string' || !value.trim()) fail(`Metadado vazio ${key}: ${path}`);
|
|
72
|
+
result[key] = value.replace(/\s+/g, ' ').trim();
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ruleScope(meta, path) {
|
|
78
|
+
if (!['always_on', 'model_decision', 'glob', 'manual'].includes(meta.trigger)) fail(`Trigger de regra ausente ou invalido: ${path}`);
|
|
79
|
+
if (meta.globs && meta.glob && meta.globs !== meta.glob) fail(`Glob e globs conflitantes: ${path}`);
|
|
80
|
+
const globs = meta.globs || meta.glob;
|
|
81
|
+
if (meta.trigger === 'glob' && !globs) fail(`Regra glob exige globs: ${path}`);
|
|
82
|
+
return { trigger: meta.trigger, ...(globs ? { globs } : {}) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function localLinks(text, path, scan) {
|
|
86
|
+
const clean = text.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1\s*$/gm, '');
|
|
87
|
+
const raw = [...clean.matchAll(/\]\((<[^>]+>|[^\s)]+)(?:\s+["'][^\n]*?["'])?\)/g)].map(match => match[1]);
|
|
88
|
+
raw.push(...[...clean.matchAll(/^\s*\[[^\]]+\]:\s*(<[^>]+>|\S+)/gm)].map(match => match[1]));
|
|
89
|
+
const references = [];
|
|
90
|
+
for (let href of raw) {
|
|
91
|
+
href = href.replace(/^<|>$/g, '');
|
|
92
|
+
if (!href || href.startsWith('#') || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href)) continue;
|
|
93
|
+
let location;
|
|
94
|
+
try { location = decodeURIComponent(href.split(/[?#]/)[0]); } catch { fail(`Link invalido em ${path}: ${href}`); }
|
|
95
|
+
if (!location) continue;
|
|
96
|
+
if (location.startsWith('/') || location.includes('\\')) fail(`Link deve ser relativo em ${path}: ${href}`);
|
|
97
|
+
const target = posix.normalize(posix.join(posix.dirname(path), location)).replace(/\/+$/, '');
|
|
98
|
+
if (!generated.has(target)) scan.safe(target, true);
|
|
99
|
+
references.push(target);
|
|
100
|
+
}
|
|
101
|
+
return sorted(references);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function registry(paths) {
|
|
105
|
+
const values = sorted(paths);
|
|
106
|
+
return '{\n "entries": [\n' + values.map((path, index) => ` {"path": ${JSON.stringify(path)}}${index < values.length - 1 ? ',' : ''}\n`).join('') + ' ]\n}\n';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildCatalog(root) {
|
|
110
|
+
requireNode(); root = realpathSync(resolve(root));
|
|
111
|
+
const scan = scanner(root);
|
|
112
|
+
const catalog = { schemaVersion: 1, skills: [], agents: [], commands: [], rules: [], templates: [], tools: [], scripts: [], externalSkills: [] };
|
|
113
|
+
for (const [kind, principal] of Object.entries(kinds)) {
|
|
114
|
+
const files = scan.walk(`.pbai4devs/${kind}`);
|
|
115
|
+
const mains = files.filter(path => posix.basename(path) === principal);
|
|
116
|
+
for (const path of mains) {
|
|
117
|
+
const folder = posix.dirname(path);
|
|
118
|
+
const text = scan.read(path), meta = metadata(text, path, true, kind === 'rules' ? ['name', 'description', 'trigger', 'globs', 'glob'] : undefined);
|
|
119
|
+
const title = text.match(/^# (.+)$/m)?.[1];
|
|
120
|
+
if (!title) fail(`Titulo ausente: ${path}`);
|
|
121
|
+
if (!meta.description) fail(`Descricao ausente: ${path}`);
|
|
122
|
+
const entry = { id: folder.slice(`.pbai4devs/${kind}/`.length), name: meta.name || posix.basename(folder), description: meta.description, path, assets: files.filter(file => file !== path && file.startsWith(folder + '/')) };
|
|
123
|
+
if (kind === 'rules') Object.assign(entry, ruleScope(meta, path));
|
|
124
|
+
if (kind === 'commands') entry.references = localLinks(text, path, scan);
|
|
125
|
+
catalog[kind].push(entry);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const tools = loadTools(root);
|
|
129
|
+
for (const name of Object.keys(tools).sort(compare)) {
|
|
130
|
+
const config = tools[name];
|
|
131
|
+
const path = `.pbai4devs/tools/${name}/TOOL.json`;
|
|
132
|
+
const entryPath = `.pbai4devs/tools/${name}/${config.scripts.entry}`;
|
|
133
|
+
if (!stat(scan.safe(entryPath, true))?.isFile()) fail(`Script de ferramenta nao e arquivo: ${entryPath}`);
|
|
134
|
+
const folder = posix.dirname(path);
|
|
135
|
+
const assets = scan.walk(folder).filter(file => file !== path && !file.slice(folder.length + 1).match(/^(?:versions|bin)\/|^(?:CURRENT\.json|package-lock\.json)$/));
|
|
136
|
+
catalog.tools.push({ id: name, name: config.name, description: config.description, path, assets, group: config.group, entry: entryPath, actions: toolActions(name, config) });
|
|
137
|
+
}
|
|
138
|
+
const scriptFiles = scan.walk('.pbai4devs/scripts');
|
|
139
|
+
const sharedAssets = scriptFiles.filter(path => path.startsWith('.pbai4devs/scripts/lib/'));
|
|
140
|
+
const directories = sorted(scriptFiles.filter(path => /^\.pbai4devs\/scripts\/[^/]+\/SCRIPT\.(?:mjs|cjs|js|conf)$/.test(path)).map(path => posix.dirname(path)));
|
|
141
|
+
for (const folder of directories) {
|
|
142
|
+
const name = posix.basename(folder);
|
|
143
|
+
const config = `${folder}/SCRIPT.conf`;
|
|
144
|
+
const configInfo = stat(scan.safe(config)); const hasConfig = Boolean(configInfo);
|
|
145
|
+
if (hasConfig && !configInfo.isFile()) fail(`SCRIPT.conf deve ser um arquivo: ${config}`);
|
|
146
|
+
const executor = hasConfig ? parseScriptConfig(scan.read(config)) : 'node';
|
|
147
|
+
const path = executor === 'playwright-mcp' ? `${folder}/SCRIPT.js`
|
|
148
|
+
: ['mjs', 'cjs', 'js'].map(extension => `${folder}/SCRIPT.${extension}`).find(candidate => stat(scan.safe(candidate))) || `${folder}/SCRIPT.mjs`;
|
|
149
|
+
if (!stat(scan.safe(path, true))?.isFile()) fail(`Entrada de script nao e arquivo: ${path}`);
|
|
150
|
+
const readme = `${folder}/README.md`;
|
|
151
|
+
const meta = stat(scan.safe(readme))?.isFile() ? metadata(scan.read(readme), readme, false) : {};
|
|
152
|
+
const assets = sorted([...scriptFiles.filter(file => file !== path && file.startsWith(folder + '/')), ...sharedAssets]);
|
|
153
|
+
catalog.scripts.push({ id: name, name, description: meta.description || `Automação ${name}.`, path, assets, support: { modules: sharedAssets.filter(file => file.endsWith('.mjs')), tests: assets.filter(file => /\/tests\//.test(file) && !/\/SMOKE[^/]*\.mjs$/.test(file)), smokes: assets.filter(file => /\/SMOKE[^/]*\.mjs$/.test(file)) }, executor, argv: executor === 'node' ? ['node', path] : [], ...(hasConfig ? { configuration: config } : {}) });
|
|
154
|
+
}
|
|
155
|
+
const externalFile = '.pbai4devs/external-skills/CATALOG.json';
|
|
156
|
+
if (stat(scan.safe(externalFile))) {
|
|
157
|
+
const external = validateExternalCatalog(scan.json(externalFile));
|
|
158
|
+
for (const item of external.skills) {
|
|
159
|
+
const manager = '.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs';
|
|
160
|
+
const actions = stat(scan.safe(manager))?.isFile() ? Object.fromEntries(['discover', 'install', 'check', 'update', 'read'].map(action => [action, { executor: 'node', argv: ['node', manager, action, item.id] }])) : null;
|
|
161
|
+
catalog.externalSkills.push({ id: item.id, name: item.name, description: item.use, path: externalFile, assets: [], repository: item.repository, commit: item.commit, upstreamPath: item.path, license: item.license, status: item.status, ...(actions ? { actions } : {}), ...(item.notes ? { notes: item.notes } : {}), ...(item.licenseSource ? { licenseSource: item.licenseSource } : {}) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const kind of Object.keys(catalog).filter(key => Array.isArray(catalog[key]))) catalog[kind].sort((a, b) => compare(a.id, b.id));
|
|
165
|
+
const outputs = new Map([['.pbai4devs/catalog.json', JSON.stringify(catalog, null, 2) + '\n']]);
|
|
166
|
+
outputs.set('.pbai4devs/skills.json', registry(catalog.skills.map(entry => posix.dirname(entry.path))));
|
|
167
|
+
outputs.set('.pbai4devs/rules.json', registry(catalog.rules.map(entry => entry.path)));
|
|
168
|
+
outputs.set('.pbai4devs/agents.json', registry(catalog.agents.map(entry => entry.path)));
|
|
169
|
+
return { catalog, outputs };
|
|
170
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
// Atualiza apenas o bloco reservado; regras externas permanecem byte a byte.
|
|
8
|
+
export const ignoreBegin = '# BEGIN AGENT IGNORE';
|
|
9
|
+
export const ignoreEnd = '# END AGENT IGNORE';
|
|
10
|
+
|
|
11
|
+
const outputDirectories = new Set(['.git', '.agent-tests', '.cache', 'node_modules', '__pycache__', 'opencode-loop', 'sessions', 'logs', 'testresults', 'test-results', 'blob-report', 'tmp', 'temp']);
|
|
12
|
+
const localFiles = new Set(['auth.json', 'credentials.json', 'history.jsonl', '.ds_store', 'thumbs.db', 'desktop.ini']);
|
|
13
|
+
|
|
14
|
+
// Mesmos nomes de saídas locais ignoradas; não exclui fixtures/cache nem arquivos .template.
|
|
15
|
+
export function isGeneratedOutput(path, directory = false) {
|
|
16
|
+
const parts = path.replaceAll('\\', '/').split('/');
|
|
17
|
+
return parts.some((part, index) => {
|
|
18
|
+
const name = part.toLowerCase();
|
|
19
|
+
const folder = index < parts.length - 1 || directory;
|
|
20
|
+
const localFile = localFiles.has(name) || /\.(?:log|swp|swo)$|~$|^\._/.test(name) || /\.local\.(?:jsonc?|toml|ya?ml)$/.test(name);
|
|
21
|
+
const secretEnv = /^\.env(?:\.|$)/.test(name) && !/^\.env\.(?:example|sample|template|.*\.example)$/.test(part);
|
|
22
|
+
return /^\.playwright/.test(name) || (folder && (outputDirectories.has(name) || name.startsWith('playwright'))) || (!folder && (localFile || secretEnv));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateIgnoreClients(value) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('clients.json deve conter um objeto.');
|
|
28
|
+
const names = new Set();
|
|
29
|
+
for (const [name, settings] of Object.entries(value)) {
|
|
30
|
+
if (!/^\.[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || ['.git', '.pbai4devs', '.agent-tests'].includes(name.toLowerCase()) || names.has(name.toLowerCase())) throw new Error(`Cliente .gitignore invalido: ${name}`);
|
|
31
|
+
names.add(name.toLowerCase());
|
|
32
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings) || Object.keys(settings).some(key => !['resources', 'allow'].includes(key))) throw new Error(`Configuracao .gitignore invalida: ${name}`);
|
|
33
|
+
if (settings.resources !== undefined && typeof settings.resources !== 'boolean') throw new Error(`resources deve ser booleano: ${name}`);
|
|
34
|
+
const allow = settings.allow === undefined ? [] : settings.allow;
|
|
35
|
+
if (!Array.isArray(allow) || new Set(allow).size !== allow.length) throw new Error(`allow deve ser uma lista sem duplicados: ${name}`);
|
|
36
|
+
for (const path of allow) {
|
|
37
|
+
if (typeof path !== 'string' || !path || /[\\:*?\[\]!#<>|\x00-\x1f\x7f]/.test(path) || path.trim() !== path || path.replace(/\/$/, '').split('/').some(part => !part || part === '.' || part === '..')) throw new Error(`Caminho allow deve ser relativo e literal: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderClientIgnore(template, settings = {}) {
|
|
44
|
+
const token = '{{ALLOWLIST}}';
|
|
45
|
+
if (!template.includes(token)) return template;
|
|
46
|
+
if (template.split(token).length !== 2) throw new Error('agents.ignore exige um unico {{ALLOWLIST}}.');
|
|
47
|
+
const allowed = [...(settings.resources === false ? [] : ['skills/', 'agents/', 'commands/', 'rules/']), ...(settings.allow ?? [])];
|
|
48
|
+
const directories = allowed.filter(path => path.endsWith('/'));
|
|
49
|
+
const patterns = new Set();
|
|
50
|
+
for (const path of allowed) {
|
|
51
|
+
const directory = path.endsWith('/');
|
|
52
|
+
const parts = path.replace(/\/$/, '').split('/');
|
|
53
|
+
for (let end = 1; end < parts.length; end++) {
|
|
54
|
+
const ancestor = parts.slice(0, end).join('/') + '/';
|
|
55
|
+
patterns.add(`!/${ancestor}`);
|
|
56
|
+
if (!directories.some(value => ancestor.startsWith(value))) patterns.add(`/${ancestor}*`);
|
|
57
|
+
}
|
|
58
|
+
if (directory) patterns.add(`!/${parts.join('/')}/`);
|
|
59
|
+
else patterns.add(`!/${path}`);
|
|
60
|
+
}
|
|
61
|
+
return template.replace(token, [...patterns].join('\n'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const localStat = file => { try { return lstatSync(file); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; } };
|
|
65
|
+
const localRead = file => localStat(file) ? readFileSync(file, 'utf8') : null;
|
|
66
|
+
const portable = path => path.split(sep).join('/');
|
|
67
|
+
|
|
68
|
+
function canonicalExisting(path) {
|
|
69
|
+
if (typeof realpathSync.native === 'function') {
|
|
70
|
+
try { return realpathSync.native(path); }
|
|
71
|
+
catch (error) { if (!['ENOSYS', 'ENOTSUP', 'EINVAL'].includes(error.code)) throw error; }
|
|
72
|
+
}
|
|
73
|
+
return realpathSync(path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkedLocalPath(file) {
|
|
77
|
+
let current = resolve(file);
|
|
78
|
+
while (true) {
|
|
79
|
+
if (localStat(current)?.isSymbolicLink()) throw new Error(`Link simbolico em exclusao local: ${file}`);
|
|
80
|
+
const parent = dirname(current); if (parent === current) break; current = parent;
|
|
81
|
+
}
|
|
82
|
+
const item = localStat(file);
|
|
83
|
+
if (item && !item.isFile()) throw new Error(`Exclusao local exige arquivo regular: ${file}`);
|
|
84
|
+
return file;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function localBlock(current, first, last, body) {
|
|
88
|
+
const text = current ?? '';
|
|
89
|
+
const begins = [...text.matchAll(new RegExp(`^${first}\\r?$`, 'gm'))];
|
|
90
|
+
const ends = [...text.matchAll(new RegExp(`^${last}\\r?$`, 'gm'))];
|
|
91
|
+
if (begins.length !== ends.length || begins.length > 1 || begins.length && begins[0].index > ends[0].index) throw new Error('Bloco de exclusao local invalido.');
|
|
92
|
+
const block = body === null ? '' : `${first}\n${body.trimEnd()}\n${last}\n`;
|
|
93
|
+
if (!begins.length) return block ? text + (text && !text.endsWith('\n') ? '\n' : '') + block : text;
|
|
94
|
+
const start = begins[0].index;
|
|
95
|
+
const offset = ends[0].index + ends[0][0].length;
|
|
96
|
+
const finish = offset + (text[offset] === '\n' ? 1 : 0);
|
|
97
|
+
return text.slice(0, start) + block + text.slice(finish);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function gitCommand(root, args, input) {
|
|
101
|
+
const env = { ...process.env };
|
|
102
|
+
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
|
|
103
|
+
return spawnSync('git', [...(['ls-files', 'rm'].includes(args[0]) ? ['--literal-pathspecs'] : []), '-C', root, ...args], { input, encoding: 'utf8', env, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function checkedOutput(path) {
|
|
107
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || /[\\\x00-\x1f\x7f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) throw new Error(`Saida local invalida: ${path}`);
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function escapePattern(path) { return path.replace(/[\\!#*?\[\] ]/g, '\\$&'); }
|
|
112
|
+
|
|
113
|
+
function rootProbes(root, roots) {
|
|
114
|
+
return roots.filter(path => { const item = localStat(join(root, path)); return !item || item.isDirectory(); }).map(path => `${path}/.agentdevkit-ignore-probe`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function localRoot(value) {
|
|
118
|
+
const missing = []; let parent = resolve(value);
|
|
119
|
+
while (!localStat(parent)) { missing.unshift(parent.slice(dirname(parent).length + 1)); parent = dirname(parent); }
|
|
120
|
+
if (!localStat(parent).isDirectory() || localStat(parent).isSymbolicLink()) throw new Error('Raiz local exige diretorio real.');
|
|
121
|
+
const canonical = canonicalExisting(parent);
|
|
122
|
+
return { root: join(canonical, ...missing), parent: canonical };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Os padrões vêm do hub; apenas os nomes concretos dos derivados são calculados.
|
|
126
|
+
export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
127
|
+
const resolved = localRoot(root); root = resolved.root;
|
|
128
|
+
const localTemplate = join(root, '.pbai4devs/templates/gitignore/local.ignore');
|
|
129
|
+
const templateFile = checkedLocalPath(templateRoot ? join(templateRoot, '.pbai4devs/templates/gitignore/local.ignore') : localStat(localTemplate) ? localTemplate : fileURLToPath(new URL('../../templates/gitignore/local.ignore', import.meta.url)));
|
|
130
|
+
const template = readFileSync(templateFile, 'utf8');
|
|
131
|
+
if (template.split('{{OUTPUTS}}').length !== 2) throw new Error('local.ignore exige um unico {{OUTPUTS}}.');
|
|
132
|
+
if (template.split('{{ROOTS}}').length > 2) throw new Error('local.ignore permite um unico {{ROOTS}}.');
|
|
133
|
+
const localPolicy = join(templateRoot || root, '.pbai4devs/templates/clients/ADAPTERS.json');
|
|
134
|
+
const policy = JSON.parse(readFileSync(checkedLocalPath(localStat(localPolicy) ? localPolicy : fileURLToPath(new URL('../../templates/clients/ADAPTERS.json', import.meta.url))), 'utf8'));
|
|
135
|
+
const roots = [...new Set(policy.adapters.flatMap(adapter => [adapter.root, ...(adapter.extraRoots || [])]))].filter(path => !['.github', '.vscode'].includes(path));
|
|
136
|
+
if (roots.some(path => typeof path !== 'string' || !/^\.[a-z0-9][a-z0-9-]*$/.test(path) || ['.git', '.pbai4devs', '.agent-tests'].includes(path))) throw new Error('Raiz de cliente invalida para exclusao.');
|
|
137
|
+
const paths = [...new Set([...outputs.map(checkedOutput), ...template.split(/\r?\n/).filter(line => line.startsWith('/')).map(line => checkedOutput(line.slice(1)))])].sort();
|
|
138
|
+
const info = gitCommand(resolved.parent, ['rev-parse', '--show-toplevel', '--git-path', 'info/exclude']);
|
|
139
|
+
let location = null;
|
|
140
|
+
if (info.status === 0) {
|
|
141
|
+
const [reportedTop, reportedExclude] = info.stdout.trimEnd().split(/\r?\n/);
|
|
142
|
+
if (!reportedTop || !reportedExclude) throw new Error('Localizacao Git invalida.');
|
|
143
|
+
const exclude = resolve(resolved.parent, reportedExclude);
|
|
144
|
+
const top = canonicalExisting(reportedTop);
|
|
145
|
+
const prefix = relative(top, root);
|
|
146
|
+
if (isAbsolute(prefix) || prefix === '..' || prefix.startsWith(`..${sep}`)) throw new Error('Projeto fora da arvore Git.');
|
|
147
|
+
checkedLocalPath(exclude);
|
|
148
|
+
const canonicalExclude = localStat(exclude) ? canonicalExisting(exclude) : join(localRoot(dirname(exclude)).root, basename(exclude));
|
|
149
|
+
location = { top, exclude: checkedLocalPath(canonicalExclude), prefix: portable(prefix) };
|
|
150
|
+
} else if (info.error && info.error.code !== 'ENOENT' || !info.error && !/not a git repository/i.test(info.stderr || '')) {
|
|
151
|
+
throw new Error(`Nao foi possivel localizar Git: ${info.error?.message || info.stderr}`);
|
|
152
|
+
}
|
|
153
|
+
const id = createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
154
|
+
const first = `# BEGIN AGENTDEVKIT LOCAL ${id}`, last = `# END AGENTDEVKIT LOCAL ${id}`;
|
|
155
|
+
const fixed = template.replace('{{ROOTS}}', roots.map(path => `/${path}/`).join('\n')).replace('{{OUTPUTS}}', paths.filter(path => !roots.some(root => path === root || path.startsWith(root + '/'))).map(path => `/${escapePattern(path)}`).join('\n'));
|
|
156
|
+
const body = fixed.split(/\r?\n/).map(line => location?.prefix && line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line).join('\n');
|
|
157
|
+
const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
|
|
158
|
+
const before = localRead(target), after = localBlock(before, first, last, body);
|
|
159
|
+
const edits = before === after ? [] : [{ path: target, before, after }];
|
|
160
|
+
const probes = [...paths, ...rootProbes(root, roots)];
|
|
161
|
+
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
162
|
+
if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
|
|
163
|
+
const ignored = gitCommand(location.top, ['check-ignore', '--no-index', '--stdin', '-z'], [...wanted].join('\0') + '\0');
|
|
164
|
+
if (!ignored.error && [0, 1].includes(ignored.status) && ignored.stdout.split('\0').filter(Boolean).length === wanted.size) edits.length = 0;
|
|
165
|
+
}
|
|
166
|
+
if (location) {
|
|
167
|
+
const fallback = checkedLocalPath(join(root, '.gitignore'));
|
|
168
|
+
const old = localRead(fallback), next = localBlock(old, first, last, null);
|
|
169
|
+
if (old !== null && old !== next) edits.push({ path: fallback, before: old, after: next });
|
|
170
|
+
}
|
|
171
|
+
const tracked = location ? gitCommand(location.top, ['ls-files', '-z']).stdout.split('\0').filter(path => wanted.has(path)).map(path => location.prefix ? path.slice(location.prefix.length + 1) : path) : [];
|
|
172
|
+
return { root, status: location ? 'git' : 'fallback', location, paths, roots, edits, trackedOutputs: tracked, first, last };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
176
|
+
const missing = [];
|
|
177
|
+
const replacements = replacedBlocks.map(item => {
|
|
178
|
+
const path = canonicalExisting(checkedLocalPath(resolve(item.path))), inside = relative(localRoot(plan.root).root, path);
|
|
179
|
+
if (isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`) || !Number.isInteger(item.firstLine) || !Number.isInteger(item.lastLine) || item.firstLine < 1 || item.lastLine < item.firstLine || !/^[a-f0-9]{64}$/.test(item.sha256)) throw new Error('Faixa de ignore gerenciado invalida.');
|
|
180
|
+
const content = readFileSync(checkedLocalPath(path), 'utf8');
|
|
181
|
+
const lines = [...content.matchAll(/[^\r\n]*(?:\r\n|\n|\r|$)/g)].filter(match => match[0]);
|
|
182
|
+
if (item.lastLine > lines.length) throw new Error('Faixa de ignore fora do arquivo.');
|
|
183
|
+
const first = lines[item.firstLine - 1], last = lines[item.lastLine - 1];
|
|
184
|
+
const block = content.slice(first.index, last.index + last[0].replace(/[\r\n]+$/, '').length);
|
|
185
|
+
if (createHash('sha256').update(block).digest('hex') !== item.sha256) throw new Error('Bloco de ignore mudou antes da verificacao.');
|
|
186
|
+
return { ...item, path };
|
|
187
|
+
});
|
|
188
|
+
if (plan.location && plan.paths.length) {
|
|
189
|
+
const probes = [...plan.paths, ...rootProbes(plan.root, plan.roots || [])];
|
|
190
|
+
const relativePaths = probes.map(path => plan.location.prefix ? `${plan.location.prefix}/${path}` : path);
|
|
191
|
+
const result = gitCommand(plan.location.top, ['check-ignore', '--verbose', '--no-index', '--stdin', '-z'], relativePaths.join('\0') + '\0');
|
|
192
|
+
if (result.error || ![0, 1].includes(result.status)) throw new Error(result.error?.message || result.stderr || 'Falha ao conferir exclusoes Git.');
|
|
193
|
+
const values = result.stdout.split('\0'), matches = new Map();
|
|
194
|
+
for (let index = 0; index + 3 < values.length; index += 4) matches.set(values[index + 3], { source: values[index], line: Number(values[index + 1]), pattern: values[index + 2] });
|
|
195
|
+
for (let index = 0; index < relativePaths.length; index++) {
|
|
196
|
+
const match = matches.get(relativePaths[index]);
|
|
197
|
+
const negative = match?.pattern.startsWith('!');
|
|
198
|
+
const humanOverride = negative && /(?:^|[\\/])\.gitignore$/.test(match.source);
|
|
199
|
+
const replaced = negative && replacements.some(item => item.path === canonicalExisting(resolve(plan.location.top, match.source)) && match.line >= item.firstLine && match.line <= item.lastLine);
|
|
200
|
+
if (!replaced && ((!plan.edits.length && (!match || negative)) || humanOverride)) missing.push(probes[index]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: plan.edits.length === 0 && missing.length === 0 && plan.trackedOutputs.length === 0, status: plan.status, missing, trackedOutputs: plan.trackedOutputs, changed: plan.edits.length };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function assertLocalIgnoreUnchanged(plan) {
|
|
207
|
+
for (const edit of plan.edits) {
|
|
208
|
+
checkedLocalPath(edit.path);
|
|
209
|
+
if (localRead(edit.path) !== edit.before) throw new Error(`Exclusao alterada apos planejar: ${edit.path}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function applyLocalIgnore(plan) {
|
|
214
|
+
assertLocalIgnoreUnchanged(plan);
|
|
215
|
+
const written = [];
|
|
216
|
+
try {
|
|
217
|
+
for (const edit of plan.edits) {
|
|
218
|
+
mkdirSync(dirname(edit.path), { recursive: true });
|
|
219
|
+
const temporary = `${edit.path}.agentdevkit-${randomUUID()}`;
|
|
220
|
+
try { writeFileSync(temporary, edit.after, { flag: 'wx' }); renameSync(temporary, edit.path); }
|
|
221
|
+
finally { rmSync(temporary, { force: true }); }
|
|
222
|
+
written.push(edit);
|
|
223
|
+
}
|
|
224
|
+
const verified = checkLocalIgnore({ ...plan, edits: [], trackedOutputs: [] });
|
|
225
|
+
if (verified.missing.length) throw new Error(`Regras humanas reabrem derivados locais: ${verified.missing.join(', ')}`);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
for (const edit of written.reverse()) {
|
|
228
|
+
if (localRead(edit.path) !== edit.after) continue;
|
|
229
|
+
if (edit.before === null) rmSync(edit.path, { force: true }); else writeFileSync(edit.path, edit.before);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
return { status: plan.status, changed: plan.edits.length, trackedOutputs: plan.trackedOutputs };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function planUntrack(root, expectedHashes = {}) {
|
|
237
|
+
root = localRoot(root).root;
|
|
238
|
+
const candidates = Object.keys(expectedHashes).map(checkedOutput).sort();
|
|
239
|
+
for (const hash of Object.values(expectedHashes)) if (!/^[a-f0-9]{64}$/.test(hash)) throw new Error('Hash de ownership invalido para desindexar.');
|
|
240
|
+
if (!candidates.length || !localStat(root)) return { root, paths: [], hashes: {}, index: '', conflicts: [] };
|
|
241
|
+
const result = gitCommand(root, ['ls-files', '--stage', '-z']);
|
|
242
|
+
if (result.status !== 0) {
|
|
243
|
+
if (result.error?.code === 'ENOENT' || /not a git repository/i.test(result.stderr || '')) return { root, paths: [], hashes: {}, index: '', conflicts: [] };
|
|
244
|
+
throw new Error(result.error?.message || result.stderr || 'Falha ao ler indice Git.');
|
|
245
|
+
}
|
|
246
|
+
const conflicts = [];
|
|
247
|
+
const records = result.stdout.split('\0').filter(Boolean);
|
|
248
|
+
const wanted = new Set(candidates);
|
|
249
|
+
const matches = new Map();
|
|
250
|
+
for (const record of records) {
|
|
251
|
+
const position = record.indexOf('\t'), path = record.slice(position + 1);
|
|
252
|
+
if (position < 0 || !wanted.has(path)) continue;
|
|
253
|
+
matches.set(path, [...(matches.get(path) || []), record]);
|
|
254
|
+
}
|
|
255
|
+
const paths = [...matches.keys()].filter(path => {
|
|
256
|
+
const file = checkedLocalPath(join(root, path));
|
|
257
|
+
const entries = matches.get(path);
|
|
258
|
+
const match = entries.length === 1 && entries[0].match(/^\d+ ([a-f0-9]+) 0\t/);
|
|
259
|
+
const blob = match ? gitCommand(root, ['cat-file', 'blob', match[1]]) : null;
|
|
260
|
+
const owned = localStat(file) && createHash('sha256').update(readFileSync(file)).digest('hex') === expectedHashes[path];
|
|
261
|
+
const indexOwned = blob?.status === 0 && createHash('sha256').update(blob.stdout).digest('hex') === expectedHashes[path];
|
|
262
|
+
if (!owned || !indexOwned) conflicts.push(path);
|
|
263
|
+
return owned && indexOwned;
|
|
264
|
+
});
|
|
265
|
+
const selected = new Set(paths);
|
|
266
|
+
const index = records.filter(record => selected.has(record.slice(record.indexOf('\t') + 1))).join('\0');
|
|
267
|
+
return { root, paths, hashes: Object.fromEntries(paths.map(path => [path, expectedHashes[path]])), index, conflicts };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function applyUntrack(plan) {
|
|
271
|
+
if (plan.conflicts?.length) throw new Error(`Indice contem alteracoes nao pertencentes ao kit: ${plan.conflicts.join(', ')}`);
|
|
272
|
+
if (!plan.paths.length) return { removed: [] };
|
|
273
|
+
const current = planUntrack(plan.root, plan.hashes);
|
|
274
|
+
if (current.index !== plan.index || current.paths.length !== plan.paths.length) throw new Error('Ownership ou indice mudou antes de desindexar.');
|
|
275
|
+
const location = gitCommand(plan.root, ['rev-parse', '--show-toplevel']);
|
|
276
|
+
if (location.status !== 0 || location.error) throw new Error('Nao foi possivel localizar a raiz Git para desindexar.');
|
|
277
|
+
const top = canonicalExisting(location.stdout.trimEnd()), prefix = portable(relative(top, localRoot(plan.root).root));
|
|
278
|
+
const paths = plan.paths.map(path => prefix ? `${prefix}/${path}` : path);
|
|
279
|
+
const result = gitCommand(top, ['rm', '--cached', '--pathspec-from-file=-', '--pathspec-file-nul'], paths.join('\0') + '\0');
|
|
280
|
+
if (result.status !== 0 || result.error) throw new Error(result.error?.message || result.stderr || 'Falha ao desindexar derivados.');
|
|
281
|
+
return { removed: [...plan.paths] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Reconstitui apenas entradas removidas; o índice atual e alterações concorrentes
|
|
285
|
+
// permanecem. O lock é do próprio Git e o trabalho ocorre numa cópia desse índice.
|
|
286
|
+
export function restoreUntrack(plan) {
|
|
287
|
+
if (!plan?.paths?.length) return { restored: [], preserved: [] };
|
|
288
|
+
const located = gitCommand(plan.root, ['rev-parse', '--show-toplevel', '--git-path', 'index']);
|
|
289
|
+
if (located.status !== 0 || located.error) throw new Error('Nao foi possivel localizar o indice para rollback.');
|
|
290
|
+
const [reportedTop, relativeIndex] = located.stdout.trimEnd().split(/\r?\n/);
|
|
291
|
+
const reportedIndex = resolve(plan.root, relativeIndex);
|
|
292
|
+
checkedLocalPath(reportedIndex);
|
|
293
|
+
const top = canonicalExisting(reportedTop), indexPath = canonicalExisting(reportedIndex);
|
|
294
|
+
checkedLocalPath(indexPath);
|
|
295
|
+
if (!localStat(indexPath)) throw new Error('Indice ausente; rollback automatico recusado.');
|
|
296
|
+
const lock = `${indexPath}.lock`;
|
|
297
|
+
let descriptor, acquired = false;
|
|
298
|
+
try {
|
|
299
|
+
descriptor = openSync(lock, 'wx');
|
|
300
|
+
acquired = true;
|
|
301
|
+
writeFileSync(descriptor, readFileSync(indexPath));
|
|
302
|
+
const current = gitCommand(plan.root, ['ls-files', '--stage', '-z']);
|
|
303
|
+
if (current.status !== 0 || current.error) throw new Error('Nao foi possivel ler indice atual para rollback.');
|
|
304
|
+
const existing = new Set(current.stdout.split('\0').filter(Boolean).map(record => record.slice(record.indexOf('\t') + 1)));
|
|
305
|
+
const entries = plan.index.split('\0').filter(Boolean);
|
|
306
|
+
const selected = new Set(plan.paths), restored = [], preserved = [];
|
|
307
|
+
const prefix = portable(relative(top, localRoot(plan.root).root));
|
|
308
|
+
const records = [];
|
|
309
|
+
for (const record of entries) {
|
|
310
|
+
const at = record.indexOf('\t'), path = record.slice(at + 1);
|
|
311
|
+
if (!selected.has(path)) continue;
|
|
312
|
+
if (existing.has(path)) { preserved.push(path); continue; }
|
|
313
|
+
restored.push(path);
|
|
314
|
+
records.push(record.slice(0, at + 1) + (prefix ? `${prefix}/${path}` : path));
|
|
315
|
+
}
|
|
316
|
+
if (!records.length) return { restored, preserved };
|
|
317
|
+
const env = { ...process.env };
|
|
318
|
+
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE']) delete env[key];
|
|
319
|
+
env.GIT_INDEX_FILE = lock;
|
|
320
|
+
const result = spawnSync('git', ['-C', top, 'update-index', '-z', '--index-info'], { input: records.join('\0') + '\0', encoding: 'utf8', env, windowsHide: true, timeout: 20000 });
|
|
321
|
+
if (result.status !== 0 || result.error) throw new Error(result.error?.message || result.stderr || 'Rollback do indice falhou.');
|
|
322
|
+
closeSync(descriptor); descriptor = undefined;
|
|
323
|
+
renameSync(lock, indexPath);
|
|
324
|
+
return { restored, preserved };
|
|
325
|
+
} finally {
|
|
326
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
327
|
+
if (acquired) rmSync(lock, { force: true });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function mergeGitignore(current, template) {
|
|
332
|
+
if (typeof current !== 'string' || typeof template !== 'string' || current.includes('\0') || template.includes('\0')) throw new Error('Conteudo .gitignore invalido.');
|
|
333
|
+
const body = template.replace(/\r\n?/g, '\n').replace(/\n*$/, '');
|
|
334
|
+
if (!body.trim() || body.split('\n').some(line => [ignoreBegin, ignoreEnd].includes(line.trim()))) throw new Error('Template .gitignore vazio ou com marcadores reservados.');
|
|
335
|
+
const markers = [];
|
|
336
|
+
const lines = /([^\r\n]*)(\r\n|\n|\r|$)/g;
|
|
337
|
+
for (const match of current.matchAll(lines)) {
|
|
338
|
+
const value = match[1].trim();
|
|
339
|
+
if ([ignoreBegin, ignoreEnd].includes(value)) markers.push({ value, start: match.index, end: match.index + match[0].length });
|
|
340
|
+
}
|
|
341
|
+
const block = `${ignoreBegin}\n${body}\n${ignoreEnd}\n`;
|
|
342
|
+
if (!markers.length) {
|
|
343
|
+
const separator = !current || /(?:\r\n|\n|\r){2}$/.test(current) ? '' : /(?:\r\n|\n|\r)$/.test(current) ? '\n' : '\n\n';
|
|
344
|
+
return { text: current + separator + block, hasBlock: false };
|
|
345
|
+
}
|
|
346
|
+
if (markers.length !== 2 || markers[0].value !== ignoreBegin || markers[1].value !== ignoreEnd) throw new Error('Marcadores .gitignore incompletos, duplicados ou fora de ordem.');
|
|
347
|
+
return { text: current.slice(0, markers[0].start) + block + current.slice(markers[1].end), hasBlock: true };
|
|
348
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function npmCandidates({ node = process.execPath, environment = process.env } = {}) {
|
|
5
|
+
const candidates = [join(dirname(node), 'node_modules/npm/bin/npm-cli.js'), join(dirname(node), '../lib/node_modules/npm/bin/npm-cli.js'), environment.npm_execpath];
|
|
6
|
+
for (const entry of (environment.PATH || environment.Path || '').split(delimiter).filter(Boolean)) {
|
|
7
|
+
for (const name of ['npm', 'npm.cmd']) {
|
|
8
|
+
const file = join(entry, name);
|
|
9
|
+
if (existsSync(file)) candidates.push(realpathSync(file), join(entry, 'node_modules/npm/bin/npm-cli.js'));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return [...new Set(candidates.filter(file => file && file.endsWith('npm-cli.js') && existsSync(file) && statSync(file).isFile()).map(file => realpathSync(file)))];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findNpmCli(options) {
|
|
16
|
+
return npmCandidates(options)[0] || null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
const principals = { skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md' };
|
|
3
|
+
const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'instrucoes', 'prompt', 'agente']);
|
|
4
|
+
const fail = message => { throw new Error(message); };
|
|
5
|
+
// Layouts de arquivo preservam os grupos, retirando apenas o nome do principal.
|
|
6
|
+
export function resourcePath(source, kind, mode = 'agrupado') {
|
|
7
|
+
if (!Object.hasOwn(principals, kind) || !layouts.has(mode)) fail(`Formato invalido: ${kind}/${mode}`);
|
|
8
|
+
const path = source.slice('.pbai4devs/'.length);
|
|
9
|
+
if (mode === 'agrupado') return path;
|
|
10
|
+
const slug = posix.basename(posix.dirname(source));
|
|
11
|
+
if (mode === 'plano') {
|
|
12
|
+
if (!/^[a-z0-9-]+$/.test(slug)) fail(`Nome invalido para layout plano: ${source}`);
|
|
13
|
+
return kind === 'skills' ? `skills/${slug}/SKILL.md` : `${kind}/${slug}.md`;
|
|
14
|
+
}
|
|
15
|
+
if (kind === 'skills') fail(`Skills exigem formato agrupado ou plano: ${mode}`);
|
|
16
|
+
const folder = posix.dirname(path);
|
|
17
|
+
const extensions = { arquivo: '.md', mdc: '.mdc', instrucoes: '.instructions.md', prompt: '.prompt.md', agente: '.agent.md' };
|
|
18
|
+
const scopes = { instrucoes: 'instructions', prompt: 'prompts' };
|
|
19
|
+
return `${scopes[mode] || kind}${folder.slice(kind.length)}${extensions[mode]}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
6
|
+
|
|
7
|
+
export function requireNode(minimum = 24, version = process.versions.node) {
|
|
8
|
+
if (process.versions.bun || !/^\d+\.\d+\.\d+/.test(version || '') || Number(version.split('.')[0]) < minimum) {
|
|
9
|
+
throw new Error(`Instale Node.js >=${minimum} com npm: https://nodejs.org/en/download .`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isMain(metaUrl) {
|
|
14
|
+
return Boolean(process.argv[1]) && metaUrl === pathToFileURL(resolve(process.argv[1])).href;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function findExecutable(name, environment = process.env, platform = process.platform) {
|
|
18
|
+
const extensions = platform === 'win32' ? (environment.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';') : [''];
|
|
19
|
+
const names = platform === 'win32' && !/\.[^/\\]+$/.test(name) ? [name, ...extensions.map(ext => name + ext.toLowerCase())] : [name];
|
|
20
|
+
const directories = isAbsolute(name) ? [''] : (environment.PATH || environment.Path || '').split(delimiter).filter(Boolean);
|
|
21
|
+
for (const directory of directories) for (const candidate of names) {
|
|
22
|
+
const file = directory ? join(directory, candidate) : candidate;
|
|
23
|
+
try {
|
|
24
|
+
if (statSync(file).isFile()) { accessSync(file, platform === 'win32' ? constants.F_OK : constants.X_OK); return resolve(file); }
|
|
25
|
+
} catch { /* Candidato ausente ou sem permissao. */ }
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Contrato unico do seletor e do catalogo para SCRIPT.conf.
|
|
2
|
+
export function parseScriptConfig(text) {
|
|
3
|
+
const entries = text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'));
|
|
4
|
+
if (entries.length !== 1 || !/^executor\s*=\s*(node|playwright-mcp)$/.test(entries[0])) {
|
|
5
|
+
throw new Error('SCRIPT.conf exige uma unica entrada executor=node ou executor=playwright-mcp.');
|
|
6
|
+
}
|
|
7
|
+
return entries[0].split('=')[1].trim();
|
|
8
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Recuperação de Node.js por JavaScript sem instalação global.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Preparar o runtime
|
|
6
|
+
|
|
7
|
+
O inicializador usa Node.js 18+ para preparar Node.js 24+ e npm locais.
|
|
8
|
+
Se Node/npm estiverem totalmente ausentes, o agente deve instalá-los pelo
|
|
9
|
+
gerenciador do sistema operacional antes de executar JavaScript ou npx.
|
|
10
|
+
|
|
11
|
+
Com Node disponível, execute na raiz do projeto:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
node .pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs --runtime-only
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
O modo `--runtime-only` imprime `node=<caminho absoluto>` sem instalar
|
|
18
|
+
ferramentas. Sem a opção, continua com `setup`; aceita grupo ou nome.
|
|
19
|
+
|
|
20
|
+
O fluxo reutiliza um runtime saudável ou o cache local. Se necessário, baixa
|
|
21
|
+
Node 24 de `nodejs.org`, verifica SHA-256 e extrai usando `tar` nativo.
|
|
22
|
+
Windows usa ZIP; macOS/Linux usam tar.gz. Não há scripts Bash ou PowerShell,
|
|
23
|
+
mudança permanente de PATH ou instalação global neste fluxo.
|
|
24
|
+
|
|
25
|
+
O cache fica em `.pbai4devs/tools/.cache/runtime/`. Falhas de download,
|
|
26
|
+
integridade ou validação preservam o runtime anterior. Arquivos e links fora
|
|
27
|
+
da distribuição são recusados. `tar` deve estar disponível no sistema.
|
|
28
|
+
|
|
29
|
+
Navegadores, permissões desktop e FFmpeg dependem da tarefa e não são
|
|
30
|
+
instalados ou ativados por este inicializador.
|
|
31
|
+
|
|
32
|
+
Fonte: [distribuições oficiais do Node.js][node].
|
|
33
|
+
|
|
34
|
+
[node]: https://nodejs.org/dist/latest-v24.x/
|