@pathbit/agentdevkit 0.0.0-stage → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +109 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +87 -0
  3. package/.pbai4devs/README.md +50 -0
  4. package/.pbai4devs/agents/README.md +18 -0
  5. package/.pbai4devs/agents/revisor/AGENT.md +16 -0
  6. package/.pbai4devs/agents/revisor/antigravity.yaml +8 -0
  7. package/.pbai4devs/agents/revisor/claude.yaml +5 -0
  8. package/.pbai4devs/agents/revisor/cursor.yaml +5 -0
  9. package/.pbai4devs/agents/revisor/gemini.yaml +6 -0
  10. package/.pbai4devs/agents/revisor/github.yaml +5 -0
  11. package/.pbai4devs/agents/revisor/kimi-code.yaml +5 -0
  12. package/.pbai4devs/agents/revisor/mimocode.yaml +10 -0
  13. package/.pbai4devs/agents/revisor/opencode.yaml +7 -0
  14. package/.pbai4devs/agents/revisor/zcode.yaml +5 -0
  15. package/.pbai4devs/commands/README.md +30 -0
  16. package/.pbai4devs/commands/atualizar-ferramentas/COMMAND.md +17 -0
  17. package/.pbai4devs/commands/diagnosticar/COMMAND.md +18 -0
  18. package/.pbai4devs/commands/instalar-ferramentas/COMMAND.md +21 -0
  19. package/.pbai4devs/commands/preparar-dev/COMMAND.md +14 -0
  20. package/.pbai4devs/commands/proximos-passos/COMMAND.md +9 -0
  21. package/.pbai4devs/commands/revisar-entrega/COMMAND.md +29 -0
  22. package/.pbai4devs/commands/sincronizar/COMMAND.md +20 -0
  23. package/.pbai4devs/commands/status-roadmap/COMMAND.md +12 -0
  24. package/.pbai4devs/commands/testar-navegador/COMMAND.md +16 -0
  25. package/.pbai4devs/commands/verificar-agentes/COMMAND.md +19 -0
  26. package/.pbai4devs/estado/README.md +12 -0
  27. package/.pbai4devs/external-skills/CATALOG.json +121 -0
  28. package/.pbai4devs/external-skills/README.md +48 -0
  29. package/.pbai4devs/rules/README.md +18 -0
  30. package/.pbai4devs/rules/ambiente-local/RULE.md +33 -0
  31. package/.pbai4devs/rules/ambiente-local/cursor.yaml +4 -0
  32. package/.pbai4devs/rules/documentacao/RULE.md +11 -0
  33. package/.pbai4devs/rules/documentacao/cursor.yaml +4 -0
  34. package/.pbai4devs/rules/estrutura/RULE.md +33 -0
  35. package/.pbai4devs/rules/estrutura/cursor.yaml +4 -0
  36. package/.pbai4devs/rules/git/RULE.md +18 -0
  37. package/.pbai4devs/rules/git/cursor.yaml +4 -0
  38. package/.pbai4devs/rules/proximos-passos/RULE.md +14 -0
  39. package/.pbai4devs/rules/qualidade/RULE.md +32 -0
  40. package/.pbai4devs/rules/qualidade/cursor.yaml +4 -0
  41. package/.pbai4devs/scripts/README.md +69 -0
  42. package/.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs +41 -0
  43. package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +30 -0
  44. package/.pbai4devs/scripts/executar-script/README.md +28 -0
  45. package/.pbai4devs/scripts/executar-script/SCRIPT.mjs +78 -0
  46. package/.pbai4devs/scripts/executar-script/tests/TEST.mjs +122 -0
  47. package/.pbai4devs/scripts/gerenciar-skills-externas/README.md +31 -0
  48. package/.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs +182 -0
  49. package/.pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs +134 -0
  50. package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +190 -0
  51. package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +161 -0
  52. package/.pbai4devs/scripts/lib/README.md +9 -0
  53. package/.pbai4devs/scripts/lib/adapters.mjs +135 -0
  54. package/.pbai4devs/scripts/lib/catalog.mjs +170 -0
  55. package/.pbai4devs/scripts/lib/gitignore.mjs +348 -0
  56. package/.pbai4devs/scripts/lib/npm.mjs +17 -0
  57. package/.pbai4devs/scripts/lib/resource-path.mjs +20 -0
  58. package/.pbai4devs/scripts/lib/runtime.mjs +28 -0
  59. package/.pbai4devs/scripts/lib/script-config.mjs +8 -0
  60. package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md +34 -0
  61. package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs +189 -0
  62. package/.pbai4devs/scripts/preparar-ferramentas/README.md +71 -0
  63. package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +455 -0
  64. package/.pbai4devs/scripts/preparar-ferramentas/tests/FIXTURE.html +20 -0
  65. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_BROWSER.mjs +55 -0
  66. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_DESKTOP.mjs +61 -0
  67. package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_VERCEL.mjs +192 -0
  68. package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +552 -0
  69. package/.pbai4devs/scripts/preparar-ferramentas/tests/bootstrap.test.mjs +94 -0
  70. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash/input.txt +2 -0
  71. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash-overlay/input.txt +1 -0
  72. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/skills/agentdevkit-smoke/SKILL.md +8 -0
  73. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/index.html +6 -0
  74. package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/webreel.config.json +10 -0
  75. package/.pbai4devs/scripts/revisar-entrega/README.md +36 -0
  76. package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +249 -0
  77. package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +249 -0
  78. package/.pbai4devs/scripts/sincronizar-agentes/README.md +41 -0
  79. package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +473 -0
  80. package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +290 -0
  81. package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +869 -0
  82. package/.pbai4devs/scripts/testar-infraestrutura/SCRIPT.mjs +39 -0
  83. package/.pbai4devs/scripts/testar-infraestrutura/tests/TEST.mjs +29 -0
  84. package/.pbai4devs/scripts/testar-maturidade/README.md +54 -0
  85. package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +477 -0
  86. package/.pbai4devs/scripts/testar-maturidade/assets/calculator.mjs +1 -0
  87. package/.pbai4devs/scripts/testar-maturidade/assets/calculator.test.mjs.template +8 -0
  88. package/.pbai4devs/scripts/testar-maturidade/assets/http-api.test.mjs.template +28 -0
  89. package/.pbai4devs/scripts/testar-maturidade/assets/server.mjs +34 -0
  90. package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +305 -0
  91. package/.pbai4devs/scripts/testar-navegador/REFERENCE.md +49 -0
  92. package/.pbai4devs/scripts/testar-navegador/SCRIPT.conf +1 -0
  93. package/.pbai4devs/scripts/testar-navegador/SCRIPT.js +64 -0
  94. package/.pbai4devs/scripts/testar-navegador/tests/FAIL.js +8 -0
  95. package/.pbai4devs/scripts/testar-navegador/tests/SUCCESS.js +13 -0
  96. package/.pbai4devs/scripts/verificar-recursos/README.md +25 -0
  97. package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +209 -0
  98. package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +310 -0
  99. package/.pbai4devs/scripts/verificar-recursos/tests/ambiente-local.mjs +80 -0
  100. package/.pbai4devs/skills/README.md +24 -0
  101. package/.pbai4devs/skills/git/git-branch/SKILL.md +11 -0
  102. package/.pbai4devs/skills/git/git-commit/SKILL.md +16 -0
  103. package/.pbai4devs/skills/git/git-diff/SKILL.md +9 -0
  104. package/.pbai4devs/skills/git/git-fetch/SKILL.md +10 -0
  105. package/.pbai4devs/skills/git/git-log/SKILL.md +9 -0
  106. package/.pbai4devs/skills/git/git-merge/SKILL.md +13 -0
  107. package/.pbai4devs/skills/git/git-pull/SKILL.md +12 -0
  108. package/.pbai4devs/skills/git/git-push/SKILL.md +12 -0
  109. package/.pbai4devs/skills/git/git-status/SKILL.md +9 -0
  110. package/.pbai4devs/skills/git/git-worktree/SKILL.md +10 -0
  111. package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +37 -0
  112. package/.pbai4devs/skills/produto/criar-prd/SKILL.md +35 -0
  113. package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +34 -0
  114. package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +26 -0
  115. package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +62 -0
  116. package/.pbai4devs/skills/projeto/criar-agente/SKILL.md +20 -0
  117. package/.pbai4devs/skills/projeto/criar-comando/SKILL.md +25 -0
  118. package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +41 -0
  119. package/.pbai4devs/skills/projeto/criar-regra/SKILL.md +19 -0
  120. package/.pbai4devs/skills/projeto/criar-skill/SKILL.md +22 -0
  121. package/.pbai4devs/skills/projeto/executar-script/SKILL.md +29 -0
  122. package/.pbai4devs/skills/projeto/preparar-dev/SKILL.md +45 -0
  123. package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +56 -0
  124. package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +23 -0
  125. package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +42 -0
  126. package/.pbai4devs/skills/projeto/usar-skills-externas/SKILL.md +28 -0
  127. package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +45 -0
  128. package/.pbai4devs/skills/specs/criar-spec/SKILL.md +33 -0
  129. package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +26 -0
  130. package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +48 -0
  131. package/.pbai4devs/templates/README.md +65 -0
  132. package/.pbai4devs/templates/adr/TEMPLATE.md +21 -0
  133. package/.pbai4devs/templates/ambiente-local/Makefile.template +48 -0
  134. package/.pbai4devs/templates/ambiente-local/TEMPLATE.md +48 -0
  135. package/.pbai4devs/templates/ambiente-local/compose.local.yml.template +16 -0
  136. package/.pbai4devs/templates/ambiente-local/dockerignore.template +28 -0
  137. package/.pbai4devs/templates/changelog/TEMPLATE.md +17 -0
  138. package/.pbai4devs/templates/claude/TEMPLATE.md +8 -0
  139. package/.pbai4devs/templates/clients/ADAPTERS.json +306 -0
  140. package/.pbai4devs/templates/clients/TEMPLATE.md +12 -0
  141. package/.pbai4devs/templates/comando/TEMPLATE.md +15 -0
  142. package/.pbai4devs/templates/configuracao/TEMPLATE.md +46 -0
  143. package/.pbai4devs/templates/configuracao-raiz/TEMPLATE.md +22 -0
  144. package/.pbai4devs/templates/configuracao-raiz/editorconfig.template +21 -0
  145. package/.pbai4devs/templates/configuracao-raiz/env.example.template +3 -0
  146. package/.pbai4devs/templates/configuracao-raiz/gitattributes.template +6 -0
  147. package/.pbai4devs/templates/contribuicao/TEMPLATE.md +23 -0
  148. package/.pbai4devs/templates/design/TEMPLATE.md +30 -0
  149. package/.pbai4devs/templates/entrypoints/AGENTS.md +19 -0
  150. package/.pbai4devs/templates/entrypoints/CLAUDE.md +7 -0
  151. package/.pbai4devs/templates/entrypoints/GEMINI.md +7 -0
  152. package/.pbai4devs/templates/entrypoints/TEMPLATE.md +12 -0
  153. package/.pbai4devs/templates/gitignore/TEMPLATE.md +42 -0
  154. package/.pbai4devs/templates/gitignore/agents.ignore +45 -0
  155. package/.pbai4devs/templates/gitignore/clients.json +20 -0
  156. package/.pbai4devs/templates/gitignore/local.ignore +10 -0
  157. package/.pbai4devs/templates/gitignore/shared.ignore +60 -0
  158. package/.pbai4devs/templates/instrucoes-agentes/TEMPLATE.md +25 -0
  159. package/.pbai4devs/templates/plano-implementacao/TEMPLATE.md +27 -0
  160. package/.pbai4devs/templates/prd/TEMPLATE.md +30 -0
  161. package/.pbai4devs/templates/readme-pasta/TEMPLATE.md +16 -0
  162. package/.pbai4devs/templates/readme-projeto/TEMPLATE.md +34 -0
  163. package/.pbai4devs/templates/release/TEMPLATE.md +23 -0
  164. package/.pbai4devs/templates/revisao-entrega/TEMPLATE.md +41 -0
  165. package/.pbai4devs/templates/roadmap/TEMPLATE.md +18 -0
  166. package/.pbai4devs/templates/seguranca/TEMPLATE.md +22 -0
  167. package/.pbai4devs/templates/spec/TEMPLATE.md +61 -0
  168. package/.pbai4devs/templates/tasks/TEMPLATE.md +20 -0
  169. package/.pbai4devs/templates/ux-ui/TEMPLATE.md +26 -0
  170. package/.pbai4devs/tools/README.md +87 -0
  171. package/.pbai4devs/tools/agent-browser/README.md +26 -0
  172. package/.pbai4devs/tools/agent-browser/SCRIPT.mjs +5 -0
  173. package/.pbai4devs/tools/agent-browser/TOOL.json +33 -0
  174. package/.pbai4devs/tools/agent-desktop/README.md +26 -0
  175. package/.pbai4devs/tools/agent-desktop/SCRIPT.mjs +5 -0
  176. package/.pbai4devs/tools/agent-desktop/TOOL.json +30 -0
  177. package/.pbai4devs/tools/just-bash/README.md +23 -0
  178. package/.pbai4devs/tools/just-bash/SCRIPT.mjs +5 -0
  179. package/.pbai4devs/tools/just-bash/TOOL.json +27 -0
  180. package/.pbai4devs/tools/markdownlint-cli2/README.md +18 -0
  181. package/.pbai4devs/tools/markdownlint-cli2/SCRIPT.mjs +5 -0
  182. package/.pbai4devs/tools/markdownlint-cli2/TOOL.json +28 -0
  183. package/.pbai4devs/tools/opensrc/README.md +25 -0
  184. package/.pbai4devs/tools/opensrc/SCRIPT.mjs +5 -0
  185. package/.pbai4devs/tools/opensrc/TOOL.json +29 -0
  186. package/.pbai4devs/tools/portless/README.md +21 -0
  187. package/.pbai4devs/tools/portless/SCRIPT.mjs +5 -0
  188. package/.pbai4devs/tools/portless/TOOL.json +30 -0
  189. package/.pbai4devs/tools/skills/README.md +24 -0
  190. package/.pbai4devs/tools/skills/SCRIPT.mjs +5 -0
  191. package/.pbai4devs/tools/skills/TOOL.json +33 -0
  192. package/.pbai4devs/tools/webreel/README.md +23 -0
  193. package/.pbai4devs/tools/webreel/SCRIPT.mjs +5 -0
  194. package/.pbai4devs/tools/webreel/TOOL.json +25 -0
  195. package/CHANGELOG.md +58 -0
  196. package/CONTRIBUTING.md +73 -0
  197. package/LICENSE +21 -0
  198. package/README.en.md +78 -0
  199. package/README.es.md +80 -0
  200. package/README.md +88 -2
  201. package/bin/README.md +8 -0
  202. package/bin/agentdevkit.mjs +35 -0
  203. package/docs/README.md +81 -0
  204. package/docs/guides/README.md +14 -0
  205. package/docs/guides/cli.md +81 -0
  206. package/docs/guides/installation.md +78 -0
  207. package/docs/guides/resources.md +121 -0
  208. package/docs/guides/tools-and-testing.md +85 -0
  209. package/docs/guides/troubleshooting.md +151 -0
  210. package/docs/guides/workflow.md +130 -0
  211. package/docs/maintainers/README.md +12 -0
  212. package/docs/maintainers/community.md +49 -0
  213. package/docs/maintainers/development.md +54 -0
  214. package/docs/maintainers/maintenance.md +104 -0
  215. package/docs/maintainers/packages.md +76 -0
  216. package/docs/product/README.md +12 -0
  217. package/docs/product/design.md +94 -0
  218. package/docs/product/prd.md +64 -0
  219. package/docs/product/roadmap.md +36 -0
  220. package/docs/product/ux-ui.md +42 -0
  221. package/docs/specs/README.md +20 -0
  222. package/docs/specs/github-packages/SPEC.md +45 -0
  223. package/docs/specs/v0.0.1/README.md +10 -0
  224. package/docs/specs/v0.0.1/SPEC.md +57 -0
  225. package/docs/specs/v0.0.2/README.md +10 -0
  226. package/docs/specs/v0.0.2/SPEC.md +67 -0
  227. package/docs/specs/v0.0.3/README.md +10 -0
  228. package/docs/specs/v0.0.3/SPEC.md +64 -0
  229. package/docs/specs/v0.0.4/README.md +10 -0
  230. package/docs/specs/v0.0.4/SPEC.md +53 -0
  231. package/docs/specs/v0.0.5/README.md +9 -0
  232. package/docs/specs/v0.0.5/SPEC.md +42 -0
  233. package/package.json +52 -4
  234. package/src/README.md +16 -0
  235. package/src/adapters/index.mjs +1 -0
  236. package/src/cli.mjs +162 -0
  237. package/src/config.mjs +14 -0
  238. package/src/i18n.mjs +42 -0
  239. package/src/install.mjs +367 -0
  240. package/src/next.mjs +79 -0
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env node
2
+ // Definicoes versionadas por ferramenta; pacotes e estado permanecem locais.
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, realpathSync, openSync, readSync, closeSync, chmodSync, readdirSync, lstatSync, rmdirSync, unlinkSync } from 'node:fs';
4
+ import { dirname, join, resolve, delimiter, isAbsolute, relative, extname, sep } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { hostname } from 'node:os';
8
+ import { projectRoot, requireNode, isMain } from '../lib/runtime.mjs';
9
+
10
+ const registry = 'https://registry.npmjs.org/';
11
+ const versionPattern = /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/;
12
+ const json = file => JSON.parse(readFileSync(file, 'utf8'));
13
+ const fail = (message, exitCode) => { const error = new Error(message); if (Number.isInteger(exitCode)) error.exitCode = exitCode; throw error; };
14
+ const failureCode = error => Number.isInteger(error?.exitCode) && error.exitCode > 0 ? error.exitCode : 1;
15
+
16
+ function selectTools(catalog, selected) {
17
+ const grouped = Object.keys(catalog).filter(name => catalog[name].group === selected);
18
+ const names = selected === 'all' ? Object.keys(catalog) : selected === 'base' ? grouped : Object.hasOwn(catalog, selected) || !grouped.length ? [selected] : grouped;
19
+ if (!names.length) fail(`Nenhuma ferramenta definida para: ${selected}`);
20
+ return names;
21
+ }
22
+
23
+ export function platformKey(platform = process.platform, arch = process.arch) {
24
+ if (!['darwin', 'linux', 'win32'].includes(platform) || !['arm64', 'x64'].includes(arch)) fail(`SO/arquitetura nao suportado: ${platform}-${arch}`);
25
+ return `${platform}-${arch}`;
26
+ }
27
+
28
+ export function validateToolConfig(name, value) {
29
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || value?.name !== name) fail(`Nome de ferramenta invalido: ${name}`);
30
+ if (typeof value.description !== 'string' || !value.description.trim() || value.description.includes('\0')) fail(`Descricao ausente ou invalida: ${name}`);
31
+ const download = value.download;
32
+ if (download?.provider !== 'npm' || download.registry !== registry || download.channel !== 'latest') fail(`Download nao suportado: ${name}`);
33
+ if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/.test(download.package || '') || !/^[\w.-]+\/[\w.-]+$/.test(download.repository || '')) fail(`Origem invalida: ${name}`);
34
+ 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}`);
35
+ 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
+ if (value.binary !== undefined && (typeof value.binary !== 'string' || !/^[\w.-]+$/.test(value.binary))) fail(`Executavel invalido: ${name}`);
37
+ if (value.executables !== undefined) {
38
+ if (!value.executables || typeof value.executables !== 'object' || Array.isArray(value.executables) || !Object.keys(value.executables).length) fail(`Mapa de executaveis invalido: ${name}`);
39
+ for (const [platform, files] of Object.entries(value.executables)) {
40
+ if (!value.platforms.includes(platform) || !/^(?:darwin|linux)-(?:arm64|x64)$/.test(platform)) fail(`Plataforma de executaveis invalida: ${name}/${platform}`);
41
+ if (!Array.isArray(files) || !files.length || new Set(files).size !== files.length) fail(`Lista de executaveis invalida: ${name}/${platform}`);
42
+ for (const file of files) {
43
+ if (typeof file !== 'string' || !file || file.includes('\0') || file.includes('\\') || file.includes(':') || file.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho de executavel deve ser relativo ao pacote: ${name}`);
44
+ }
45
+ }
46
+ }
47
+ if (value.scripts?.entry !== 'SCRIPT.mjs' || Object.keys(value.scripts).length !== 1) fail(`Script invalido; use scripts.entry=SCRIPT.mjs: ${name}`);
48
+ if (value.nodeMinimum !== undefined) fail(`nodeMinimum foi substituido pelo requisito comum Node >=24: ${name}`);
49
+ const environmentKeys = new Set();
50
+ for (const field of ['env', 'envPaths']) {
51
+ if (value[field] === undefined) continue;
52
+ if (!value[field] || typeof value[field] !== 'object' || Array.isArray(value[field])) fail(`${field} invalido: ${name}`);
53
+ for (const [key, content] of Object.entries(value[field])) {
54
+ const normalized = key.toUpperCase();
55
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ['HOME', 'CODEX_HOME', 'PATH'].includes(normalized)) fail(`Variavel de ambiente protegida ou invalida: ${key}`);
56
+ if (environmentKeys.has(normalized)) fail(`Variavel de ambiente duplicada: ${key}`);
57
+ environmentKeys.add(normalized);
58
+ if (typeof content !== 'string' || content.includes('\0')) fail(`Valor de ambiente invalido: ${key}`);
59
+ }
60
+ }
61
+ if (value.envPaths !== undefined) {
62
+ for (const [key, location] of Object.entries(value.envPaths)) {
63
+ if (!location || location.includes('\\') || /^[A-Za-z]:/.test(location) || location.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho de ambiente deve ser relativo: ${key}`);
64
+ }
65
+ }
66
+ let source; try { source = new URL(value.source); } catch { fail(`Documentacao invalida: ${name}`); }
67
+ if (source.protocol !== 'https:' || source.username || source.password) fail(`Documentacao invalida: ${name}`);
68
+ return value;
69
+ }
70
+
71
+ export function toolActions(name, entry) {
72
+ validateToolConfig(name, entry);
73
+ const descriptions = {
74
+ install: 'Instala ou reutiliza uma instalacao saudavel.',
75
+ update: 'Consulta latest e atualiza com rollback em caso de falha.',
76
+ check: 'Executa o probe local sem instalar nem consultar latest.',
77
+ info: 'Mostra configuracao, plataforma, runtime e versao registrada.',
78
+ run: 'Executa o CLI instalado com os argumentos informados.',
79
+ unlock: 'Remove apenas um lock local cujo processo comprovadamente terminou.',
80
+ };
81
+ return Object.fromEntries(Object.entries(descriptions).map(([action, description]) => [action, {
82
+ executor: 'node', argv: ['node', `.pbai4devs/tools/${name}/${entry.scripts.entry}`, action], description,
83
+ ...(action === 'run' ? { requiresArguments: true } : {}),
84
+ }]));
85
+ }
86
+
87
+ export function toolWrapper(name) {
88
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) fail(`Nome de ferramenta invalido: ${name}`);
89
+ return `#!/usr/bin/env node\nimport { main } from '../../scripts/preparar-ferramentas/SCRIPT.mjs';\n\ntry { process.exitCode = await main(['--tool', ${JSON.stringify(name)}, ...process.argv.slice(2)]); }\ncatch (error) { console.error(error.message); process.exitCode = 1; }\n`;
90
+ }
91
+
92
+ function sourcePath(root, name) {
93
+ const file = resolve(root, name); const child = relative(root, file);
94
+ if (isAbsolute(child) || child === '..' || child.startsWith(`..${sep}`)) fail(`Caminho fora da raiz: ${name}`);
95
+ let current = root;
96
+ for (const part of child.split(sep).filter(Boolean)) {
97
+ current = join(current, part);
98
+ try { if (lstatSync(current).isSymbolicLink()) fail(`Fonte de ferramenta nao pode ser um link: ${name}`); }
99
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
100
+ }
101
+ return file;
102
+ }
103
+
104
+ export function loadCatalog(root = projectRoot) {
105
+ root = realpathSync(root);
106
+ const home = sourcePath(root, '.pbai4devs/tools'); const catalog = Object.create(null);
107
+ if (!existsSync(home)) return catalog;
108
+ for (const item of readdirSync(home, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
109
+ if (item.name.startsWith('.') || ['bin', 'npm-cache'].includes(item.name)) continue;
110
+ const folder = sourcePath(root, join('.pbai4devs/tools', item.name));
111
+ if (!lstatSync(folder).isDirectory()) continue;
112
+ const file = sourcePath(root, join('.pbai4devs/tools', item.name, 'TOOL.json'));
113
+ if (!existsSync(file) || !lstatSync(file).isFile()) fail(`TOOL.json ausente ou invalido: ${item.name}`);
114
+ const entry = validateToolConfig(item.name, json(file));
115
+ const script = sourcePath(root, join('.pbai4devs/tools', item.name, entry.scripts.entry));
116
+ if (!existsSync(script) || !lstatSync(script).isFile()) fail(`Script ausente ou invalido: ${item.name}/${entry.scripts.entry}`);
117
+ if (readFileSync(script, 'utf8').replace(/\r\n?/g, '\n') !== toolWrapper(item.name)) fail(`Apontamento de ferramenta divergente: ${item.name}; use toolWrapper(nome).`);
118
+ catalog[item.name] = entry;
119
+ }
120
+ return catalog;
121
+ }
122
+
123
+ export function validateRelease(name, entry, release) {
124
+ const expected = entry.download.package;
125
+ const repository = typeof release.repository === 'string' ? release.repository : release.repository?.url;
126
+ if (release.name !== expected || !versionPattern.test(release.version || '')) fail(`Nome/versao invalido no registro: ${name}`);
127
+ const origin = new URL(repository?.replace(/^git\+/, '').replace(/\.git$/, ''));
128
+ if (origin.protocol !== 'https:' || origin.hostname !== 'github.com' || origin.pathname !== `/${entry.download.repository}`) fail('Repositorio de origem divergente.');
129
+ const tarball = new URL(release.dist?.tarball);
130
+ if (tarball.origin !== registry.slice(0, -1) || !decodeURIComponent(tarball.pathname).startsWith(`/${expected}/-/`) || tarball.search || tarball.hash || !release.dist?.integrity?.startsWith('sha512-')) fail('Tarball ou integridade invalida.');
131
+ return release;
132
+ }
133
+
134
+ function npmCli(runtime, environment) {
135
+ const candidates = [join(dirname(runtime.file), 'node_modules/npm/bin/npm-cli.js'), join(dirname(runtime.file), '../lib/node_modules/npm/bin/npm-cli.js'), environment.npm_execpath];
136
+ for (const entry of (environment.PATH || '').split(delimiter)) for (const name of ['npm', 'npm.cmd']) {
137
+ const file = join(entry, name);
138
+ if (existsSync(file)) candidates.push(realpathSync(file), join(entry, 'node_modules/npm/bin/npm-cli.js'));
139
+ }
140
+ const cli = candidates.find(file => file && file.endsWith('npm-cli.js') && existsSync(file));
141
+ if (!cli) fail('npm nao encontrado junto do Node; siga os pre-requisitos do projeto.');
142
+ return cli;
143
+ }
144
+
145
+ export function binaryInvocation(prefix, name, args = [], binaryName = name.split('/').at(-1), node = process.execPath) {
146
+ const base = join(prefix, 'node_modules', name); const pkg = json(join(base, 'package.json'));
147
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[binaryName];
148
+ if (typeof bin !== 'string' || !bin || bin.includes('\0')) fail(`Executavel ausente ou invalido: ${name}`);
149
+ const binary = resolve(base, bin); const traversal = relative(base, binary);
150
+ if (isAbsolute(traversal) || traversal === '..' || traversal.startsWith('..' + (process.platform === 'win32' ? '\\' : '/'))) fail('Executavel fora do pacote.');
151
+ const resolved = relative(prefix, realpathSync(binary));
152
+ if (isAbsolute(resolved) || resolved === '..' || resolved.startsWith('..' + (process.platform === 'win32' ? '\\' : '/'))) fail('Executavel fora da instalacao local.');
153
+ const header = Buffer.alloc(128); const handle = openSync(binary, 'r');
154
+ try { readSync(handle, header, 0, header.length, 0); } finally { closeSync(handle); }
155
+ return /^#![^\n]*\bnode\b/.test(header.toString()) || ['.js', '.mjs', '.cjs'].includes(extname(binary)) ? [node, [binary, ...args]] : [binary, args];
156
+ }
157
+
158
+ export function toolEnvironment(entry, local, runtime) {
159
+ const environment = Object.assign(Object.create(null), process.env);
160
+ const set = (key, value) => {
161
+ if (process.platform === 'win32') for (const existing of Object.keys(environment)) if (existing.toUpperCase() === key.toUpperCase()) delete environment[existing];
162
+ environment[key] = value;
163
+ };
164
+ for (const [key, value] of Object.entries(entry.env || {})) set(key, value);
165
+ for (const [key, location] of Object.entries(entry.envPaths || {})) {
166
+ let directory = resolve(local);
167
+ for (const part of ['', ...location.split('/')]) {
168
+ if (part) directory = join(directory, part);
169
+ try { if (lstatSync(directory).isSymbolicLink()) fail(`Caminho de ambiente nao pode ser um link: ${key}`); }
170
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
171
+ }
172
+ set(key, resolve(local, location));
173
+ }
174
+ if (runtime) { const path = environment.PATH || environment.Path || ''; set('PATH', `${dirname(runtime.file)}${path ? delimiter + path : ''}`); }
175
+ return environment;
176
+ }
177
+
178
+ function execute(prefix, entry, args, capture = false, input, environment, runtime) {
179
+ const [file, options] = binaryInvocation(prefix, entry.download.package, args, entry.binary || entry.name, runtime.file);
180
+ const result = spawnSync(file, options, { stdio: capture ? 'pipe' : 'inherit', cwd: capture ? prefix : undefined, input, env: environment, encoding: 'utf8', timeout: capture ? 30000 : undefined, windowsHide: true });
181
+ if (result.error) throw result.error;
182
+ 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);
183
+ return result.status ?? 1;
184
+ }
185
+
186
+ function prepareExecutables(prefix, entry, platform) {
187
+ const files = entry.executables?.[platform];
188
+ if (!files || process.platform === 'win32') return;
189
+ const within = (base, file) => { const child = relative(base, file); return !isAbsolute(child) && child !== '..' && !child.startsWith(`..${sep}`); };
190
+ const stage = realpathSync(prefix);
191
+ const packageRoot = realpathSync(join(prefix, 'node_modules', entry.download.package));
192
+ if (!within(stage, packageRoot)) fail(`Pacote fora do staging: ${entry.name}`);
193
+ const targets = files.map(file => {
194
+ const target = realpathSync(join(packageRoot, file));
195
+ if (!within(packageRoot, target)) fail(`Executavel fora do pacote: ${entry.name}/${file}`);
196
+ const info = lstatSync(target);
197
+ if (!info.isFile()) fail(`Executavel nao e arquivo regular: ${entry.name}/${file}`);
198
+ return { target, mode: (info.mode & 0o777) | 0o111 };
199
+ });
200
+ for (const { target, mode } of targets) chmodSync(target, mode);
201
+ }
202
+
203
+ export class ToolManager {
204
+ constructor({ root = projectRoot, catalog, fetchRelease, install, npm = npmCli, nodeVersion = process.versions.node, platform = process.platform, arch = process.arch } = {}) {
205
+ this.root = resolve(root); this.home = join(this.root, '.pbai4devs/tools'); this.target = `${platform}-${arch}`;
206
+ this.nodeVersion = nodeVersion; this.npm = npm;
207
+ this.catalog = catalog ?? loadCatalog(this.root);
208
+ for (const [name, entry] of Object.entries(this.catalog)) validateToolConfig(name, entry);
209
+ this.fetchRelease = fetchRelease || (async (name, entry) => {
210
+ const response = await fetch(`${entry.download.registry}${encodeURIComponent(entry.download.package)}/${entry.download.channel}`, { signal: AbortSignal.timeout(20000) });
211
+ if (!response.ok) fail(`Registro indisponivel: HTTP ${response.status}`);
212
+ return response.json();
213
+ });
214
+ this.installPackage = install || ((prefix, packageName, version, entry, runtime) => {
215
+ const environment = toolEnvironment(entry, this.local(entry.name), runtime);
216
+ 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}`], {
217
+ cwd: prefix, encoding: 'utf8', timeout: 180000,
218
+ env: { ...environment, npm_config_engine_strict: 'true', npm_config_update_notifier: 'false', PUPPETEER_SKIP_DOWNLOAD: 'true', PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }
219
+ });
220
+ if (result.error || result.status !== 0) fail(`npm: ${result.error?.message || result.stderr?.trim() || result.status}`, result.status ?? 1);
221
+ });
222
+ }
223
+ entry(name) {
224
+ if (!Object.hasOwn(this.catalog, name)) fail(`Ferramenta desconhecida: ${name}`);
225
+ return this.catalog[name];
226
+ }
227
+ runtime(name) {
228
+ const entry = this.entry(name);
229
+ if (!entry.platforms.includes(this.target)) fail(`Plataforma nao suportada por ${name}: ${this.target}`);
230
+ requireNode(24, this.nodeVersion);
231
+ return { file: process.execPath, version: process.version };
232
+ }
233
+ preflight(names) {
234
+ const issues = [];
235
+ for (const name of names) try { this.runtime(name); } catch (error) { issues.push(`${name}: ${error.message}`); }
236
+ if (issues.length) fail(`Pre-requisitos indisponiveis; nenhuma ferramenta foi alterada:\n${issues.join('\n')}`);
237
+ }
238
+ checkNpm() {
239
+ requireNode(24, this.nodeVersion);
240
+ const runtime = { file: process.execPath, version: process.version };
241
+ const environment = { ...toolEnvironment({}, this.home, runtime), npm_config_update_notifier: 'false' };
242
+ let file;
243
+ try { file = realpathSync(this.npm(runtime, environment)); }
244
+ catch (error) { fail(`npm indisponivel: ${error.message}. Instale Node.js 24+ com npm: https://nodejs.org/en/download .`); }
245
+ if (!lstatSync(file).isFile()) fail(`npm nao e um arquivo: ${file}`);
246
+ const result = spawnSync(runtime.file, [file, '--version'], { cwd: this.root, env: environment, encoding: 'utf8', timeout: 15000, windowsHide: true });
247
+ const version = result.stdout?.trim();
248
+ if (result.error || result.status !== 0 || !versionPattern.test(version || '')) fail(`npm indisponivel: ${result.error?.message || result.stderr?.trim() || version || result.status}. Instale Node.js 24+ com npm: https://nodejs.org/en/download .`, result.status || 1);
249
+ return { path: file, version };
250
+ }
251
+ async setup(selected = 'all') {
252
+ const names = selectTools(this.catalog, selected);
253
+ const explicit = !['all', 'base'].includes(selected) && Object.hasOwn(this.catalog, selected);
254
+ const results = new Map(names.map(name => [name, null])); const compatible = [];
255
+ let npm = null;
256
+ for (const name of names) {
257
+ if (this.entry(name).platforms.includes(this.target)) compatible.push(name);
258
+ else results.set(name, { name, status: explicit ? 'failed' : 'not-applicable', phase: 'platform', exitCode: explicit ? 1 : null, reason: `Plataforma nao suportada: ${this.target}` });
259
+ }
260
+ try { if (compatible.length) { this.preflight(compatible); npm = this.checkNpm(); } }
261
+ catch (error) { for (const name of compatible) results.set(name, { name, status: 'failed', phase: 'preflight', exitCode: failureCode(error), reason: error.message }); }
262
+ if (npm) for (const name of compatible) {
263
+ let phase = 'install';
264
+ try {
265
+ const installed = await this.install(name); phase = 'check';
266
+ const checked = this.check(name);
267
+ results.set(name, { name, status: 'passed', phase, exitCode: 0, version: checked.version, changed: installed.changed });
268
+ } catch (error) { results.set(name, { name, status: 'failed', phase, exitCode: failureCode(error), reason: error.message }); }
269
+ }
270
+ const entries = [...results.values()];
271
+ const summary = { passed: entries.filter(item => item.status === 'passed').length, failed: entries.filter(item => item.status === 'failed').length, notApplicable: entries.filter(item => item.status === 'not-applicable').length };
272
+ return { mode: 'setup', selection: selected, platform: this.target, node: process.version, nodePath: process.execPath, npm, results: entries, summary, status: summary.failed ? 'failed' : summary.passed ? 'passed' : 'not-applicable', exitCode: summary.failed ? 1 : 0 };
273
+ }
274
+ local(name) { this.entry(name); return join(this.home, name, '.local'); }
275
+ current(name) {
276
+ this.entry(name);
277
+ const local = this.local(name); const legacy = join(this.home, name);
278
+ const base = existsSync(join(local, 'CURRENT.json')) ? local : legacy;
279
+ const file = join(base, 'CURRENT.json');
280
+ if (!existsSync(file)) fail(`Ferramenta ausente: ${name}; execute install ${name}.`);
281
+ const info = json(file);
282
+ if (info.name !== name || info.platform !== this.target || !versionPattern.test(info.version || '')) fail(`Instalacao incompativel: ${name}`);
283
+ return { ...info, layout: base === local ? 'local' : 'legacy', prefix: join(base, 'versions', `${info.version}-${this.target}`) };
284
+ }
285
+ check(name) {
286
+ const entry = this.entry(name); const runtime = this.runtime(name); const info = this.current(name);
287
+ const pkg = json(join(info.prefix, 'node_modules', entry.download.package, 'package.json'));
288
+ if (pkg.name !== entry.download.package || pkg.version !== info.version) fail(`Versao local divergente: ${name}`);
289
+ execute(info.prefix, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, this.local(name), runtime), runtime);
290
+ return info;
291
+ }
292
+ info(name) {
293
+ const config = this.entry(name); let installed = null; let issue; let runtime = null;
294
+ try { runtime = this.runtime(name); } catch (error) { issue = error.message; }
295
+ try { const current = this.current(name); installed = { version: current.version, platform: current.platform, layout: current.layout }; }
296
+ catch (error) { issue ??= error.message; }
297
+ return { ...config, platform: this.target, supported: runtime !== null, runtime: runtime ? { path: runtime.file, version: runtime.version } : null, installed, ...(issue ? { issue } : {}) };
298
+ }
299
+ launchers(name) {
300
+ this.entry(name);
301
+ const bin = sourcePath(this.root, join(this.home, 'bin'));
302
+ const launcher = sourcePath(this.root, join(bin, `${name}.mjs`));
303
+ mkdirSync(bin, { recursive: true });
304
+ writeFileSync(launcher, `#!/usr/bin/env node\nimport { main } from '../../scripts/preparar-ferramentas/SCRIPT.mjs';\ntry { process.exitCode = await main(['run', ${JSON.stringify(name)}, ...process.argv.slice(2)]); }\ncatch (error) { console.error(error.message); process.exitCode = 1; }\n`);
305
+ chmodSync(launcher, 0o755);
306
+ for (const legacy of [name, `${name}.cmd`, `${name}.ps1`, `${name}.sh`]) rmSync(join(bin, legacy), { force: true });
307
+ }
308
+ migrate(name) {
309
+ const local = this.local(name); const tool = join(this.home, name);
310
+ const legacyState = join(tool, 'CURRENT.json'); const legacyVersions = join(tool, 'versions');
311
+ if (existsSync(legacyState)) {
312
+ if (existsSync(join(local, 'CURRENT.json')) || (existsSync(join(local, 'versions')) && readdirSync(join(local, 'versions')).length)) fail(`Layouts local e antigo coexistem: ${name}; revise antes de migrar.`);
313
+ if (!existsSync(legacyVersions)) fail(`Versoes antigas ausentes: ${name}`);
314
+ if (existsSync(join(local, 'versions'))) rmdirSync(join(local, 'versions'));
315
+ renameSync(legacyVersions, join(local, 'versions'));
316
+ try { renameSync(legacyState, join(local, 'CURRENT.json')); }
317
+ catch (error) { renameSync(join(local, 'versions'), legacyVersions); throw error; }
318
+ }
319
+ const oldCache = join(this.home, 'npm-cache');
320
+ if (existsSync(oldCache)) {
321
+ const cache = join(this.home, '.cache'); mkdirSync(cache, { recursive: true });
322
+ try { renameSync(oldCache, join(cache, existsSync(join(cache, 'npm')) ? `npm-legacy-${process.pid}-${Date.now()}` : 'npm')); }
323
+ catch (error) { if (error.code !== 'ENOENT' || existsSync(oldCache)) throw error; }
324
+ }
325
+ }
326
+ async withLock(name, action) {
327
+ const local = this.local(name);
328
+ for (const directory of [this.home, join(this.home, name), local]) if (existsSync(directory) && lstatSync(directory).isSymbolicLink()) fail(`Diretorio local nao pode ser um link: ${directory}`);
329
+ mkdirSync(local, { recursive: true }); const lock = join(local, '.update-lock');
330
+ try { mkdirSync(lock); } catch (error) {
331
+ if (error.code === 'EEXIST') {
332
+ const status = this.lockStatus(name);
333
+ fail(status.state === 'stale' ? `Lock orfao: ${name}; use unlock ${name}.` : `Atualizacao em andamento ou lock indeterminado: ${name}`);
334
+ }
335
+ throw error;
336
+ }
337
+ try { writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() })); return await action(); }
338
+ finally { rmSync(lock, { recursive: true, force: true }); }
339
+ }
340
+ lockStatus(name) {
341
+ const lock = sourcePath(this.root, join(this.local(name), '.update-lock'));
342
+ if (!existsSync(lock)) return { state: 'missing', lock };
343
+ try {
344
+ if (!lstatSync(lock).isDirectory()) return { state: 'indeterminate', lock };
345
+ const ownerFile = sourcePath(this.root, join(lock, 'owner.json'));
346
+ if (!lstatSync(ownerFile).isFile()) return { state: 'indeterminate', lock };
347
+ const owner = json(ownerFile);
348
+ if (!Number.isSafeInteger(owner?.pid) || owner.pid <= 0 || owner.pid > 2147483647 || owner.hostname !== hostname()) return { state: 'indeterminate', lock };
349
+ try { process.kill(owner.pid, 0); return { state: 'active', lock, pid: owner.pid }; }
350
+ catch (error) { return { state: error.code === 'ESRCH' ? 'stale' : 'indeterminate', lock, pid: owner.pid }; }
351
+ } catch { return { state: 'indeterminate', lock }; }
352
+ }
353
+ unlock(name) {
354
+ const lock = sourcePath(this.root, join(this.local(name), '.update-lock'));
355
+ if (!existsSync(lock)) return { name, removed: false, state: 'missing' };
356
+ const recovery = join(lock, 'recovery.json'), token = randomUUID(); let claimed = false, moved = false;
357
+ try {
358
+ const handle = openSync(recovery, 'wx');
359
+ try { writeFileSync(handle, JSON.stringify({ token })); claimed = true; } finally { closeSync(handle); }
360
+ const ownerFile = sourcePath(this.root, join(lock, 'owner.json'));
361
+ const ownerSnapshot = readFileSync(ownerFile, 'utf8');
362
+ const status = this.lockStatus(name);
363
+ if (status.state !== 'stale') fail(`Lock ${status.state}: ${name}; nenhum lock foi removido.`);
364
+ if (json(sourcePath(this.root, recovery)).token !== token || readFileSync(ownerFile, 'utf8') !== ownerSnapshot) fail(`Lock mudou durante a recuperacao: ${name}.`);
365
+ const retired = sourcePath(this.root, join(this.local(name), `.unlocked-${token}`));
366
+ renameSync(lock, retired); moved = true;
367
+ rmSync(retired, { recursive: true });
368
+ return { name, removed: true, state: 'stale' };
369
+ } finally {
370
+ if (claimed && !moved) {
371
+ try { if (json(recovery).token === token) unlinkSync(recovery); } catch { /* O dono pode ter concluido e removido o lock. */ }
372
+ }
373
+ }
374
+ }
375
+ async install(name) { this.preflight([name]); return this.withLock(name, async () => {
376
+ this.migrate(name);
377
+ let installed;
378
+ try { installed = this.check(name); } catch { /* Ausente ou quebrado: preparar uma instalacao. */ }
379
+ if (installed) { this.launchers(name); return { ...installed, changed: false }; }
380
+ return this.updateLocked(name);
381
+ }); }
382
+ async update(name) { this.preflight([name]); return this.withLock(name, async () => { this.migrate(name); return this.updateLocked(name); }); }
383
+ commitState(name, temporary) { renameSync(temporary, join(this.local(name), 'CURRENT.json')); }
384
+ async updateLocked(name) {
385
+ const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry));
386
+ let installed;
387
+ try { installed = this.check(name); } catch { /* Ausente ou quebrado: reparar em staging. */ }
388
+ if (installed?.version === release.version) { this.launchers(name); return { ...installed, changed: false }; }
389
+ const local = this.local(name); mkdirSync(join(local, 'versions'), { recursive: true });
390
+ const stage = mkdtempSync(join(local, '.stage-')); const target = join(local, 'versions', `${release.version}-${this.target}`);
391
+ const temporary = join(local, `CURRENT.${process.pid}.${Date.now()}.json`); let previous; let moved = false;
392
+ try {
393
+ this.installPackage(stage, entry.download.package, release.version, entry, runtime);
394
+ const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
395
+ 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.');
396
+ prepareExecutables(stage, entry, this.target);
397
+ execute(stage, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, local, runtime), runtime);
398
+ 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() };
399
+ writeFileSync(temporary, `${JSON.stringify(info, null, 2)}\n`);
400
+ this.launchers(name);
401
+ if (existsSync(target)) { previous = `${target}.previous-${process.pid}-${Date.now()}`; renameSync(target, previous); }
402
+ try { renameSync(stage, target); moved = true; this.commitState(name, temporary); }
403
+ catch (error) { if (moved) rmSync(target, { recursive: true, force: true }); if (previous) renameSync(previous, target); throw error; }
404
+ return { ...info, layout: 'local', prefix: target, changed: true };
405
+ } finally { rmSync(stage, { recursive: true, force: true }); rmSync(temporary, { force: true }); }
406
+ }
407
+ run(name, args) { const entry = this.entry(name); const runtime = this.runtime(name); return execute(this.current(name).prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime); }
408
+ }
409
+
410
+ 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].';
411
+ const focusedHelp = name => `${name}: setup | install | update | check | info | unlock | run [argumentos]\nsetup instala e verifica; unlock recupera lock local orfao; check verifica; info mostra a configuracao; run executa.`;
412
+ export async function main(input = process.argv.slice(2), manager) {
413
+ requireNode();
414
+ const args = [...input]; let focused;
415
+ if (args[0] === '--tool') { args.shift(); focused = args.shift(); if (!focused) fail('Falta o nome de --tool.'); }
416
+ const aliases = { '--setup': 'setup', '--update': 'update', '--check': 'check', '--run': 'run', '--info': 'info', '--install': 'install' };
417
+ const supplied = args.shift(); const mode = aliases[supplied] || supplied || 'help';
418
+ if (['help', '--help', '-h'].includes(mode)) { console.log(focused ? focusedHelp(focused) : help); return 0; }
419
+ manager ??= new ToolManager();
420
+ if (focused) manager.entry(focused);
421
+ if (mode === 'unlock') {
422
+ const name = focused || args.shift(); if (!name || args.length) fail('Use unlock <nome> para uma unica ferramenta.');
423
+ console.log(JSON.stringify(manager.unlock(name))); return 0;
424
+ }
425
+ if (mode === 'run') { const name = focused || args.shift(); if (!name) fail('Uso: run <ferramenta> [argumentos]'); return manager.run(name, args); }
426
+ if (mode === 'list') {
427
+ if (focused || args.length) fail('list nao aceita argumentos.');
428
+ for (const name of Object.keys(manager.catalog)) {
429
+ const info = manager.info(name);
430
+ console.log(JSON.stringify({ name, group: info.group, platform: info.platform, supported: info.supported, installed: info.installed?.version || null, ...(info.issue ? { issue: info.issue } : {}) }));
431
+ }
432
+ return 0;
433
+ }
434
+ if (mode === 'setup') {
435
+ const selected = focused || args.shift() || 'all'; if (args.length) fail('Argumentos excedentes.');
436
+ const { results, ...summary } = await manager.setup(selected);
437
+ for (const result of results) console.log(JSON.stringify({ event: 'tool', ...result }));
438
+ console.log(JSON.stringify({ event: 'summary', ...summary }));
439
+ return summary.exitCode;
440
+ }
441
+ if (!['install', 'update', 'check', 'info'].includes(mode)) fail(help);
442
+ const selected = focused || args.shift() || 'base'; if (args.length) fail('Argumentos excedentes.');
443
+ const names = selectTools(manager.catalog, selected);
444
+ if (mode === 'install' || mode === 'update') manager.preflight?.(names);
445
+ for (const name of names) {
446
+ if (mode === 'info') { console.log(JSON.stringify(manager.info(name))); continue; }
447
+ const info = await manager[mode](name);
448
+ console.log(`${name}=${info.version}; platform=${info.platform}; ${info.changed ? 'updated' : 'ready'}`);
449
+ }
450
+ return 0;
451
+ }
452
+ if (isMain(import.meta.url)) {
453
+ try { process.exitCode = await main(); }
454
+ catch (error) { console.error(error.message); process.exitCode = 1; }
455
+ }
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>Prova da ferramenta local</title>
6
+ <main>
7
+ <h1>Prova da ferramenta local</h1>
8
+ <form>
9
+ <label>Nome<input name="nome" required></label>
10
+ <button type="submit">Confirmar</button>
11
+ </form>
12
+ <p role="status"></p>
13
+ </main>
14
+ <script>
15
+ document.querySelector('form').onsubmit = event => {
16
+ event.preventDefault();
17
+ document.querySelector('[role="status"]').textContent = `Ola, ${event.target.nome.value}`;
18
+ };
19
+ </script>
20
+ </html>
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { findExecutable, projectRoot, requireNode } from '../../lib/runtime.mjs';
8
+
9
+ requireNode();
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ const tool = join(projectRoot, '.pbai4devs/tools/agent-browser/SCRIPT.mjs');
12
+ const candidates = [process.env.AGENT_BROWSER_EXECUTABLE_PATH,
13
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
14
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
15
+ process.env.PROGRAMFILES && join(process.env.PROGRAMFILES, 'Google/Chrome/Application/chrome.exe'),
16
+ process.env['PROGRAMFILES(X86)'] && join(process.env['PROGRAMFILES(X86)'], 'Google/Chrome/Application/chrome.exe'),
17
+ process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe'),
18
+ process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Chromium/Application/chrome.exe'),
19
+ 'google-chrome', 'chromium', 'chromium-browser'].filter(Boolean);
20
+ const browser = candidates.map(name => findExecutable(name)).find(Boolean);
21
+ if (!browser) { console.error('Navegador ausente; defina AGENT_BROWSER_EXECUTABLE_PATH.'); process.exit(69); }
22
+ const output = join(projectRoot, '.agent-tests/tools-browser');
23
+ mkdirSync(output, { recursive: true });
24
+ const session = `agents-tool-smoke-${process.pid}`;
25
+ const run = (args, file, useSession = true) => {
26
+ const result = spawnSync(process.execPath, [tool, 'run', ...(useSession ? ['--session', session] : []), ...args],
27
+ { cwd: projectRoot, encoding: 'utf8', timeout: 45000, windowsHide: true });
28
+ if (file) writeFileSync(join(output, file), `${result.stdout || ''}${result.stderr || ''}`);
29
+ assert.ifError(result.error);
30
+ assert.equal(result.status, 0, result.stderr || result.stdout);
31
+ return result.stdout.trim();
32
+ };
33
+ const close = () => {
34
+ const result = spawnSync(process.execPath, [tool, 'run', '--session', session, 'close'],
35
+ { encoding: 'utf8', timeout: 10000, windowsHide: true });
36
+ writeFileSync(join(output, 'close.json'), JSON.stringify({ status: result.status, error: result.error?.message, stdout: result.stdout, stderr: result.stderr }, null, 2) + '\n');
37
+ assert.ifError(result.error);
38
+ assert.equal(result.status, 0, 'Falha ao encerrar a sessao do navegador; veja close.json.');
39
+ };
40
+ for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => {
41
+ try { close(); } catch (error) { console.error(error.message); }
42
+ process.exit(signal === 'SIGINT' ? 130 : 143);
43
+ });
44
+ try {
45
+ run(['skills', 'get', 'core'], 'guide.txt', false);
46
+ const fixture = 'data:text/html;charset=utf-8,' + encodeURIComponent(readFileSync(join(here, 'FIXTURE.html'), 'utf8'));
47
+ run(['--executable-path', browser, 'open', fixture], 'open.txt');
48
+ run(['snapshot', '-i'], 'snapshot.txt');
49
+ run(['find', 'label', 'Nome', 'fill', 'Agente'], 'fill.txt');
50
+ run(['find', 'text', 'Confirmar', 'click'], 'click.txt');
51
+ run(['snapshot'], 'result.txt');
52
+ assert.equal(run(['get', 'text', '[role="status"]']), 'Ola, Agente');
53
+ run(['screenshot', join(output, 'tela.png')], 'screenshot.txt');
54
+ } finally { close(); }
55
+ console.log('agent-browser-functional=passed');
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // Abre apenas um Chrome de teste e lê sua janela por acessibilidade.
3
+ import assert from 'node:assert/strict';
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { join } from 'node:path';
7
+ import { findExecutable, projectRoot, requireNode } from '../../lib/runtime.mjs';
8
+
9
+ requireNode();
10
+ const output = join(projectRoot, '.agent-tests/tools-desktop');
11
+ mkdirSync(output, { recursive: true });
12
+ const session = `desktop-smoke-${process.pid}`;
13
+ const title = `Prova desktop ${process.pid}`;
14
+ const application = process.env.AGENT_DESKTOP_TEST_APP || (process.platform === 'darwin' ? 'Google Chrome' : 'chrome');
15
+ const paths = [process.env.AGENT_BROWSER_EXECUTABLE_PATH,
16
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
17
+ process.env.PROGRAMFILES && join(process.env.PROGRAMFILES, 'Google/Chrome/Application/chrome.exe'),
18
+ process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe'),
19
+ 'google-chrome', 'chromium', 'chrome'].filter(Boolean);
20
+ const browserPath = paths.map(path => findExecutable(path)).find(Boolean);
21
+ assert.ok(browserPath, 'Configure AGENT_BROWSER_EXECUTABLE_PATH para um Chrome instalado.');
22
+
23
+ function run(tool, args, log, allowFailure = false) {
24
+ const result = spawnSync(process.execPath, [join(projectRoot, '.pbai4devs/tools', tool, 'SCRIPT.mjs'), 'run', ...args],
25
+ { cwd: projectRoot, encoding: 'utf8', timeout: 30000, windowsHide: true });
26
+ if (log) writeFileSync(join(output, log), JSON.stringify({ status: result.status, error: result.error?.message, stdout: result.stdout, stderr: result.stderr }, null, 2) + '\n');
27
+ assert.ifError(result.error);
28
+ if (!allowFailure) assert.equal(result.status, 0, result.stderr || result.stdout);
29
+ return result.stdout;
30
+ }
31
+
32
+ let passed = false;
33
+ try {
34
+ const html = `<title>${title}</title><h1>Prova controlada</h1><button>Confirmar teste</button>`;
35
+ run('agent-browser', ['--session', session, '--headed', '--args', '--force-renderer-accessibility', '--executable-path', browserPath, 'open', 'data:text/html,' + encodeURIComponent(html)], 'open.json');
36
+ let ownWindow;
37
+ for (let attempt = 0; attempt < 10 && !ownWindow; attempt++) {
38
+ const response = JSON.parse(run('agent-desktop', ['list-windows', '--app', application]));
39
+ assert.equal(response.ok, true);
40
+ const windows = Array.isArray(response.data) ? response.data : response.data.windows || [];
41
+ ownWindow = windows.find(window => JSON.stringify(window).includes(title));
42
+ if (!ownWindow) await new Promise(resolve => setTimeout(resolve, 300));
43
+ }
44
+ assert.ok(ownWindow, 'Janela isolada de teste nao encontrada.');
45
+ const id = ownWindow.id || ownWindow.window_id || ownWindow.windowId;
46
+ assert.ok(id, 'Identificador da janela ausente.');
47
+ writeFileSync(join(output, 'window.json'), JSON.stringify(ownWindow, null, 2) + '\n');
48
+ let result;
49
+ for (let attempt = 0; attempt < 4; attempt++) {
50
+ result = JSON.parse(run('agent-desktop', ['snapshot', '--app', application, '--window-id', String(id), '--max-depth', '20', '--timeout-ms', '15000'], 'snapshot.json', true));
51
+ if (result.ok || result.error?.disposition?.retry !== 'safe') break;
52
+ await new Promise(resolve => setTimeout(resolve, 300));
53
+ }
54
+ assert.equal(result.ok, true);
55
+ assert.match(JSON.stringify(result.data), /Confirmar teste/);
56
+ passed = true;
57
+ } finally {
58
+ run('agent-browser', ['--session', session, 'close'], 'close.json');
59
+ }
60
+ assert.ok(passed);
61
+ console.log('agent-desktop-accessibility=passed; physical-input=none');