@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.
Files changed (242) hide show
  1. package/.pbai4devs/COMPATIBILIDADE.md +109 -0
  2. package/.pbai4devs/INSTRUCTIONS.md +87 -0
  3. package/.pbai4devs/README.md +53 -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 +65 -0
  196. package/CONTRIBUTING.md +73 -0
  197. package/LICENSE +21 -0
  198. package/README.en.md +89 -0
  199. package/README.es.md +91 -0
  200. package/README.md +94 -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 +86 -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 +87 -0
  216. package/docs/product/README.md +12 -0
  217. package/docs/product/design.md +104 -0
  218. package/docs/product/prd.md +70 -0
  219. package/docs/product/roadmap.md +38 -0
  220. package/docs/product/ux-ui.md +42 -0
  221. package/docs/specs/README.md +21 -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/docs/specs/v0.0.6/README.md +10 -0
  234. package/docs/specs/v0.0.6/SPEC.md +77 -0
  235. package/package.json +67 -4
  236. package/src/README.md +16 -0
  237. package/src/adapters/index.mjs +1 -0
  238. package/src/cli.mjs +162 -0
  239. package/src/config.mjs +14 -0
  240. package/src/i18n.mjs +42 -0
  241. package/src/install.mjs +373 -0
  242. package/src/next.mjs +79 -0
@@ -0,0 +1,552 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, readdirSync, existsSync, copyFileSync, symlinkSync, unlinkSync, chmodSync, statSync, renameSync } from 'node:fs';
4
+ import { resolve, join, dirname, delimiter } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { hostname } from 'node:os';
8
+ import { ToolManager, platformKey, validateRelease, validateToolConfig, loadCatalog, main, toolEnvironment, binaryInvocation, toolActions, toolWrapper } from '../SCRIPT.mjs';
9
+ import { requireNode } from '../../lib/runtime.mjs';
10
+ import './bootstrap.test.mjs';
11
+
12
+ const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
13
+ const tool = (name = 'test-tool', group = 'base') => ({ name, description: 'Ferramenta da fixture.', group, source: `https://github.com/owner/${name}`, download: { provider: 'npm', package: name, registry: 'https://registry.npmjs.org/', repository: `owner/${name}`, channel: 'latest' }, platforms: [platformKey()], scripts: { entry: 'SCRIPT.mjs' }, probe: ['--version'] });
14
+ const release = (version, name = 'test-tool') => ({ name, version, repository: { url: `git+https://github.com/owner/${name}.git` }, dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, integrity: 'sha512-fixture' } });
15
+ function createSymlink(t, target, path, type) {
16
+ try { symlinkSync(target, path, type); return true; }
17
+ catch (error) {
18
+ if (!['EPERM', 'EACCES'].includes(error.code)) throw error;
19
+ t.skip('Sistema sem permissao para criar symlinks de teste.');
20
+ return false;
21
+ }
22
+ }
23
+ function packageFiles(prefix, name, version, { tamper = false, broken = false, noBin = false } = {}) {
24
+ const base = join(prefix, 'node_modules', name); mkdirSync(base, { recursive: true });
25
+ writeFileSync(join(base, 'package.json'), JSON.stringify({ name, version, ...(!noBin ? { bin: 'cli.cjs' } : {}) }));
26
+ writeFileSync(join(base, 'cli.cjs'), `#!/usr/bin/env node
27
+ if (${broken}) process.exit(9);
28
+ if (process.argv[2] === 'exit') process.exit(37);
29
+ if (process.argv[2] === 'require-env' && process.env.TOOL_FIXTURE_HOME !== process.argv[3]) process.exit(43);
30
+ if (process.argv[2] === 'require-literal' && process.env.DISABLE_TELEMETRY !== process.argv[3]) process.exit(44);
31
+ if (process.argv[2] === 'require-runtime' && process.env.PATH.split(require('node:path').delimiter)[0] !== process.argv[3]) process.exit(45);
32
+ if (process.argv[2] === 'echo') console.log(JSON.stringify(process.argv.slice(3)));
33
+ else if (process.argv[2] === 'env') console.log(JSON.stringify({state:process.env.TOOL_FIXTURE_HOME,keep:process.env.TOOL_FIXTURE_KEEP}));
34
+ else console.log(${JSON.stringify(version)});
35
+ `);
36
+ writeFileSync(join(prefix, 'package-lock.json'), JSON.stringify({ packages: { [`node_modules/${name}`]: { integrity: tamper ? 'sha512-divergente' : 'sha512-fixture' } } }));
37
+ }
38
+ function fixture(t, catalog = { 'test-tool': tool() }, options = {}) {
39
+ mkdirSync(join(project, '.agent-tests'), { recursive: true });
40
+ const root = mkdtempSync(join(project, '.agent-tests/tools com espaço-'));
41
+ t.after(() => rmSync(root, { recursive: true, force: true }));
42
+ for (const [name, config] of Object.entries(catalog)) { const folder = join(root, '.pbai4devs/tools', name); mkdirSync(folder, { recursive: true }); writeFileSync(join(folder, 'TOOL.json'), JSON.stringify(config)); writeFileSync(join(folder, 'SCRIPT.mjs'), toolWrapper(name)); }
43
+ const common = join(root, '.pbai4devs/scripts/preparar-ferramentas'); mkdirSync(common, { recursive: true });
44
+ copyFileSync(join(project, '.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs'), join(common, 'SCRIPT.mjs'));
45
+ mkdirSync(join(root, '.pbai4devs/scripts/lib'), { recursive: true });
46
+ copyFileSync(join(project, '.pbai4devs/scripts/lib/runtime.mjs'), join(root, '.pbai4devs/scripts/lib/runtime.mjs'));
47
+ const state = { version: '1.0.0', installs: 0, fetches: 0, fail: false, tamper: false, broken: false, noBin: false };
48
+ const manager = new ToolManager({ root, catalog, fetchRelease: async name => { state.fetches++; return release(state.version, name); }, install(prefix, name, version) { state.installs++; if (state.fail) throw new Error('download de prova falhou'); packageFiles(prefix, name, version, state); }, ...options });
49
+ return { root, manager, state, local: join(root, '.pbai4devs/tools/test-tool/.local') };
50
+ }
51
+
52
+ function setupNpm(root, { version = '10.0.0', exitCode = 0 } = {}) {
53
+ const path = join(root, 'fake-npm-check.cjs');
54
+ writeFileSync(path, `if(process.execPath !== ${JSON.stringify(process.execPath)} || process.argv[2] !== '--version') process.exit(71); console.log(${JSON.stringify(version)}); process.exit(${exitCode});\n`);
55
+ return path;
56
+ }
57
+
58
+ // Descoberta e validacao acontecem antes de qualquer instalacao ou execucao.
59
+ test('TOOL.json e a origem unica; configuracoes invalidas e caminhos perigosos falham', t => {
60
+ const { root } = fixture(t); assert.deepEqual(Object.keys(loadCatalog(root)), ['test-tool']);
61
+ assert.equal(loadCatalog(root)['test-tool'].download.channel, 'latest');
62
+ assert.throws(() => validateToolConfig('../outside', tool()), /Nome/);
63
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), download: { ...tool().download, registry: 'https://evil.example/' } }), /Download/);
64
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), scripts: { entry: '../SCRIPT.mjs' } }), /Script/);
65
+ writeFileSync(join(root, '.pbai4devs/tools/test-tool/TOOL.json'), '{invalid'); assert.throws(() => loadCatalog(root), SyntaxError);
66
+ });
67
+
68
+ test('metadados incompletos nao se tornam ferramentas executaveis', () => {
69
+ for (const change of [{ description: '' }, { description: undefined }, { source: undefined }, { source: 'file:///tmp/tool' }, { group: '' }, { platforms: [] }, { platforms: [platformKey(), platformKey()] }, { probe: [] }, { probe: ['bad\0arg'] }, { binary: '../tool' }]) {
70
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), ...change }));
71
+ }
72
+ });
73
+
74
+ test('catalogo exige ponte real da ferramenta correta e ignora apenas estado conhecido', async t => {
75
+ for (const scenario of ['missing-script', 'directory-script', 'wrong-tool', 'missing-config']) await t.test(scenario, subtest => {
76
+ const { root } = fixture(subtest); const folder = join(root, '.pbai4devs/tools/test-tool');
77
+ if (scenario === 'missing-config') rmSync(join(folder, 'TOOL.json'));
78
+ else {
79
+ rmSync(join(folder, 'SCRIPT.mjs'));
80
+ if (scenario === 'directory-script') mkdirSync(join(folder, 'SCRIPT.mjs'));
81
+ if (scenario === 'wrong-tool') writeFileSync(join(folder, 'SCRIPT.mjs'), toolWrapper('other-tool'));
82
+ }
83
+ assert.throws(() => loadCatalog(root), /ausente|invalido|divergente/);
84
+ });
85
+ await t.test('state', subtest => {
86
+ const { root } = fixture(subtest);
87
+ for (const folder of ['bin', 'npm-cache', '.cache']) { const path = join(root, '.pbai4devs/tools', folder); mkdirSync(path); writeFileSync(join(path, 'TOOL.json'), 'invalid'); }
88
+ assert.deepEqual(Object.keys(loadCatalog(root)), ['test-tool']);
89
+ });
90
+ });
91
+
92
+ test('ponte simbolica de ferramenta nao escapa da origem', t => {
93
+ const { root } = fixture(t); const script = join(root, '.pbai4devs/tools/test-tool/SCRIPT.mjs'); rmSync(script);
94
+ if (!createSymlink(t, join(root, 'outside.mjs'), script)) return;
95
+ assert.throws(() => loadCatalog(root), /nao pode ser um link/);
96
+ });
97
+
98
+ test('acoes comuns apontam para a ponte executavel e usam check para saude local', async t => {
99
+ const { root, manager, state } = fixture(t); await manager.install('test-tool');
100
+ const actions = toolActions('test-tool', tool());
101
+ assert.deepEqual(Object.keys(actions), ['install', 'update', 'check', 'info', 'run', 'unlock']);
102
+ assert.equal(actions.run.requiresArguments, true);
103
+ for (const action of ['check', 'info']) {
104
+ const [runtime, ...args] = actions[action].argv;
105
+ assert.equal(runtime, 'node'); assert.equal(actions[action].executor, 'node');
106
+ const result = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8' });
107
+ assert.equal(result.status, 0, result.stderr);
108
+ if (action === 'info') assert.equal(JSON.parse(result.stdout).installed.version, '1.0.0');
109
+ }
110
+ assert.equal(state.installs, 1); assert.equal(state.fetches, 1);
111
+ });
112
+
113
+ test('pacote sem binario nao deixa uma instalacao marcada como pronta', async t => {
114
+ const { manager, state, local } = fixture(t); state.noBin = true;
115
+ await assert.rejects(manager.install('test-tool'), /Executavel ausente/);
116
+ assert.equal(existsSync(join(local, 'CURRENT.json')), false);
117
+ assert.equal(manager.info('test-tool').installed, null);
118
+ assert.throws(() => manager.run('test-tool', ['--version']), /ausente/);
119
+ });
120
+
121
+ test('plataforma, pacote e origem oficiais sao conferidos antes de instalar', async t => {
122
+ const { manager, state } = fixture(t);
123
+ assert.throws(() => platformKey('win32', 'ia32'), /nao suportado/);
124
+ assert.throws(() => validateRelease('test-tool', tool(), { ...release('1.0.0'), repository: 'https://evilgithub.com/owner/test-tool' }), /origem divergente/);
125
+ assert.throws(() => validateRelease('test-tool', tool(), { ...release('1.0.0'), name: 'different' }), /Nome/);
126
+ assert.throws(() => validateRelease('test-tool', tool(), { ...release('1.0.0'), dist: { ...release('1.0.0').dist, tarball: 'https://evil.example/tool.tgz' } }), /Tarball/);
127
+ assert.throws(() => manager.check('__proto__'), /desconhecida/);
128
+ manager.catalog['test-tool'] = { ...tool(), platforms: [process.platform === 'darwin' ? 'linux-x64' : 'darwin-arm64'] };
129
+ await assert.rejects(manager.install('test-tool'), /Plataforma nao suportada/); assert.equal(state.fetches, 0);
130
+ });
131
+
132
+ test('install reutiliza sem rede; check, info e run nao instalam; argv e exit code preservados', async t => {
133
+ const { root, manager, state, local } = fixture(t);
134
+ assert.throws(() => manager.check('test-tool'), /ausente/); assert.equal(existsSync(local), false);
135
+ assert.equal(manager.info('test-tool').installed, null); assert.equal(existsSync(local), false);
136
+ assert.equal((await manager.install('test-tool')).changed, true); assert.equal(state.fetches, 1); assert.equal(state.installs, 1);
137
+ const snapshot = readFileSync(join(local, 'CURRENT.json'), 'utf8');
138
+ manager.fetchRelease = async () => { throw new Error('Nao deve consultar a rede'); };
139
+ assert.equal((await manager.install('test-tool')).changed, false);
140
+ assert.equal(manager.check('test-tool').version, '1.0.0'); assert.equal(manager.info('test-tool').installed.layout, 'local');
141
+ assert.equal(manager.run('test-tool', ['exit']), 37);
142
+ assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), snapshot); assert.equal(state.installs, 1);
143
+ const arg = 'valor com espaço;$(nao execute)';
144
+ const cli = spawnSync(process.execPath, [join(root, '.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs'), '--tool', 'test-tool', 'run', 'echo', arg], { encoding: 'utf8' });
145
+ assert.equal(cli.status, 0, cli.stderr); assert.deepEqual(JSON.parse(cli.stdout), [arg]);
146
+ const launcher = join(root, '.pbai4devs/tools/bin/test-tool.mjs');
147
+ assert.ok(existsSync(launcher)); assert.equal(existsSync(join(root, '.pbai4devs/tools/bin/test-tool.cmd')), false);
148
+ const viaLauncher = spawnSync(process.execPath, [launcher, 'echo', arg], { encoding: 'utf8' });
149
+ assert.equal(viaLauncher.status, 0, viaLauncher.stderr); assert.deepEqual(JSON.parse(viaLauncher.stdout), [arg]);
150
+ const viaWrapper = spawnSync(process.execPath, [join(root, '.pbai4devs/tools/test-tool/SCRIPT.mjs'), 'run', 'echo', arg], { encoding: 'utf8' });
151
+ assert.equal(viaWrapper.status, 0, viaWrapper.stderr); assert.deepEqual(JSON.parse(viaWrapper.stdout), [arg]);
152
+ });
153
+
154
+ test('launchers recusam links de diretorio, arquivo e destino ausente sem escrever fora', async t => {
155
+ for (const kind of ['directory', 'file', 'dangling']) await t.test(kind, subtest => {
156
+ const { root, manager } = fixture(subtest);
157
+ const bin = join(manager.home, 'bin'), outside = join(root, 'outside');
158
+ if (kind === 'directory') {
159
+ mkdirSync(outside);
160
+ if (!createSymlink(subtest, outside, bin, 'dir')) return;
161
+ } else {
162
+ mkdirSync(bin);
163
+ if (kind === 'file') writeFileSync(outside, 'preservar');
164
+ if (!createSymlink(subtest, outside, join(bin, 'test-tool.mjs'), 'file')) return;
165
+ }
166
+ assert.throws(() => manager.launchers('test-tool'), /nao pode ser um link/);
167
+ if (kind === 'directory') assert.deepEqual(readdirSync(outside), []);
168
+ else if (kind === 'file') assert.equal(readFileSync(outside, 'utf8'), 'preservar');
169
+ else assert.equal(existsSync(outside), false);
170
+ });
171
+ });
172
+
173
+ test('unlock recupera somente lock local com PID comprovadamente morto', async t => {
174
+ const { manager, local, state } = fixture(t);
175
+ assert.deepEqual(manager.unlock('test-tool'), { name: 'test-tool', removed: false, state: 'missing' });
176
+ assert.equal(existsSync(local), false);
177
+ const child = spawnSync(process.execPath, ['-e', ''], { encoding: 'utf8' }); assert.equal(child.status, 0);
178
+ const lock = join(local, '.update-lock'); mkdirSync(lock, { recursive: true });
179
+ writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: child.pid, hostname: hostname() }));
180
+ assert.equal(manager.lockStatus('test-tool').state, 'stale');
181
+ await assert.rejects(manager.withLock('test-tool', async () => true), /Lock orfao/);
182
+ assert.deepEqual(manager.unlock('test-tool'), { name: 'test-tool', removed: true, state: 'stale' });
183
+ assert.equal(existsSync(lock), false);
184
+ await manager.withLock('test-tool', async () => assert.equal(JSON.parse(readFileSync(join(lock, 'owner.json'))).hostname, hostname()));
185
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
186
+ await assert.rejects(main(['unlock', 'all'], manager), /desconhecida/);
187
+ await assert.rejects(main(['unlock', 'test-tool', 'extra'], manager), /unica ferramenta/);
188
+ });
189
+
190
+ test('unlock preserva locks ativos, incertos e recuperacao concorrente', async t => {
191
+ for (const scenario of ['active', 'legacy', 'remote', 'invalid', 'recovering']) await t.test(scenario, subtest => {
192
+ const { manager, local } = fixture(subtest); const lock = join(local, '.update-lock'); mkdirSync(lock, { recursive: true });
193
+ const owner = scenario === 'legacy' ? { pid: 2147483647 } : { pid: process.pid, hostname: scenario === 'remote' ? `${hostname()}-outro` : hostname() };
194
+ const content = scenario === 'invalid' ? '{invalido' : JSON.stringify(owner); writeFileSync(join(lock, 'owner.json'), content);
195
+ if (scenario === 'recovering') writeFileSync(join(lock, 'recovery.json'), 'outro recuperador');
196
+ assert.throws(() => manager.unlock('test-tool'), scenario === 'recovering' ? /EEXIST/ : /Lock active|Lock indeterminate/);
197
+ assert.equal(readFileSync(join(lock, 'owner.json'), 'utf8'), content);
198
+ assert.equal(existsSync(join(lock, 'recovery.json')), scenario === 'recovering');
199
+ });
200
+ });
201
+
202
+ test('unlock detecta troca de dono antes de remover o lock', t => {
203
+ const { manager, local } = fixture(t); const lock = join(local, '.update-lock'); mkdirSync(lock, { recursive: true });
204
+ writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: 2147483647, hostname: hostname() }));
205
+ const replacement = JSON.stringify({ pid: process.pid, hostname: hostname() });
206
+ manager.lockStatus = () => { writeFileSync(join(lock, 'owner.json'), replacement); return { state: 'stale', lock }; };
207
+ assert.throws(() => manager.unlock('test-tool'), /mudou durante/);
208
+ assert.equal(readFileSync(join(lock, 'owner.json'), 'utf8'), replacement);
209
+ assert.equal(existsSync(join(lock, 'recovery.json')), false);
210
+ });
211
+
212
+ test('update consulta latest; falhas de download, integridade e probe preservam instalacao', async t => {
213
+ const { manager, state, local } = fixture(t); await manager.install('test-tool');
214
+ assert.equal((await manager.update('test-tool')).changed, false); assert.equal(state.fetches, 2); assert.equal(state.installs, 1);
215
+ const before = readFileSync(join(local, 'CURRENT.json'), 'utf8'); state.version = '2.0.0'; state.fail = true;
216
+ await assert.rejects(manager.update('test-tool'), /download de prova/); state.fail = false; state.tamper = true;
217
+ await assert.rejects(manager.update('test-tool'), /nao corresponde/); state.tamper = false; state.broken = true;
218
+ await assert.rejects(manager.update('test-tool'), /test-tool/); state.broken = false; state.noBin = true;
219
+ await assert.rejects(manager.update('test-tool'), /Executavel ausente/); state.noBin = false;
220
+ assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), before); assert.equal(manager.check('test-tool').version, '1.0.0');
221
+ assert.ok(readdirSync(local).every(name => !name.startsWith('.stage-') && (name === 'CURRENT.json' || !name.startsWith('CURRENT.')) && name !== '.update-lock'));
222
+ assert.equal((await manager.update('test-tool')).version, '2.0.0'); assert.equal(manager.check('test-tool').version, '2.0.0');
223
+ });
224
+
225
+ test('falha ao gravar CURRENT restaura o target anterior e o pacote em uso', async t => {
226
+ const { manager, state, local } = fixture(t); await manager.install('test-tool');
227
+ const before = readFileSync(join(local, 'CURRENT.json'), 'utf8'); state.version = '2.0.0';
228
+ const cache = join(local, 'versions', `2.0.0-${platformKey()}`); packageFiles(cache, 'test-tool', '2.0.0'); writeFileSync(join(cache, 'preserve.txt'), 'cached');
229
+ manager.commitState = () => { throw new Error('CURRENT indisponivel'); };
230
+ await assert.rejects(manager.update('test-tool'), /CURRENT indisponivel/);
231
+ assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), before); assert.equal(readFileSync(join(cache, 'preserve.txt'), 'utf8'), 'cached');
232
+ assert.equal(manager.check('test-tool').version, '1.0.0'); assert.ok(!existsSync(join(local, '.update-lock')));
233
+ });
234
+
235
+ test('duas atualizacoes nao disputam a mesma ferramenta nem apagam lock alheio', async t => {
236
+ const { manager, state, local } = fixture(t); let enter; let proceed;
237
+ const entered = new Promise(resolve => { enter = resolve; });
238
+ const allowed = new Promise(resolve => { proceed = resolve; });
239
+ manager.fetchRelease = async () => { state.fetches++; enter(); await allowed; return release('1.0.0'); };
240
+ const first = manager.update('test-tool'); await entered;
241
+ await assert.rejects(manager.update('test-tool'), /em andamento/); assert.ok(existsSync(join(local, '.update-lock')));
242
+ proceed(); await first; assert.equal(state.fetches, 1); assert.equal(state.installs, 1); assert.ok(!existsSync(join(local, '.update-lock')));
243
+ });
244
+
245
+ test('migracao move instalacao e cache antigos sem rede nem novo download', async t => {
246
+ const { root, manager, state, local } = fixture(t);
247
+ const legacy = join(root, '.pbai4devs/tools/test-tool'); const prefix = join(legacy, 'versions', `1.0.0-${platformKey()}`);
248
+ packageFiles(prefix, 'test-tool', '1.0.0');
249
+ const metadata = JSON.stringify({ name: 'test-tool', version: '1.0.0', platform: platformKey() })+'\n'; writeFileSync(join(legacy, 'CURRENT.json'), metadata);
250
+ const oldCache = join(root, '.pbai4devs/tools/npm-cache'); mkdirSync(oldCache); writeFileSync(join(oldCache, 'cache-marker'), 'preserved');
251
+ manager.fetchRelease = async () => { throw new Error('Migracao nao deve usar rede'); };
252
+ assert.equal(manager.check('test-tool').layout, 'legacy'); assert.equal(existsSync(local), false);
253
+ await manager.install('test-tool'); assert.equal(manager.check('test-tool').layout, 'local');
254
+ assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), metadata); assert.equal(existsSync(join(legacy, 'CURRENT.json')), false); assert.equal(existsSync(join(legacy, 'versions')), false);
255
+ assert.equal(readFileSync(join(root, '.pbai4devs/tools/.cache/npm/cache-marker'), 'utf8'), 'preserved'); assert.equal(state.installs, 0); assert.equal(state.fetches, 0);
256
+ });
257
+
258
+ test('rename do hub preserva pacote e caches e setup regenera launcher sem rede', async t => {
259
+ const { root, manager, state, local } = fixture(t);
260
+ await manager.install('test-tool');
261
+ const hub = join(root, '.pbai4devs'); const oldHub = join(root, '.agents');
262
+ const receipt = readFileSync(join(local, 'CURRENT.json'), 'utf8');
263
+ const packageFile = join(manager.current('test-tool').prefix, 'node_modules/test-tool/cli.cjs');
264
+ const packageBefore = readFileSync(packageFile, 'utf8');
265
+ for (const kind of ['npm', 'runtime/node/test-platform']) {
266
+ const folder = join(hub, 'tools/.cache', kind); mkdirSync(folder, { recursive: true });
267
+ writeFileSync(join(folder, 'preserved.txt'), kind);
268
+ }
269
+ renameSync(hub, oldHub);
270
+ const launcher = join(oldHub, 'tools/bin/test-tool.mjs');
271
+ writeFileSync(launcher, `import { main } from ${JSON.stringify(pathToFileURL(join(oldHub, 'scripts/preparar-ferramentas/SCRIPT.mjs')).href)};\nawait main(['run', 'test-tool', ...process.argv.slice(2)]);\n`);
272
+ renameSync(oldHub, hub);
273
+ assert.notEqual(spawnSync(process.execPath, [join(hub, 'tools/bin/test-tool.mjs'), '--version'], { encoding: 'utf8' }).status, 0);
274
+ const repaired = new ToolManager({ root, npm: () => setupNpm(root),
275
+ fetchRelease: async () => { throw new Error('Rename nao deve consultar o registro'); },
276
+ install: () => { throw new Error('Rename nao deve reinstalar pacote'); } });
277
+ const result = await repaired.setup('all');
278
+ assert.equal(result.status, 'passed'); assert.equal(result.results[0].changed, false);
279
+ assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), receipt);
280
+ assert.equal(readFileSync(packageFile, 'utf8'), packageBefore);
281
+ assert.equal(state.installs, 1); assert.equal(state.fetches, 1);
282
+ for (const kind of ['npm', 'runtime/node/test-platform']) assert.equal(readFileSync(join(hub, 'tools/.cache', kind, 'preserved.txt'), 'utf8'), kind);
283
+ const rewritten = join(hub, 'tools/bin/test-tool.mjs');
284
+ assert.doesNotMatch(readFileSync(rewritten, 'utf8'), /\/\.agents\//);
285
+ const proof = spawnSync(process.execPath, [rewritten, 'echo', 'hub migrado'], { encoding: 'utf8' });
286
+ assert.equal(proof.status, 0, proof.stderr); assert.deepEqual(JSON.parse(proof.stdout), ['hub migrado']);
287
+ assert.equal(existsSync(oldHub), false);
288
+ const moved = `${root}-movido`; renameSync(root, moved);
289
+ t.after(() => rmSync(moved, { recursive: true, force: true }));
290
+ const relocated = spawnSync(process.execPath, [join(moved, '.pbai4devs/tools/bin/test-tool.mjs'), 'echo', 'repositorio movido'], { encoding: 'utf8' });
291
+ assert.equal(relocated.status, 0, relocated.stderr); assert.deepEqual(JSON.parse(relocated.stdout), ['repositorio movido']);
292
+ });
293
+
294
+ test('symlink de executavel para fora da instalacao e layout conflitante sao recusados', async t => {
295
+ const { root, manager, local } = fixture(t); await manager.install('test-tool');
296
+ writeFileSync(join(root, '.pbai4devs/tools/test-tool/CURRENT.json'), JSON.stringify({ name: 'test-tool', version: '0.1.0', platform: platformKey() }));
297
+ mkdirSync(join(root, '.pbai4devs/tools/test-tool/versions')); await assert.rejects(manager.install('test-tool'), /coexistem/); assert.ok(existsSync(join(local, 'CURRENT.json')));
298
+ await t.test('executavel simbolico externo', subtest => {
299
+ const info = manager.current('test-tool'); const binary = join(info.prefix, 'node_modules/test-tool/cli.cjs'); rmSync(binary);
300
+ if (!createSymlink(subtest, process.execPath, binary)) return;
301
+ assert.throws(() => manager.check('test-tool'), /fora da instalacao/);
302
+ });
303
+ });
304
+
305
+ test('envPaths isola estado em probes e run sem trocar ambiente global ou permitir escapes', async t => {
306
+ for (const key of ['HOME', 'home', 'CODEX_HOME', 'Path', 'INVALID-NAME']) assert.throws(() => validateToolConfig('test-tool', { ...tool(), envPaths: { [key]: 'state' } }), /Variavel/);
307
+ for (const location of ['', '.', '..', '../outside', '/tmp/outside', 'state/../outside', 'C:\\outside', 'C:outside', 'state\\outside']) assert.throws(() => validateToolConfig('test-tool', { ...tool(), envPaths: { TOOL_FIXTURE_HOME: location } }), /relativo/);
308
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), envPaths: [] }), /envPaths/);
309
+ const config = { ...tool(), envPaths: { TOOL_FIXTURE_HOME: 'state' } };
310
+ const { root, manager, local } = fixture(t, { 'test-tool': config });
311
+ const expected = join(local, 'state'); const original = process.env.TOOL_FIXTURE_HOME;
312
+ const environment = toolEnvironment(config, local);
313
+ assert.equal(environment.TOOL_FIXTURE_HOME, expected); assert.equal(environment.HOME, process.env.HOME);
314
+ const pathKey = Object.keys(environment).find(key => key.toUpperCase() === 'PATH');
315
+ assert.ok(pathKey, 'The inherited PATH must remain present.');
316
+ assert.equal(environment[pathKey], process.env.PATH);
317
+ assert.equal(process.env.TOOL_FIXTURE_HOME, original);
318
+ manager.catalog['test-tool'].probe = ['require-env', expected];
319
+ await manager.install('test-tool'); assert.equal(manager.check('test-tool').version, '1.0.0');
320
+ const result = spawnSync(process.execPath, [join(root, '.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs'), '--tool', 'test-tool', 'run', 'env'], { encoding: 'utf8', env: { ...process.env, TOOL_FIXTURE_HOME: 'global-default', TOOL_FIXTURE_KEEP: 'untouched' } });
321
+ assert.equal(result.status, 0, result.stderr); assert.deepEqual(JSON.parse(result.stdout), { state: expected, keep: 'untouched' });
322
+ assert.equal(process.env.TOOL_FIXTURE_HOME, original); assert.ok(!existsSync(expected));
323
+ for (const [name, target] of [['diretorio simbolico', root], ['link simbolico quebrado', join(root, 'missing-target')]]) {
324
+ await t.test(name, subtest => {
325
+ if (!createSymlink(subtest, target, expected, 'dir')) return;
326
+ subtest.after(() => unlinkSync(expected));
327
+ assert.throws(() => toolEnvironment(config, local), /nao pode ser um link/);
328
+ });
329
+ }
330
+ });
331
+
332
+ test('ajuda focada e listagem concisa nao criam estado nem despejam configuracao', t => {
333
+ const { root, local } = fixture(t);
334
+ const script = join(root, '.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs');
335
+ const help = spawnSync(process.execPath, [script, '--tool', 'test-tool'], { encoding: 'utf8' });
336
+ assert.equal(help.status, 0); assert.match(help.stdout, /^test-tool:/); assert.doesNotMatch(help.stdout, /base\|all|--tool/);
337
+ const list = spawnSync(process.execPath, [script, 'list'], { encoding: 'utf8' });
338
+ assert.equal(list.status, 0); const entry = JSON.parse(list.stdout);
339
+ assert.deepEqual(Object.keys(entry).sort(), ['group', 'installed', 'issue', 'name', 'platform', 'supported']);
340
+ assert.equal(entry.installed, null); assert.equal(existsSync(local), false);
341
+ });
342
+
343
+ test('interface simples, aliases e --tool roteiam sem executar atualizacao implicita', async () => {
344
+ const seen = []; const info = name => ({ name, version: '1.0.0', platform: platformKey() });
345
+ const manager = { catalog: { base: { group: 'base' }, desktop: { group: 'desktop' } }, entry(name) { if(!Object.hasOwn(this.catalog,name)) throw new Error('unknown'); }, info, run(name,args) { seen.push(['run',name,...args]);return 37; }, async install(name){seen.push(['install',name]);return info(name);},async update(name){seen.push(['update',name]);return info(name);},check(name){seen.push(['check',name]);return info(name);} };
346
+ await main([],manager); await main(['--tool','base'],manager); assert.deepEqual(seen,[]);
347
+ await main(['install','base'],manager); await main(['--update','all'],manager); await main(['--tool','desktop','check'],manager); await main(['--check','base'],manager);
348
+ assert.deepEqual(seen,[['install','base'],['update','base'],['update','desktop'],['check','desktop'],['check','base']]);
349
+ assert.equal(await main(['--tool','desktop','run','echo','a b'],manager),37); assert.deepEqual(seen.at(-1),['run','desktop','echo','a b']);
350
+ assert.equal(await main(['--run','base','exit'],manager),37);
351
+ await assert.rejects(main(['--tool','base','update','extra'],manager),/excedentes/);
352
+ await assert.rejects(main(['unknown'],manager),/Uso/);
353
+ });
354
+
355
+ test('env literal aceita apenas strings e variaveis nao protegidas', () => {
356
+ assert.equal(validateToolConfig('test-tool', { ...tool(), env: { DISABLE_TELEMETRY: '1', EMPTY_VALUE: '' } }).env.EMPTY_VALUE, '');
357
+ for (const key of ['HOME', 'home', 'CODEX_HOME', 'Path', 'INVALID-NAME']) assert.throws(() => validateToolConfig('test-tool', { ...tool(), env: { [key]: 'literal' } }), /Variavel/);
358
+ for (const value of [null, 1, false, {}, [], 'bad\0value']) assert.throws(() => validateToolConfig('test-tool', { ...tool(), env: { TEST_VALUE: value } }), /Valor/);
359
+ for (const value of [null, [], 'value']) assert.throws(() => validateToolConfig('test-tool', { ...tool(), env: value }), /env invalido/);
360
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), env: { STATE: 'literal' }, envPaths: { state: 'state' } }), /duplicada/);
361
+ });
362
+
363
+ test('Node 24 e o unico runtime; versoes anteriores falham antes de alterar arquivos', async t => {
364
+ assert.doesNotThrow(() => requireNode(24, '24.0.0'));
365
+ assert.doesNotThrow(() => requireNode(24, '26.13.0'));
366
+ for (const version of ['22.22.0', '20.19.0', '', 'invalid']) assert.throws(() => requireNode(24, version), /Node.js >=24/);
367
+ const { manager, state, local } = fixture(t, { 'test-tool': tool() }, { nodeVersion: '22.22.0' });
368
+ assert.equal(manager.info('test-tool').supported, false);
369
+ await assert.rejects(manager.install('test-tool'), /Node.js >=24/);
370
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0); assert.equal(existsSync(local), false);
371
+ });
372
+
373
+ test('definicoes usam um SCRIPT.mjs e rejeitam wrappers e runtimes antigos', () => {
374
+ assert.equal(validateToolConfig('test-tool', tool()).scripts.entry, 'SCRIPT.mjs');
375
+ for (const scripts of [{ entry: 'SCRIPT.sh' }, { posix: 'SCRIPT.sh', windows: 'SCRIPT.cmd', powershell: 'SCRIPT.ps1' }, { entry: 'SCRIPT.mjs', windows: 'SCRIPT.cmd' }]) {
376
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), scripts }), /Script invalido/);
377
+ }
378
+ assert.throws(() => validateToolConfig('test-tool', { ...tool(), nodeMinimum: '22.0.0' }), /requisito comum/);
379
+ });
380
+
381
+ test('list e info reportam ferramentas incompativeis e preflight de all impede alteracoes parciais', async t => {
382
+ const cases = [{ ...tool('unsupported-tool'), platforms: [process.platform === 'darwin' ? 'linux-x64' : 'darwin-arm64'] }];
383
+ for (const unsupported of cases) {
384
+ const catalog = { 'test-tool': tool(), 'unsupported-tool': unsupported };
385
+ const { manager, state, root, local } = fixture(t, catalog);
386
+ const info = manager.info('unsupported-tool'); assert.equal(info.supported, false); assert.equal(info.runtime, null); assert.match(info.issue, /nao suportada|nao encontrado/);
387
+ assert.equal(manager.info('test-tool').supported, true);
388
+ await main(['list'], manager); await main(['info', 'all'], manager);
389
+ for (const mode of ['install', 'update']) await assert.rejects(main([mode, 'all'], manager), /nenhuma ferramenta foi alterada/);
390
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0); assert.equal(existsSync(local), false);
391
+ assert.equal(existsSync(join(root, '.pbai4devs/tools/unsupported-tool/.local')), false);
392
+ }
393
+ });
394
+
395
+ test('grupos gerais selecionam suas ferramentas e nomes explicitos mantem precedencia', async t => {
396
+ const catalog = { 'test-tool': tool(), 'source-a': tool('source-a', 'source'), 'source-b': tool('source-b', 'source') };
397
+ const { manager, state } = fixture(t, catalog);
398
+ await main(['install', 'source'], manager); assert.equal(state.installs, 2); assert.equal(state.fetches, 2);
399
+ assert.equal(manager.info('test-tool').installed, null);
400
+ await main(['check', 'source'], manager); assert.equal(state.installs, 2); assert.equal(state.fetches, 2);
401
+ manager.catalog.source = tool('source', 'base');
402
+ await main(['install', 'source'], manager); assert.equal(state.installs, 3);
403
+ assert.equal(manager.current('source').version, '1.0.0');
404
+ });
405
+
406
+ test('env literal chega a probes e run; PATH selecionado e estado ficam restritos ao subprocesso', async t => {
407
+ const original = { literal: process.env.DISABLE_TELEMETRY, path: process.env.PATH };
408
+ const config = { ...tool(), env: { DISABLE_TELEMETRY: '1' }, probe: ['require-literal', '1'] };
409
+ const { manager, local } = fixture(t, { 'test-tool': config });
410
+ await manager.install('test-tool'); assert.equal(manager.check('test-tool').version, '1.0.0');
411
+ assert.equal(manager.run('test-tool', ['require-literal', '1']), 0);
412
+ const runtime = manager.runtime('test-tool');
413
+ assert.equal(toolEnvironment(config, local, runtime).PATH.split(delimiter)[0], dirname(runtime.file));
414
+ assert.equal(manager.run('test-tool', ['require-runtime', dirname(runtime.file)]), 0);
415
+ assert.equal(process.env.DISABLE_TELEMETRY, original.literal); assert.equal(process.env.PATH, original.path);
416
+ });
417
+
418
+ test('npm e CLI usam Node atual, sem scripts de instalacao ou npm real', async t => {
419
+ const config = { ...tool(), env: { DISABLE_TELEMETRY: '1' }, probe: ['require-literal', '1'] };
420
+ const { root, local } = fixture(t, { 'test-tool': config });
421
+ const npm = join(root, 'fake-npm/npm-cli.cjs'); mkdirSync(dirname(npm), { recursive: true });
422
+ writeFileSync(npm, `const { mkdirSync, writeFileSync } = require('node:fs');
423
+ const { join } = require('node:path');
424
+ ${packageFiles.toString()}
425
+ const args = process.argv.slice(2); const prefix = args[args.indexOf('--prefix') + 1];
426
+ packageFiles(prefix, 'test-tool', '1.0.0');
427
+ writeFileSync(join(prefix, 'npm-proof.json'), JSON.stringify({args,path:process.env.PATH,literal:process.env.DISABLE_TELEMETRY,strict:process.env.npm_config_engine_strict}));
428
+ `);
429
+ const manager = new ToolManager({ root, catalog: { 'test-tool': config }, npm: () => npm, fetchRelease: async () => release('1.0.0') });
430
+ const installed = await manager.install('test-tool'); const proof = JSON.parse(readFileSync(join(installed.prefix, 'npm-proof.json'), 'utf8'));
431
+ assert.ok(proof.args.includes('--ignore-scripts')); assert.ok(proof.args.includes('--omit=dev')); assert.equal(proof.strict, 'true'); assert.equal(proof.literal, '1');
432
+ assert.equal(proof.path.split(delimiter)[0], dirname(process.execPath));
433
+ assert.equal(binaryInvocation(installed.prefix, 'test-tool')[0], process.execPath);
434
+ assert.equal(installed.nodePath, process.execPath); assert.equal(installed.node, process.version);
435
+ assert.equal(manager.check('test-tool').version, '1.0.0'); assert.equal(manager.run('test-tool', ['require-runtime', dirname(process.execPath)]), 0);
436
+ assert.equal(JSON.parse(readFileSync(join(local, 'CURRENT.json'), 'utf8')).nodePath, process.execPath);
437
+ });
438
+
439
+ test('setup padrao prepara todas as ferramentas compativeis e confirma cada instalacao', async t => {
440
+ const unsupported = { ...tool('unsupported-tool'), platforms: [process.platform === 'darwin' ? 'linux-x64' : 'darwin-arm64'] };
441
+ const { root, manager, state, local } = fixture(t, { 'test-tool': tool(), 'unsupported-tool': unsupported });
442
+ manager.npm = () => setupNpm(root);
443
+ let installed = false; let checksAfterInstall = 0;
444
+ const install = manager.install.bind(manager); const check = manager.check.bind(manager);
445
+ manager.install = async name => { const result = await install(name); installed = true; return result; };
446
+ manager.check = name => { if (installed) checksAfterInstall++; return check(name); };
447
+ const result = await manager.setup();
448
+ assert.equal(result.selection, 'all'); assert.equal(result.exitCode, 0); assert.equal(result.status, 'passed');
449
+ assert.equal(result.nodePath, process.execPath);
450
+ assert.deepEqual(result.summary, { passed: 1, failed: 0, notApplicable: 1 });
451
+ assert.equal(result.npm.version, '10.0.0'); assert.equal(result.results[0].phase, 'check'); assert.equal(checksAfterInstall, 1);
452
+ assert.equal(result.results[1].status, 'not-applicable'); assert.equal(result.results[1].exitCode, null);
453
+ assert.equal(existsSync(join(root, '.pbai4devs/tools/unsupported-tool/.local')), false);
454
+ assert.equal(state.installs, 1); assert.equal(state.fetches, 1); assert.ok(existsSync(join(local, 'CURRENT.json')));
455
+ });
456
+
457
+ test('setup verifica npm antes de mutar mesmo quando a ferramenta ja esta saudavel', async t => {
458
+ const { root, manager, state, local } = fixture(t); await manager.install('test-tool');
459
+ const before = readFileSync(join(local, 'CURRENT.json'), 'utf8');
460
+ manager.npm = () => join(root, 'npm-missing.cjs');
461
+ const result = await manager.setup();
462
+ assert.equal(result.exitCode, 1); assert.equal(result.results[0].phase, 'preflight'); assert.match(result.results[0].reason, /npm indisponivel/);
463
+ assert.equal(state.installs, 1); assert.equal(state.fetches, 1); assert.equal(readFileSync(join(local, 'CURRENT.json'), 'utf8'), before);
464
+ });
465
+
466
+ test('setup recusa npm ausente, quebrado ou invalido sem criar estado de ferramentas', async t => {
467
+ for (const scenario of ['missing', 'broken', 'invalid']) await t.test(scenario, async subtest => {
468
+ const { root, manager, state, local } = fixture(subtest);
469
+ const path = scenario === 'missing' ? join(root, 'missing-npm.cjs') : setupNpm(root, scenario === 'broken' ? { exitCode: 19 } : { version: 'not-a-version' });
470
+ manager.npm = () => path;
471
+ const result = await manager.setup();
472
+ assert.equal(result.exitCode, 1); assert.equal(result.summary.failed, 1); assert.equal(result.results[0].phase, 'preflight');
473
+ if (scenario === 'broken') assert.equal(result.results[0].exitCode, 19);
474
+ assert.match(result.results[0].reason, /https:\/\/nodejs.org\/en\/download/);
475
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0); assert.equal(existsSync(local), false);
476
+ });
477
+ });
478
+
479
+ test('setup coleta falhas de instalacao e check e continua as demais ferramentas', async t => {
480
+ const catalog = Object.fromEntries(['first', 'second', 'third'].map(name => [name, tool(name)]));
481
+ const { root, manager } = fixture(t, catalog); manager.npm = () => setupNpm(root);
482
+ const seen = [];
483
+ manager.install = async name => { seen.push(`install:${name}`); if (name === 'first') throw Object.assign(new Error('install failure'), { exitCode: 17 }); return { changed: true }; };
484
+ manager.check = name => { seen.push(`check:${name}`); if (name === 'second') throw Object.assign(new Error('probe failure'), { exitCode: 23 }); return { version: '1.0.0' }; };
485
+ const result = await manager.setup();
486
+ assert.equal(result.exitCode, 1); assert.deepEqual(result.summary, { passed: 1, failed: 2, notApplicable: 0 });
487
+ assert.deepEqual(result.results.map(item => [item.name, item.status, item.phase, item.exitCode]), [['first', 'failed', 'install', 17], ['second', 'failed', 'check', 23], ['third', 'passed', 'check', 0]]);
488
+ assert.deepEqual(seen, ['install:first', 'install:second', 'check:second', 'install:third', 'check:third']);
489
+ });
490
+
491
+ test('setup grupo marca N/A mas nome explicito incompativel falha sem instalar', async t => {
492
+ const catalog = { 'unsupported-tool': { ...tool('unsupported-tool', 'desktop'), platforms: [process.platform === 'darwin' ? 'linux-x64' : 'darwin-arm64'] } };
493
+ const { manager, state, local } = fixture(t, catalog);
494
+ manager.npm = () => { throw new Error('Nao deve verificar npm sem ferramentas compativeis'); };
495
+ const group = await manager.setup('desktop'); assert.equal(group.status, 'not-applicable'); assert.equal(group.exitCode, 0);
496
+ const explicit = await manager.setup('unsupported-tool'); assert.equal(explicit.status, 'failed'); assert.equal(explicit.exitCode, 1); assert.equal(explicit.results[0].status, 'failed');
497
+ assert.equal(state.fetches, 0); assert.equal(state.installs, 0); assert.equal(existsSync(local), false);
498
+ });
499
+
500
+ test('CLI setup padrao all retorna o codigo agregado e rejeita argumentos excedentes', async t => {
501
+ const { root, manager } = fixture(t); manager.npm = () => setupNpm(root);
502
+ assert.equal(await main(['setup'], manager), 0);
503
+ manager.npm = () => join(root, 'missing.cjs');
504
+ assert.equal(await main(['--setup', 'all'], manager), 1);
505
+ await assert.rejects(main(['setup', 'all', 'extra'], manager), /excedentes/);
506
+ });
507
+
508
+ test('executables valida plataforma e caminhos relativos sem autorizar comandos arbitrarios', () => {
509
+ const base = { ...tool(), platforms: ['linux-x64', 'darwin-arm64'] };
510
+ assert.deepEqual(validateToolConfig('test-tool', { ...base, executables: { 'linux-x64': ['bin/probe with space'] } }).executables, { 'linux-x64': ['bin/probe with space'] });
511
+ for (const executables of [null, [], 'bin/probe', {}, { 'win32-x64': ['bin/probe'] }, { 'linux-arm64': ['bin/probe'] }, { 'linux-x64': [] }, { 'linux-x64': 'bin/probe' }, { 'linux-x64': ['bin/probe', 'bin/probe'] }]) {
512
+ assert.throws(() => validateToolConfig('test-tool', { ...base, executables }), /[Ee]xecutave/);
513
+ }
514
+ for (const file of ['/tmp/probe', '../probe', 'bin/../probe', './probe', 'bin//probe', 'bin/', 'C:/probe', 'C:\\probe', 'bin\\probe', '', 'bad\0path', null]) {
515
+ assert.throws(() => validateToolConfig('test-tool', { ...base, executables: { 'linux-x64': [file] } }), /relativo/);
516
+ }
517
+ });
518
+
519
+ test('executables prepara somente binarios da plataforma no staging antes de executar probe real', { skip: process.platform === 'win32' }, async t => {
520
+ const other = process.platform === 'darwin' ? 'linux-x64' : 'darwin-arm64';
521
+ const config = { ...tool(), platforms: [platformKey(), other], executables: { [platformKey()]: ['native/probe with space'], [other]: ['native/unused'] } };
522
+ const { manager } = fixture(t, { 'test-tool': config });
523
+ manager.installPackage = (prefix, name, version) => {
524
+ packageFiles(prefix, name, version);
525
+ const base = join(prefix, 'node_modules', name); mkdirSync(join(base, 'native'));
526
+ const native = join(base, 'native/probe with space');
527
+ writeFileSync(native, `#!${process.execPath}\nconsole.log(${JSON.stringify(version)});\n`); chmodSync(native, 0o644);
528
+ writeFileSync(join(base, 'native/unused'), 'unselected'); chmodSync(join(base, 'native/unused'), 0o644);
529
+ writeFileSync(join(base, 'cli.cjs'), `const {spawnSync}=require('node:child_process');const target=require('node:path').join(__dirname,'native/probe with space');const r=spawnSync(target,process.argv.slice(2),{stdio:'inherit'});if(r.error)throw r.error;process.exit(r.status??1);\n`);
530
+ };
531
+ const result = await manager.install('test-tool');
532
+ assert.equal(result.version, '1.0.0'); assert.equal(manager.check('test-tool').version, '1.0.0');
533
+ assert.equal(statSync(join(result.prefix, 'node_modules/test-tool/native/probe with space')).mode & 0o777, 0o755);
534
+ assert.equal(statSync(join(result.prefix, 'node_modules/test-tool/native/unused')).mode & 0o777, 0o644);
535
+ });
536
+
537
+ test('executables recusa arquivo ausente, diretorio e symlink fora do pacote sem promover staging', { skip: process.platform === 'win32' }, async t => {
538
+ for (const scenario of ['missing', 'directory', 'external-symlink']) await t.test(scenario, async subtest => {
539
+ const config = { ...tool(), executables: { [platformKey()]: ['native/probe'] } };
540
+ const { root, manager, local } = fixture(subtest, { 'test-tool': config });
541
+ const outside = join(root, 'outside-native'); writeFileSync(outside, 'untouched'); chmodSync(outside, 0o644);
542
+ manager.installPackage = (prefix, name, version) => {
543
+ packageFiles(prefix, name, version); const base = join(prefix, 'node_modules', name); mkdirSync(join(base, 'native'));
544
+ if (scenario === 'directory') mkdirSync(join(base, 'native/probe'));
545
+ if (scenario === 'external-symlink') symlinkSync(outside, join(base, 'native/probe'));
546
+ };
547
+ await assert.rejects(manager.install('test-tool'), /ENOENT|arquivo regular|fora do pacote/);
548
+ assert.equal(existsSync(join(local, 'CURRENT.json')), false);
549
+ assert.equal(statSync(outside).mode & 0o777, 0o644);
550
+ assert.ok(readdirSync(local).every(name => !name.startsWith('.stage-')));
551
+ });
552
+ });