@pathbit/agentdevkit 0.0.0-stage → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pbai4devs/COMPATIBILIDADE.md +109 -0
- package/.pbai4devs/INSTRUCTIONS.md +87 -0
- package/.pbai4devs/README.md +53 -0
- package/.pbai4devs/agents/README.md +18 -0
- package/.pbai4devs/agents/revisor/AGENT.md +16 -0
- package/.pbai4devs/agents/revisor/antigravity.yaml +8 -0
- package/.pbai4devs/agents/revisor/claude.yaml +5 -0
- package/.pbai4devs/agents/revisor/cursor.yaml +5 -0
- package/.pbai4devs/agents/revisor/gemini.yaml +6 -0
- package/.pbai4devs/agents/revisor/github.yaml +5 -0
- package/.pbai4devs/agents/revisor/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/revisor/mimocode.yaml +10 -0
- package/.pbai4devs/agents/revisor/opencode.yaml +7 -0
- package/.pbai4devs/agents/revisor/zcode.yaml +5 -0
- package/.pbai4devs/commands/README.md +30 -0
- package/.pbai4devs/commands/atualizar-ferramentas/COMMAND.md +17 -0
- package/.pbai4devs/commands/diagnosticar/COMMAND.md +18 -0
- package/.pbai4devs/commands/instalar-ferramentas/COMMAND.md +21 -0
- package/.pbai4devs/commands/preparar-dev/COMMAND.md +14 -0
- package/.pbai4devs/commands/proximos-passos/COMMAND.md +9 -0
- package/.pbai4devs/commands/revisar-entrega/COMMAND.md +29 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +20 -0
- package/.pbai4devs/commands/status-roadmap/COMMAND.md +12 -0
- package/.pbai4devs/commands/testar-navegador/COMMAND.md +16 -0
- package/.pbai4devs/commands/verificar-agentes/COMMAND.md +19 -0
- package/.pbai4devs/estado/README.md +12 -0
- package/.pbai4devs/external-skills/CATALOG.json +121 -0
- package/.pbai4devs/external-skills/README.md +48 -0
- package/.pbai4devs/rules/README.md +18 -0
- package/.pbai4devs/rules/ambiente-local/RULE.md +33 -0
- package/.pbai4devs/rules/ambiente-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/documentacao/RULE.md +11 -0
- package/.pbai4devs/rules/documentacao/cursor.yaml +4 -0
- package/.pbai4devs/rules/estrutura/RULE.md +33 -0
- package/.pbai4devs/rules/estrutura/cursor.yaml +4 -0
- package/.pbai4devs/rules/git/RULE.md +18 -0
- package/.pbai4devs/rules/git/cursor.yaml +4 -0
- package/.pbai4devs/rules/proximos-passos/RULE.md +14 -0
- package/.pbai4devs/rules/qualidade/RULE.md +32 -0
- package/.pbai4devs/rules/qualidade/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +69 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs +41 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +30 -0
- package/.pbai4devs/scripts/executar-script/README.md +28 -0
- package/.pbai4devs/scripts/executar-script/SCRIPT.mjs +78 -0
- package/.pbai4devs/scripts/executar-script/tests/TEST.mjs +122 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/README.md +31 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs +182 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs +134 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +190 -0
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +161 -0
- package/.pbai4devs/scripts/lib/README.md +9 -0
- package/.pbai4devs/scripts/lib/adapters.mjs +135 -0
- package/.pbai4devs/scripts/lib/catalog.mjs +170 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +348 -0
- package/.pbai4devs/scripts/lib/npm.mjs +17 -0
- package/.pbai4devs/scripts/lib/resource-path.mjs +20 -0
- package/.pbai4devs/scripts/lib/runtime.mjs +28 -0
- package/.pbai4devs/scripts/lib/script-config.mjs +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md +34 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs +189 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +71 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +455 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/FIXTURE.html +20 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_BROWSER.mjs +55 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_DESKTOP.mjs +61 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_VERCEL.mjs +192 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +552 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/bootstrap.test.mjs +94 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash/input.txt +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash-overlay/input.txt +1 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/skills/agentdevkit-smoke/SKILL.md +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/index.html +6 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/webreel.config.json +10 -0
- package/.pbai4devs/scripts/revisar-entrega/README.md +36 -0
- package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +249 -0
- package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +249 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +41 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +473 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +290 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +869 -0
- package/.pbai4devs/scripts/testar-infraestrutura/SCRIPT.mjs +39 -0
- package/.pbai4devs/scripts/testar-infraestrutura/tests/TEST.mjs +29 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +54 -0
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +477 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.mjs +1 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.test.mjs.template +8 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/http-api.test.mjs.template +28 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/server.mjs +34 -0
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +305 -0
- package/.pbai4devs/scripts/testar-navegador/REFERENCE.md +49 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.conf +1 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.js +64 -0
- package/.pbai4devs/scripts/testar-navegador/tests/FAIL.js +8 -0
- package/.pbai4devs/scripts/testar-navegador/tests/SUCCESS.js +13 -0
- package/.pbai4devs/scripts/verificar-recursos/README.md +25 -0
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +209 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +310 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/ambiente-local.mjs +80 -0
- package/.pbai4devs/skills/README.md +24 -0
- package/.pbai4devs/skills/git/git-branch/SKILL.md +11 -0
- package/.pbai4devs/skills/git/git-commit/SKILL.md +16 -0
- package/.pbai4devs/skills/git/git-diff/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-fetch/SKILL.md +10 -0
- package/.pbai4devs/skills/git/git-log/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-merge/SKILL.md +13 -0
- package/.pbai4devs/skills/git/git-pull/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-push/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-status/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-worktree/SKILL.md +10 -0
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +37 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +35 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +34 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +26 -0
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +62 -0
- package/.pbai4devs/skills/projeto/criar-agente/SKILL.md +20 -0
- package/.pbai4devs/skills/projeto/criar-comando/SKILL.md +25 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +41 -0
- package/.pbai4devs/skills/projeto/criar-regra/SKILL.md +19 -0
- package/.pbai4devs/skills/projeto/criar-skill/SKILL.md +22 -0
- package/.pbai4devs/skills/projeto/executar-script/SKILL.md +29 -0
- package/.pbai4devs/skills/projeto/preparar-dev/SKILL.md +45 -0
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +56 -0
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +23 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +42 -0
- package/.pbai4devs/skills/projeto/usar-skills-externas/SKILL.md +28 -0
- package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +45 -0
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +33 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +26 -0
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +48 -0
- package/.pbai4devs/templates/README.md +65 -0
- package/.pbai4devs/templates/adr/TEMPLATE.md +21 -0
- package/.pbai4devs/templates/ambiente-local/Makefile.template +48 -0
- package/.pbai4devs/templates/ambiente-local/TEMPLATE.md +48 -0
- package/.pbai4devs/templates/ambiente-local/compose.local.yml.template +16 -0
- package/.pbai4devs/templates/ambiente-local/dockerignore.template +28 -0
- package/.pbai4devs/templates/changelog/TEMPLATE.md +17 -0
- package/.pbai4devs/templates/claude/TEMPLATE.md +8 -0
- package/.pbai4devs/templates/clients/ADAPTERS.json +306 -0
- package/.pbai4devs/templates/clients/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/comando/TEMPLATE.md +15 -0
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +46 -0
- package/.pbai4devs/templates/configuracao-raiz/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/configuracao-raiz/editorconfig.template +21 -0
- package/.pbai4devs/templates/configuracao-raiz/env.example.template +3 -0
- package/.pbai4devs/templates/configuracao-raiz/gitattributes.template +6 -0
- package/.pbai4devs/templates/contribuicao/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/design/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/entrypoints/AGENTS.md +19 -0
- package/.pbai4devs/templates/entrypoints/CLAUDE.md +7 -0
- package/.pbai4devs/templates/entrypoints/GEMINI.md +7 -0
- package/.pbai4devs/templates/entrypoints/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +42 -0
- package/.pbai4devs/templates/gitignore/agents.ignore +45 -0
- package/.pbai4devs/templates/gitignore/clients.json +20 -0
- package/.pbai4devs/templates/gitignore/local.ignore +10 -0
- package/.pbai4devs/templates/gitignore/shared.ignore +60 -0
- package/.pbai4devs/templates/instrucoes-agentes/TEMPLATE.md +25 -0
- package/.pbai4devs/templates/plano-implementacao/TEMPLATE.md +27 -0
- package/.pbai4devs/templates/prd/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/readme-pasta/TEMPLATE.md +16 -0
- package/.pbai4devs/templates/readme-projeto/TEMPLATE.md +34 -0
- package/.pbai4devs/templates/release/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/revisao-entrega/TEMPLATE.md +41 -0
- package/.pbai4devs/templates/roadmap/TEMPLATE.md +18 -0
- package/.pbai4devs/templates/seguranca/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/spec/TEMPLATE.md +61 -0
- package/.pbai4devs/templates/tasks/TEMPLATE.md +20 -0
- package/.pbai4devs/templates/ux-ui/TEMPLATE.md +26 -0
- package/.pbai4devs/tools/README.md +87 -0
- package/.pbai4devs/tools/agent-browser/README.md +26 -0
- package/.pbai4devs/tools/agent-browser/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-browser/TOOL.json +33 -0
- package/.pbai4devs/tools/agent-desktop/README.md +26 -0
- package/.pbai4devs/tools/agent-desktop/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-desktop/TOOL.json +30 -0
- package/.pbai4devs/tools/just-bash/README.md +23 -0
- package/.pbai4devs/tools/just-bash/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/just-bash/TOOL.json +27 -0
- package/.pbai4devs/tools/markdownlint-cli2/README.md +18 -0
- package/.pbai4devs/tools/markdownlint-cli2/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/markdownlint-cli2/TOOL.json +28 -0
- package/.pbai4devs/tools/opensrc/README.md +25 -0
- package/.pbai4devs/tools/opensrc/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opensrc/TOOL.json +29 -0
- package/.pbai4devs/tools/portless/README.md +21 -0
- package/.pbai4devs/tools/portless/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/portless/TOOL.json +30 -0
- package/.pbai4devs/tools/skills/README.md +24 -0
- package/.pbai4devs/tools/skills/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/skills/TOOL.json +33 -0
- package/.pbai4devs/tools/webreel/README.md +23 -0
- package/.pbai4devs/tools/webreel/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/webreel/TOOL.json +25 -0
- package/CHANGELOG.md +65 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.es.md +91 -0
- package/README.md +94 -2
- package/bin/README.md +8 -0
- package/bin/agentdevkit.mjs +35 -0
- package/docs/README.md +81 -0
- package/docs/guides/README.md +14 -0
- package/docs/guides/cli.md +81 -0
- package/docs/guides/installation.md +86 -0
- package/docs/guides/resources.md +121 -0
- package/docs/guides/tools-and-testing.md +85 -0
- package/docs/guides/troubleshooting.md +151 -0
- package/docs/guides/workflow.md +130 -0
- package/docs/maintainers/README.md +12 -0
- package/docs/maintainers/community.md +49 -0
- package/docs/maintainers/development.md +54 -0
- package/docs/maintainers/maintenance.md +104 -0
- package/docs/maintainers/packages.md +87 -0
- package/docs/product/README.md +12 -0
- package/docs/product/design.md +104 -0
- package/docs/product/prd.md +70 -0
- package/docs/product/roadmap.md +38 -0
- package/docs/product/ux-ui.md +42 -0
- package/docs/specs/README.md +21 -0
- package/docs/specs/github-packages/SPEC.md +45 -0
- package/docs/specs/v0.0.1/README.md +10 -0
- package/docs/specs/v0.0.1/SPEC.md +57 -0
- package/docs/specs/v0.0.2/README.md +10 -0
- package/docs/specs/v0.0.2/SPEC.md +67 -0
- package/docs/specs/v0.0.3/README.md +10 -0
- package/docs/specs/v0.0.3/SPEC.md +64 -0
- package/docs/specs/v0.0.4/README.md +10 -0
- package/docs/specs/v0.0.4/SPEC.md +53 -0
- package/docs/specs/v0.0.5/README.md +9 -0
- package/docs/specs/v0.0.5/SPEC.md +42 -0
- package/docs/specs/v0.0.6/README.md +10 -0
- package/docs/specs/v0.0.6/SPEC.md +77 -0
- package/package.json +67 -4
- package/src/README.md +16 -0
- package/src/adapters/index.mjs +1 -0
- package/src/cli.mjs +162 -0
- package/src/config.mjs +14 -0
- package/src/i18n.mjs +42 -0
- package/src/install.mjs +373 -0
- package/src/next.mjs +79 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { synchronize, header, isReference, resourcePath } from '../SCRIPT.mjs';
|
|
9
|
+
import { buildCatalog } from '../../lib/catalog.mjs';
|
|
10
|
+
import { toolWrapper } from '../../preparar-ferramentas/SCRIPT.mjs';
|
|
11
|
+
import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '../../lib/gitignore.mjs';
|
|
12
|
+
import './LOCAL_IGNORE.mjs';
|
|
13
|
+
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
14
|
+
|
|
15
|
+
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
16
|
+
const cli = resolve(dirname(fileURLToPath(import.meta.url)), '../SCRIPT.mjs');
|
|
17
|
+
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
18
|
+
const skill = '---\nname: demo\ndescription: Exemplo.\n---\n\n# Demo\n\nNAO_DUPLICAR_INSTRUCOES\n';
|
|
19
|
+
const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
|
|
20
|
+
const read = (root, path) => readFileSync(join(root, path), 'utf8');
|
|
21
|
+
const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
|
|
22
|
+
const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
|
|
23
|
+
const toolConfig = (name = 'demo') => ({ name, description: `Ferramenta ${name}.`, group: 'base', source: `https://github.com/fixture/${name}`, download: { provider: 'npm', package: name, registry: 'https://registry.npmjs.org/', repository: `fixture/${name}`, channel: 'latest' }, platforms: [`${process.platform}-${process.arch}`], scripts: { entry: 'SCRIPT.mjs' }, probe: ['--version'] });
|
|
24
|
+
const externalConfig = () => ({ schemaVersion: 1, allowlist: ['fixture/skills'], skills: [{ id: 'external-demo', repository: 'fixture/skills', commit: 'a'.repeat(40), path: 'skills/demo/SKILL.md', name: 'external-demo', license: 'MIT', use: 'Revisar exemplo externo.', status: 'reviewed', notes: 'Sem execução automática.' }] });
|
|
25
|
+
const run = (root, options = {}) => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
|
|
26
|
+
function snapshot(root) {
|
|
27
|
+
const result = [];
|
|
28
|
+
function visit(folder, parent = '') {
|
|
29
|
+
for (const name of readdirSync(folder).sort()) {
|
|
30
|
+
const file = join(folder, name), path = parent ? `${parent}/${name}` : name, item = lstatSync(file);
|
|
31
|
+
if (item.isSymbolicLink()) result.push([path, 'link', readlinkSync(file)]);
|
|
32
|
+
else if (item.isDirectory()) { result.push([path, 'directory']); visit(file, path); }
|
|
33
|
+
else result.push([path, 'file', createHash('sha256').update(readFileSync(file)).digest('hex')]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
visit(root);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function fixture(t, minimal = false) {
|
|
40
|
+
mkdirSync(join(project, '.agent-tests'), { recursive: true });
|
|
41
|
+
const root = mkdtempSync(join(project, '.agent-tests/sync-node com espaço-'));
|
|
42
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
43
|
+
write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Cache dos clientes\n/*\n!/.gitignore\n{{ALLOWLIST}}\n/cache/\n*.log\n');
|
|
44
|
+
write(root, '.pbai4devs/templates/gitignore/clients.json', '{}\n');
|
|
45
|
+
write(root, '.pbai4devs/templates/gitignore/shared.ignore', '# Estado compartilhado\n/tools/*/.local/\n/tools/.cache/\n');
|
|
46
|
+
write(root, '.pbai4devs/espelhos.conf', minimal ? 'agente|.codex|plano\n' : [
|
|
47
|
+
'# Clientes da fixture', 'agente|.claude', 'agente|.opencode', 'agente|.github|plano', 'agente|.codex|plano', 'agente|.cursor|plano', 'agente|.kimi-code|plano', 'agente|.vscode|plano', 'agente|.agents|plano', 'agente|.agent|plano', 'agente|.gemini|plano',
|
|
48
|
+
`nativo|.github/instructions/demo.instructions.md|${source}`,
|
|
49
|
+
'nativo|.kimi-code/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/kimi-code.yaml',
|
|
50
|
+
'nativo|.opencode/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/opencode.yaml',
|
|
51
|
+
].join('\n') + '\n');
|
|
52
|
+
if (minimal) return root;
|
|
53
|
+
write(root, source, skill);
|
|
54
|
+
write(root, '.pbai4devs/skills/grupo com espaço/outro/SKILL.md', '---\nname: outro\ndescription: Outro.\n---\n\n# Outro\n');
|
|
55
|
+
write(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Perfil.\n---\n\n# Revisor\n');
|
|
56
|
+
write(root, '.pbai4devs/rules/example/RULE.md', '---\ntrigger: always_on\ndescription: Regra.\n---\n\n# Regra\n');
|
|
57
|
+
write(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: Comando.\n---\n\n# Comando\n');
|
|
58
|
+
write(root, '.pbai4devs/agents/revisor/kimi-code.yaml', '---\nname: revisor\ntools: [Read, Grep, Glob]\n---\n');
|
|
59
|
+
write(root, '.pbai4devs/agents/revisor/opencode.yaml', '---\nmode: subagent\npermission:\n edit: deny\n---\n');
|
|
60
|
+
write(root, '.pbai4devs/adapters/antigravity/agents/revisor.md', pointer('../../../agents/revisor/AGENT.md', 'Revisor'));
|
|
61
|
+
write(root, '.opencode/agents/revisor/AGENT.md', '---\nmode: subagent\npermission:\n edit: deny\n---\n\n# Revisor\n\nLeia e siga [origem](../../../.pbai4devs/agents/revisor/AGENT.md).\n');
|
|
62
|
+
write(root, '.github/workflows/ci.yml', 'workflow-preservado\n');
|
|
63
|
+
write(root, '.vscode/settings.json', '{"fixture":"config-preservada"}\n');
|
|
64
|
+
write(root, '.vscode/extensions.json', '{"recommendations":[]}\n');
|
|
65
|
+
write(root, '.codex/config.toml', 'config-native-preservada\n');
|
|
66
|
+
write(root, '.github/prompts/custom.prompt.md', '# Prompt proprio\n');
|
|
67
|
+
write(root, '.claude/notas.md', '# Nota propria\n');
|
|
68
|
+
write(root, 'README.md', 'README raiz preservado\n');
|
|
69
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra']) {
|
|
70
|
+
write(root, `.pbai4devs/${kind}/demo/REFERENCE.md`, '---\ndescription: Exclusivo.\n---\n\n# Exclusivo\n');
|
|
71
|
+
if (kind !== 'extra') write(root, `.claude/${kind}/demo/REFERENCE.md`, pointer(`../../../.pbai4devs/${kind}/demo/REFERENCE.md`));
|
|
72
|
+
}
|
|
73
|
+
write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify(toolConfig()));
|
|
74
|
+
write(root, '.pbai4devs/tools/demo/SCRIPT.mjs', toolWrapper('demo'));
|
|
75
|
+
for (const file of ['README.md', 'INSTRUCTIONS.md']) {
|
|
76
|
+
write(root, `.pbai4devs/${file}`, '---\ndescription: Guia.\n---\n\n# Guia\n');
|
|
77
|
+
write(root, `.claude/${file}`, pointer(`../.pbai4devs/${file}`, 'Guia'));
|
|
78
|
+
}
|
|
79
|
+
return root;
|
|
80
|
+
}
|
|
81
|
+
function rejected(root, options = {}, pattern) {
|
|
82
|
+
const before = snapshot(root);
|
|
83
|
+
assert.throws(() => run(root, options), pattern);
|
|
84
|
+
assert.deepEqual(snapshot(root), before, 'Uma operação rejeitada alterou a fixture.');
|
|
85
|
+
}
|
|
86
|
+
function assertCheckReadonly(root, options = {}, exitCode = 1) {
|
|
87
|
+
const before = snapshot(root);
|
|
88
|
+
assert.equal(run(root, { check: true, ...options }).exitCode, exitCode);
|
|
89
|
+
assert.deepEqual(snapshot(root), before, '--check alterou a fixture.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('sincroniza somente principais, preserva metadados e usa referências UTF-8 relativas', t => {
|
|
93
|
+
const root = fixture(t);
|
|
94
|
+
assertCheckReadonly(root);
|
|
95
|
+
assert.equal(run(root).exitCode, 0);
|
|
96
|
+
assertCheckReadonly(root, {}, 0);
|
|
97
|
+
assert.match(read(root, '.codex/skills/demo/SKILL.md'), /name: demo/);
|
|
98
|
+
assert.doesNotMatch(read(root, '.codex/skills/demo/SKILL.md'), /NAO_DUPLICAR_INSTRUCOES/);
|
|
99
|
+
assert.match(read(root, '.codex/skills/outro/SKILL.md'), /grupo%20com%20espa%C3%A7o/);
|
|
100
|
+
assert.match(read(root, '.opencode/agents/revisor/AGENT.md'), /edit: deny/);
|
|
101
|
+
assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
|
|
102
|
+
assert.ok(existsSync(join(root, '.claude/skills/projeto/demo/SKILL.md')));
|
|
103
|
+
for (const client of ['.claude', '.opencode', '.github', '.codex', '.cursor', '.kimi-code', '.vscode', '.agents', '.agent', '.gemini']) {
|
|
104
|
+
for (const kind of ['templates', 'scripts', 'tools', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'extra', 'README.md', 'INSTRUCTIONS.md']) assert.equal(existsSync(join(root, client, kind)), false, `${client}/${kind}`);
|
|
105
|
+
}
|
|
106
|
+
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
|
|
107
|
+
assert.ok(JSON.parse(read(root, '.pbai4devs/skills.json')).entries.some(entry => entry.path === '.pbai4devs/skills/grupo com espaço/outro'));
|
|
108
|
+
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, [{ path: '.pbai4devs/rules/example/RULE.md' }]);
|
|
109
|
+
assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
|
|
110
|
+
assert.match(read(root, '.vscode/settings.json'), /config-preservada/);
|
|
111
|
+
assert.ok(existsSync(join(root, '.vscode/extensions.json')));
|
|
112
|
+
assert.match(read(root, '.codex/config.toml'), /config-native-preservada/);
|
|
113
|
+
assert.ok(existsSync(join(root, '.github/prompts/custom.prompt.md')));
|
|
114
|
+
assert.ok(existsSync(join(root, '.claude/notas.md')));
|
|
115
|
+
const before = snapshot(root);
|
|
116
|
+
run(root);
|
|
117
|
+
assert.deepEqual(snapshot(root), before, 'A segunda sincronização deve ser idempotente.');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('metadados genericos seguem a origem e overrides nativos ficam no YAML central', t => {
|
|
121
|
+
const root = fixture(t); run(root);
|
|
122
|
+
write(root, '.pbai4devs/commands/check/COMMAND.md', '---\nname: check-novo\ndescription: Comando atualizado.\n---\n\n# Comando\n');
|
|
123
|
+
write(root, '.pbai4devs/rules/example/RULE.md', '---\ntrigger: glob\nglobs: "**/*.ts"\ndescription: Regra atualizada.\n---\n\n# Regra\n');
|
|
124
|
+
write(root, '.pbai4devs/agents/revisor/AGENT.md', '---\nname: revisor\ndescription: Perfil atualizado.\n---\n\n# Revisor\n');
|
|
125
|
+
write(root, '.codex/commands/check.md', read(root, '.codex/commands/check.md').replace('description: Comando.', 'description: Obsoleto.\nlocal-only: true'));
|
|
126
|
+
assertCheckReadonly(root);
|
|
127
|
+
run(root);
|
|
128
|
+
assert.match(read(root, '.codex/commands/check.md'), /name: check-novo\ndescription: Comando atualizado\./);
|
|
129
|
+
assert.doesNotMatch(read(root, '.codex/commands/check.md'), /Obsoleto|local-only/);
|
|
130
|
+
assert.match(read(root, '.codex/rules/example.md'), /trigger: glob\nglobs: "\*\*\/\*\.ts"/);
|
|
131
|
+
assert.match(read(root, '.codex/agents/revisor.md'), /Perfil atualizado\./);
|
|
132
|
+
assert.match(read(root, '.opencode/agents/revisor/AGENT.md'), /edit: deny/);
|
|
133
|
+
assert.match(read(root, '.kimi-code/agents/revisor.md'), /tools: \[Read, Grep, Glob\]/);
|
|
134
|
+
assert.match(read(root, '.github/instructions/demo.instructions.md'), /applyTo: '\*\*'/);
|
|
135
|
+
assertCheckReadonly(root, {}, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('hub unico permite tres novos clientes, regras por inclusao e metadados nativos', t => {
|
|
139
|
+
const root = fixture(t);
|
|
140
|
+
write(root, '.pbai4devs/agents/revisor/antigravity.yaml', '---\nname: revisor\nmainAgent: false\nsubagent: true\n---\n');
|
|
141
|
+
write(root, '.pbai4devs/agents/revisor/gemini.yaml', '---\nname: revisor\nkind: local\n---\n');
|
|
142
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + [
|
|
143
|
+
'nativo|.agents/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/antigravity.yaml',
|
|
144
|
+
'nativo|.agent/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/antigravity.yaml',
|
|
145
|
+
'nativo|.gemini/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/gemini.yaml',
|
|
146
|
+
].join('\n') + '\n');
|
|
147
|
+
run(root);
|
|
148
|
+
for (const client of ['.agents', '.agent', '.gemini']) {
|
|
149
|
+
assert.ok(existsSync(join(root, client, 'skills/demo/SKILL.md')));
|
|
150
|
+
assert.ok(existsSync(join(root, client, '.gitignore')));
|
|
151
|
+
for (const extra of ['tools', 'scripts', 'templates', 'docs', 'external-skills']) assert.equal(existsSync(join(root, client, extra)), false);
|
|
152
|
+
}
|
|
153
|
+
for (const client of ['.agents', '.agent']) {
|
|
154
|
+
assert.match(read(root, `${client}/agents/revisor.md`), /mainAgent: false/);
|
|
155
|
+
assert.match(read(root, `${client}/rules/example.md`), /@\[origem\]\(\.\.\/\.\.\/\.pbai4devs\/rules\/example\/RULE\.md\)/);
|
|
156
|
+
assert.ok(isReference(read(root, `${client}/rules/example.md`)));
|
|
157
|
+
const target = read(root, `${client}/rules/example.md`).match(/@\[origem\]\(([^)]+)\)/)[1];
|
|
158
|
+
assert.ok(existsSync(resolve(root, client, 'rules', decodeURIComponent(target))));
|
|
159
|
+
}
|
|
160
|
+
assert.match(read(root, '.gemini/agents/revisor.md'), /kind: local/);
|
|
161
|
+
const catalog = JSON.parse(read(root, '.pbai4devs/catalog.json'));
|
|
162
|
+
assert.equal(catalog.agents.length, 1);
|
|
163
|
+
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')), { entries: [{ path: '.pbai4devs/agents/revisor/AGENT.md' }] });
|
|
164
|
+
assert.ok(catalog.skills.every(entry => entry.path.startsWith('.pbai4devs/')));
|
|
165
|
+
assertCheckReadonly(root, {}, 0);
|
|
166
|
+
assert.doesNotThrow(() => validateIgnoreClients({ '.agents': {}, '.agent': {}, '.gemini': {} }));
|
|
167
|
+
assert.throws(() => validateIgnoreClients({ '.pbai4devs': {} }));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Gemini usa TOML com inclusao relativa ao workspace e argumentos, sem Markdown duplicado', t => {
|
|
171
|
+
const root = fixture(t);
|
|
172
|
+
write(root, '.pbai4devs/commands/check/COMMAND.md', '---\ndescription: "Verifica \\"aspas\\" e regras."\n---\n\n# Check\n');
|
|
173
|
+
write(root, '.gemini/commands/check.md', pointer('../../.pbai4devs/commands/check/COMMAND.md'));
|
|
174
|
+
run(root);
|
|
175
|
+
const target = '.gemini/commands/check.toml';
|
|
176
|
+
const text = read(root, target); const lines = text.trimEnd().split('\n');
|
|
177
|
+
assert.equal(lines[0], '# gerado: sincronizar-agentes');
|
|
178
|
+
assert.equal(JSON.parse(lines[1].slice('description = '.length)), 'Verifica "aspas" e regras.');
|
|
179
|
+
assert.equal(JSON.parse(lines[2].slice('prompt = '.length)), '@{.pbai4devs/commands/check/COMMAND.md}\n\n{{args}}');
|
|
180
|
+
assert.ok(isReference(text)); assert.equal(existsSync(join(root, '.gemini/commands/check.md')), false);
|
|
181
|
+
write(root, target, text + '# instrucao propria\n');
|
|
182
|
+
assertCheckReadonly(root); rejected(root, {}, /--force/);
|
|
183
|
+
const repaired = run(root, { force: true });
|
|
184
|
+
assert.equal(read(repaired.backupRoot, target), text + '# instrucao propria\n');
|
|
185
|
+
assert.equal(read(root, target), text);
|
|
186
|
+
rmSync(join(root, '.pbai4devs/commands/check'), { recursive: true });
|
|
187
|
+
run(root); assert.equal(existsSync(join(root, target)), false);
|
|
188
|
+
assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('nativos TOML ficam restritos aos comandos Gemini e ponteiros falsos nao sao apagados como gerados', t => {
|
|
192
|
+
for (const row of [
|
|
193
|
+
'nativo|.claude/commands/check.toml|.pbai4devs/commands/check/COMMAND.md',
|
|
194
|
+
'nativo|.gemini/agents/revisor.toml|.pbai4devs/agents/revisor/AGENT.md',
|
|
195
|
+
'nativo|.gemini/commands/check.toml|.pbai4devs/rules/example/RULE.md',
|
|
196
|
+
'nativo|.gemini/commands/check.md|.pbai4devs/commands/check/COMMAND.md',
|
|
197
|
+
'nativo|.gemini/commands/check.toml|.pbai4devs/commands/check/COMMAND.md|.pbai4devs/agents/revisor/kimi-code.yaml',
|
|
198
|
+
]) {
|
|
199
|
+
const root = fixture(t); write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
|
|
200
|
+
rejected(root, { force: true });
|
|
201
|
+
}
|
|
202
|
+
assert.equal(isReference('# gerado: sincronizar-agentes\ndescription = "teste"\nprompt = "!{rm -rf .}"\n'), false);
|
|
203
|
+
assert.equal(isReference('# gerado: sincronizar-agentes\ndescription = "teste"\nprompt = "@{.pbai4devs/commands/../private/COMMAND.md}\\n\\n{{args}}"\n'), false);
|
|
204
|
+
assert.equal(isReference('---\ndescription: regra\n---\n\n# Regra\n\n@[../../.pbai4devs/rules/demo/RULE.md]\nTexto proprio\n'), false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('gera gitignore canonico e de clientes; registro gitignore nao espelha recursos', t => {
|
|
208
|
+
const root = fixture(t, true);
|
|
209
|
+
write(root, source, skill);
|
|
210
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|plano\ngitignore|.vscode\n');
|
|
211
|
+
write(root, '.pbai4devs/templates/gitignore/clients.json', JSON.stringify({ '.vscode': { resources: false, allow: ['settings.json'] } }));
|
|
212
|
+
write(root, '.vscode/settings.json', '{"preserved":true}\n');
|
|
213
|
+
write(root, '.gitignore', '# Somente regras gerais\n*.tmp\n');
|
|
214
|
+
const result = run(root);
|
|
215
|
+
assert.equal(result.gitignores, 3);
|
|
216
|
+
assert.equal(read(root, '.pbai4devs/.gitignore'), `${ignoreBegin}\n${read(root, '.pbai4devs/templates/gitignore/shared.ignore')}${ignoreEnd}\n`);
|
|
217
|
+
for (const target of ['.codex', '.vscode']) assert.doesNotMatch(read(root, `${target}/.gitignore`), /\{\{ALLOWLIST\}\}/);
|
|
218
|
+
assert.match(read(root, '.codex/.gitignore'), /!\/skills\//);
|
|
219
|
+
assert.doesNotMatch(read(root, '.vscode/.gitignore'), /!\/skills/);
|
|
220
|
+
assert.match(read(root, '.vscode/.gitignore'), /!\/settings\.json/);
|
|
221
|
+
assert.equal(existsSync(join(root, '.vscode/skills')), false);
|
|
222
|
+
assert.equal(read(root, '.vscode/settings.json'), '{"preserved":true}\n');
|
|
223
|
+
assert.equal(read(root, '.gitignore'), '# Somente regras gerais\n*.tmp\n');
|
|
224
|
+
assertCheckReadonly(root, {}, 0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('repara somente bloco gitignore e force salva o original sem perder regras externas', t => {
|
|
228
|
+
const root = fixture(t, true); run(root);
|
|
229
|
+
const prefix = '# Regras do usuario\r\n/private-cache/\r\n';
|
|
230
|
+
const suffix = '# Excecao local\r\n!keep.log\r\n';
|
|
231
|
+
const old = `${prefix}${ignoreBegin}\r\n/custom-inside/\r\n${ignoreEnd}\r\n${suffix}`;
|
|
232
|
+
write(root, '.codex/.gitignore', old);
|
|
233
|
+
assertCheckReadonly(root); assertCheckReadonly(root, { force: true });
|
|
234
|
+
const result = run(root, { force: true });
|
|
235
|
+
assert.equal(readFileSync(join(result.backupRoot, '.codex/.gitignore'), 'utf8'), old);
|
|
236
|
+
const repaired = readFileSync(join(root, '.codex/.gitignore'), 'utf8');
|
|
237
|
+
assert.ok(repaired.startsWith(prefix)); assert.ok(repaired.endsWith(suffix));
|
|
238
|
+
assert.ok(!repaired.includes('/custom-inside/')); assert.ok(repaired.includes('/cache/'));
|
|
239
|
+
write(root, '.pbai4devs/templates/gitignore/agents.ignore', '# Novo padrao\n{{ALLOWLIST}}\n/cache/\n/session/\n');
|
|
240
|
+
assertCheckReadonly(root);
|
|
241
|
+
run(root);
|
|
242
|
+
assert.ok(read(root, '.codex/.gitignore').includes('/session/'));
|
|
243
|
+
assert.ok(readFileSync(join(root, '.codex/.gitignore'), 'utf8').endsWith(suffix));
|
|
244
|
+
assertCheckReadonly(root, {}, 0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('adiciona bloco a gitignore proprio, detecta exclusao e mantem idempotencia', t => {
|
|
248
|
+
const root = fixture(t, true);
|
|
249
|
+
const custom = '# Local\n/custom/\n!custom/keep'; write(root, '.codex/.gitignore', custom);
|
|
250
|
+
run(root); assert.ok(read(root, '.codex/.gitignore').startsWith(custom));
|
|
251
|
+
const before = snapshot(root); run(root); assert.deepEqual(snapshot(root), before);
|
|
252
|
+
rmSync(join(root, '.pbai4devs/.gitignore')); assertCheckReadonly(root);
|
|
253
|
+
assert.equal(run(root).gitignores, 1); assertCheckReadonly(root, {}, 0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('gitignore recusa marcadores invalidos e fontes ausentes antes de qualquer mutacao', t => {
|
|
257
|
+
for (const text of [ignoreBegin, ignoreEnd, `${ignoreEnd}\n${ignoreBegin}\n`, `${ignoreBegin}\na\n${ignoreEnd}\n${ignoreBegin}\nb\n${ignoreEnd}\n`]) {
|
|
258
|
+
const root = fixture(t, true); write(root, '.codex/.gitignore', text);
|
|
259
|
+
rejected(root, { force: true }, /Marcadores/);
|
|
260
|
+
}
|
|
261
|
+
for (const text of ['', `${ignoreBegin}\n/cache/\n${ignoreEnd}\n`]) {
|
|
262
|
+
const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/shared.ignore', text);
|
|
263
|
+
rejected(root, { force: true }, /Template/);
|
|
264
|
+
}
|
|
265
|
+
const missing = fixture(t, true); rmSync(join(missing, '.pbai4devs/templates/gitignore/agents.ignore'));
|
|
266
|
+
rejected(missing, { force: true }, /Template .gitignore ausente/);
|
|
267
|
+
for (const row of ['gitignore|.vscode|extra', 'gitignore|.pbai4devs', 'gitignore|../outside', 'gitignore|.git', 'gitignore|.CODEX']) {
|
|
268
|
+
const root = fixture(t, true); write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
|
|
269
|
+
rejected(root, { force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('gitignore gerado funciona no Git sem esconder definicoes versionadas', t => {
|
|
274
|
+
const root = fixture(t);
|
|
275
|
+
const git = spawnSync('git', ['init', '-q', root], { encoding: 'utf8' });
|
|
276
|
+
if (git.error?.code === 'ENOENT') { t.skip('Git ausente: prova de regras delegada a integracao.'); return; }
|
|
277
|
+
assert.equal(git.status, 0, git.stderr); run(root);
|
|
278
|
+
for (const file of ['.pbai4devs/tools/demo/.local/state.json', '.codex/cache/session.json', '.claude/debug.log']) {
|
|
279
|
+
write(root, file, '{}\n');
|
|
280
|
+
assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 0, file);
|
|
281
|
+
}
|
|
282
|
+
for (const file of ['.pbai4devs/tools/demo/TOOL.json', '.pbai4devs/tools/demo/SCRIPT.mjs', '.codex/skills/demo/SKILL.md', '.codex/.gitignore', '.pbai4devs/.gitignore']) {
|
|
283
|
+
assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 1, file);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('mergeGitignore preserva bytes exteriores e nao interpreta o arquivo como Markdown', () => {
|
|
288
|
+
const current = `---\r\ncustom: true\r\n---\r\n${ignoreBegin}\r\nold\r\n${ignoreEnd}\r\n**/other\r\n`;
|
|
289
|
+
const merged = mergeGitignore(current, '*.log\n');
|
|
290
|
+
assert.equal(merged.hasBlock, true);
|
|
291
|
+
assert.equal(merged.text, `---\r\ncustom: true\r\n---\r\n${ignoreBegin}\n*.log\n${ignoreEnd}\n**/other\r\n`);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('allowlist abre somente configuracoes e diretorios declarados no Git', t => {
|
|
295
|
+
const root = fixture(t, true);
|
|
296
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|plano\ngitignore|.vscode\n');
|
|
297
|
+
write(root, '.pbai4devs/templates/gitignore/clients.json', JSON.stringify({ '.codex': { allow: ['config.toml', 'native/file.json'] }, '.vscode': { resources: false, allow: ['settings.json', 'snippets/'] } }));
|
|
298
|
+
write(root, '.gitignore', '.DS_Store\n*.swp\nThumbs.db\n');
|
|
299
|
+
const git = spawnSync('git', ['init', '-q', root], { encoding: 'utf8' });
|
|
300
|
+
if (git.error?.code === 'ENOENT') { t.skip('Git ausente: prova de allowlist delegada a integracao.'); return; }
|
|
301
|
+
assert.equal(git.status, 0, git.stderr); run(root);
|
|
302
|
+
for (const file of ['.codex/config.toml', '.codex/native/file.json', '.codex/skills/demo/SKILL.md', '.vscode/settings.json', '.vscode/snippets/example.code-snippets']) {
|
|
303
|
+
write(root, file, 'fixture\n'); assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 1, file);
|
|
304
|
+
}
|
|
305
|
+
for (const file of ['.codex/native/extra.json', '.codex/auth.json', '.vscode/skills/demo/SKILL.md', '.vscode/tasks.json', '.codex/skills/demo/.DS_Store', '.codex/skills/demo/file.swp', '.vscode/snippets/Thumbs.db']) {
|
|
306
|
+
write(root, file, 'fixture\n'); assert.equal(spawnSync('git', ['check-ignore', '--no-index', file], { cwd: root }).status, 0, file);
|
|
307
|
+
}
|
|
308
|
+
assert.equal(existsSync(join(root, '.codex/clients.json')), false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('allowlist invalida ou token ausente falha antes de mutacoes', t => {
|
|
312
|
+
for (const config of [[], null, { '.pbai4devs': {} }, { '../outside': {} }, { '.codex': [], '.vscode': {} }, { '.codex': { resources: 'false' } }, { '.codex': { allow: null } }, { '.codex': { extra: true } }, { '.codex': {}, '.CODEX': {} }]) assert.throws(() => validateIgnoreClients(config));
|
|
313
|
+
for (const path of ['/absolute', '../outside', 'nested/../outside', './file', 'C:/file', 'back\\slash', 'double//slash', '*', '*.json', '!file', '#file', 'ends ', '']) assert.throws(() => validateIgnoreClients({ '.codex': { allow: [path] } }), /relativo/);
|
|
314
|
+
for (const body of ['{{ALLOWLIST}}\n{{ALLOWLIST}}\n']) {
|
|
315
|
+
const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/agents.ignore', body); rejected(root, { force: true }, /unico/);
|
|
316
|
+
}
|
|
317
|
+
const root = fixture(t, true); write(root, '.pbai4devs/templates/gitignore/clients.json', '{broken'); rejected(root, { force: true });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('registros são derivados, determinísticos e --check nunca os corrige', t => {
|
|
321
|
+
const root = fixture(t);
|
|
322
|
+
run(root);
|
|
323
|
+
write(root, '.pbai4devs/skills.json', '{"incorreto":true}\n');
|
|
324
|
+
assertCheckReadonly(root);
|
|
325
|
+
run(root);
|
|
326
|
+
assert.ok(Array.isArray(JSON.parse(read(root, '.pbai4devs/skills.json')).entries));
|
|
327
|
+
rmSync(join(root, '.pbai4devs/adapters/antigravity/agents/revisor.md'));
|
|
328
|
+
run(root);
|
|
329
|
+
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/agents.json')).entries, [{ path: '.pbai4devs/agents/revisor/AGENT.md' }]);
|
|
330
|
+
const empty = fixture(t, true);
|
|
331
|
+
run(empty);
|
|
332
|
+
assert.deepEqual(JSON.parse(read(empty, '.pbai4devs/skills.json')), { entries: [] });
|
|
333
|
+
assertCheckReadonly(empty, {}, 0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('skills externas baixadas não entram nos índices; --force salva e limpa cópias indevidas', t => {
|
|
337
|
+
const root = fixture(t);
|
|
338
|
+
write(root, '.pbai4devs/external-skills/README.md', '# Catálogo externo\n');
|
|
339
|
+
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
|
|
340
|
+
for (let index = 0; index < 80; index++) write(root, `.pbai4devs/external-skills/.local/upstream-${index}/SKILL.md`, '# Skill upstream sem frontmatter\n');
|
|
341
|
+
run(root);
|
|
342
|
+
const index = JSON.parse(read(root, '.pbai4devs/skills.json')).entries;
|
|
343
|
+
assert.equal(index.length, 2);
|
|
344
|
+
assert.ok(index.every(entry => entry.path.startsWith('.pbai4devs/skills/')));
|
|
345
|
+
assert.equal(existsSync(join(root, '.codex/skills/upstream-0')), false);
|
|
346
|
+
const sourceBefore = read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md');
|
|
347
|
+
write(root, '.codex/external-skills/.local/upstream-0/SKILL.md', '# Cópia indevida\n');
|
|
348
|
+
write(root, '.claude/external-skills/README.md', pointer('../../.pbai4devs/external-skills/README.md'));
|
|
349
|
+
assertCheckReadonly(root);
|
|
350
|
+
rejected(root, {}, /--force/);
|
|
351
|
+
const result = run(root, { force: true });
|
|
352
|
+
for (const client of ['.codex', '.claude']) {
|
|
353
|
+
assert.equal(existsSync(join(root, client, 'external-skills')), false);
|
|
354
|
+
assert.ok(existsSync(join(result.backupRoot, client, 'external-skills')));
|
|
355
|
+
}
|
|
356
|
+
assert.equal(read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md'), sourceBefore);
|
|
357
|
+
assert.equal(read(result.backupRoot, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
|
|
358
|
+
assertCheckReadonly(root, {}, 0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('--force salva conteúdo próprio, repara ancestrais e preserva arquivos fora do escopo', t => {
|
|
362
|
+
const root = fixture(t);
|
|
363
|
+
run(root);
|
|
364
|
+
write(root, '.claude/scripts/local.sh', 'script humano\n');
|
|
365
|
+
write(root, '.claude/tools/program.exe', Buffer.from([1, 2, 3]));
|
|
366
|
+
write(root, '.claude/docs/plain.md', '# Documento auxiliar\n');
|
|
367
|
+
write(root, '.claude/skills/local.bin', 'custom\n');
|
|
368
|
+
write(root, '.claude/skills/projeto/demo/SKILL.md', 'corrompido\n');
|
|
369
|
+
write(root, '.kimi-code/agents/revisor.md', 'corrompido\n');
|
|
370
|
+
rmSync(join(root, '.codex/skills/outro'), { recursive: true });
|
|
371
|
+
write(root, '.codex/skills/outro', 'ancestral arquivo\n');
|
|
372
|
+
write(root, '.github/prompts/edited.prompt.md', pointer() + '\nRevisar [manual](bar).\n');
|
|
373
|
+
assertCheckReadonly(root);
|
|
374
|
+
assertCheckReadonly(root, { force: true });
|
|
375
|
+
rejected(root, {}, /--force/);
|
|
376
|
+
const result = run(root, { force: true });
|
|
377
|
+
assert.ok(result.backupRoot.startsWith(join(root, '.agent-tests')));
|
|
378
|
+
for (const path of ['.claude/scripts/local.sh', '.claude/tools/program.exe', '.claude/docs/plain.md', '.claude/skills/local.bin', '.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md', '.codex/skills/outro', '.github/prompts/edited.prompt.md']) assert.ok(existsSync(join(result.backupRoot, path)), `Backup ausente: ${path}`);
|
|
379
|
+
assert.deepEqual(readFileSync(join(result.backupRoot, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
|
|
380
|
+
assert.equal(existsSync(join(root, '.claude/scripts')), false);
|
|
381
|
+
assert.equal(existsSync(join(root, '.claude/skills/local.bin')), false);
|
|
382
|
+
assert.equal(existsSync(join(root, '.github/prompts/edited.prompt.md')), false);
|
|
383
|
+
assert.match(read(root, '.codex/skills/outro/SKILL.md'), /name: outro/);
|
|
384
|
+
assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
|
|
385
|
+
assert.match(read(root, '.codex/config.toml'), /config-native-preservada/);
|
|
386
|
+
assertCheckReadonly(root, {}, 0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('atualiza fontes, adiciona clientes, remove recursos e migra agrupado para plano', t => {
|
|
390
|
+
const root = fixture(t);
|
|
391
|
+
run(root);
|
|
392
|
+
write(root, source, skill.replace('Exemplo.', 'Atualizado.'));
|
|
393
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'agente|.extra|plano\n');
|
|
394
|
+
assertCheckReadonly(root);
|
|
395
|
+
run(root);
|
|
396
|
+
assert.match(read(root, '.extra/skills/demo/SKILL.md'), /Atualizado/);
|
|
397
|
+
rmSync(join(root, '.pbai4devs/rules/example/RULE.md'));
|
|
398
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf').replace('agente|.claude\n', 'agente|.claude|plano\n'));
|
|
399
|
+
run(root);
|
|
400
|
+
assert.equal(existsSync(join(root, '.claude/skills/projeto')), false);
|
|
401
|
+
assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
402
|
+
assert.equal(existsSync(join(root, '.codex/rules/example.md')), false);
|
|
403
|
+
assert.deepEqual(JSON.parse(read(root, '.pbai4devs/rules.json')).entries, []);
|
|
404
|
+
assertCheckReadonly(root, {}, 0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('remove linhas legadas e origens nativas ausentes sem replicar documentos', t => {
|
|
408
|
+
const root = fixture(t);
|
|
409
|
+
run(root);
|
|
410
|
+
write(root, 'docs/produto/prd/demo/PRD.md', '# Produto real\n');
|
|
411
|
+
write(root, 'manual/guia com espaço/DOC.md', '# Documento real\n');
|
|
412
|
+
write(root, '.claude/manual/DOC.md', pointer('../../manual/guia%20com%20espa%C3%A7o/DOC.md'));
|
|
413
|
+
write(root, '.github/prompts/missing.prompt.md', pointer());
|
|
414
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + 'documento|manual\n' + 'nativo|.github/prompts/missing.prompt.md|.pbai4devs/commands/missing/COMMAND.md\n');
|
|
415
|
+
assertCheckReadonly(root);
|
|
416
|
+
run(root);
|
|
417
|
+
assert.doesNotMatch(read(root, '.pbai4devs/espelhos.conf'), /documento\||missing/);
|
|
418
|
+
assert.equal(existsSync(join(root, '.claude/manual/DOC.md')), false);
|
|
419
|
+
assert.equal(existsSync(join(root, '.github/prompts/missing.prompt.md')), false);
|
|
420
|
+
assert.ok(existsSync(join(root, 'docs/produto/prd/demo/PRD.md')));
|
|
421
|
+
assert.ok(existsSync(join(root, 'manual/guia com espaço/DOC.md')));
|
|
422
|
+
assertCheckReadonly(root, {}, 0);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('rejeita manifestos fora do escopo, campos extras, caminhos e caixas conflitantes sem mutação', t => {
|
|
426
|
+
const rows = [
|
|
427
|
+
'agente|.git', 'agente|.AGENTS|plano', 'agente|.GIT|plano', 'agente|.AgEnT-TeStS|plano', 'agente|.CLAUDE|plano', 'agente|.claude|plano', 'agente|.other|invalid', 'agente|.other|plano|extra',
|
|
428
|
+
'agente|../outside|plano', 'agente|C:/outside|plano', 'agente|.test//dir|plano', 'agente|.test\\dir|plano', 'agente|.test|plano|extra|another',
|
|
429
|
+
`nativo|.github/workflows/unsafe.md|${source}`, `nativo|.github/WorkFlows/unsafe.md|${source}`, `nativo|.claude/templates/unsafe.md|${source}`, `nativo|.claude/external-skills/unsafe.md|${source}`, `nativo|.claude/README.md|${source}`, `nativo|.outside/prompts/unsafe.md|${source}`,
|
|
430
|
+
'nativo|.claude/prompts/aux.md|.pbai4devs/templates/demo/REFERENCE.md', 'nativo|.github/prompts/aux.md|.pbai4devs/skills/projeto/demo/REFERENCE.md', 'nativo|.claude/prompts/wrong.md|.pbai4devs/commands/check/COMMAND.md|.pbai4devs/agents/revisor/kimi-code.yaml',
|
|
431
|
+
];
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
const root = fixture(t);
|
|
434
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + row + '\n');
|
|
435
|
+
rejected(root, { force: true });
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('colisões de destino, ancestral, metadados e slugs não são resolvidas por --force', t => {
|
|
440
|
+
for (const variant of ['slug', 'source', 'ancestor', 'metadata', 'target-case']) {
|
|
441
|
+
const root = fixture(t);
|
|
442
|
+
let extra;
|
|
443
|
+
if (variant === 'slug') write(root, '.pbai4devs/skills/duplicado/demo/SKILL.md', skill);
|
|
444
|
+
if (variant === 'source') extra = 'nativo|.codex/skills/demo/SKILL.md|.pbai4devs/commands/check/COMMAND.md';
|
|
445
|
+
if (variant === 'ancestor') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/custom.md/child.md|${source}`].join('\n');
|
|
446
|
+
if (variant === 'target-case') extra = [`nativo|.github/custom.md|${source}`, `nativo|.github/CUSTOM.md|${source}`].join('\n');
|
|
447
|
+
if (variant === 'metadata') {
|
|
448
|
+
write(root, '.pbai4devs/agents/revisor/other.yaml', '---\nname: other\n---\n');
|
|
449
|
+
extra = 'nativo|.kimi-code/agents/revisor.md|.pbai4devs/agents/revisor/AGENT.md|.pbai4devs/agents/revisor/other.yaml';
|
|
450
|
+
}
|
|
451
|
+
if (extra) write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + extra + '\n');
|
|
452
|
+
rejected(root, { force: true }, /conflito|ancestral|maiusculas/i);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('metadados incompletos, ausentes e com corpo são rejeitados antes de alterações', t => {
|
|
457
|
+
for (const text of ['name: revisor\n', '---\nname: revisor\n', '---\nname: revisor\n---\ntexto indevido\n', null]) {
|
|
458
|
+
const root = fixture(t);
|
|
459
|
+
const metadata = '.pbai4devs/agents/revisor/kimi-code.yaml';
|
|
460
|
+
if (text === null) rmSync(join(root, metadata)); else write(root, metadata, text);
|
|
461
|
+
rejected(root, { force: true }, /YAML|Metadados|Frontmatter/);
|
|
462
|
+
}
|
|
463
|
+
const root = fixture(t);
|
|
464
|
+
write(root, source, '---\nname: demo\n---\nSem titulo\n');
|
|
465
|
+
rejected(root, { force: true }, /Titulo/);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('symlinks são rejeitados sem efeitos quando o sistema permite criá-los', t => {
|
|
469
|
+
for (const location of ['.claude/tools', '.pbai4devs/skills/projeto/demo/external.md', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf']) {
|
|
470
|
+
const root = fixture(t);
|
|
471
|
+
rmSync(join(root, location), { recursive: true, force: true });
|
|
472
|
+
try { symlinkSync(join(root, 'missing-target'), join(root, location)); }
|
|
473
|
+
catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sistema não permite criar symlinks de teste.'); return; } throw error; }
|
|
474
|
+
rejected(root, { force: true }, /Link simbolico/);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('nomes de controle são rejeitados sem efeitos em todos os sistemas', t => {
|
|
479
|
+
const root = fixture(t);
|
|
480
|
+
const manifest = read(root, '.pbai4devs/espelhos.conf');
|
|
481
|
+
write(root, '.pbai4devs/espelhos.conf', manifest + `nativo|.claude/skills/invalid\u0001.md|${source}\n`);
|
|
482
|
+
rejected(root, { force: true }, /Caminho invalido/);
|
|
483
|
+
assert.match(read(root, 'README.md'), /README raiz preservado/);
|
|
484
|
+
|
|
485
|
+
if (process.platform !== 'win32') {
|
|
486
|
+
const filesystem = fixture(t);
|
|
487
|
+
write(filesystem, '.claude/skills/invalid\nREADME.md', 'nunca deve fragmentar caminho\n');
|
|
488
|
+
rejected(filesystem, { force: true }, /Caminho invalido/);
|
|
489
|
+
assert.match(read(filesystem, 'README.md'), /README raiz preservado/);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('registros não regulares são rejeitados sem efeitos', t => {
|
|
494
|
+
const other = fixture(t);
|
|
495
|
+
mkdirSync(join(other, '.pbai4devs/skills.json'));
|
|
496
|
+
rejected(other, { force: true }, /Registro nao e arquivo/);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('backup recusa raiz simbólica antes de remover conteúdo próprio', t => {
|
|
500
|
+
const root = fixture(t);
|
|
501
|
+
run(root);
|
|
502
|
+
write(root, '.claude/scripts/own.sh', 'preservar\n');
|
|
503
|
+
const outside = join(root, 'outside');
|
|
504
|
+
mkdirSync(outside);
|
|
505
|
+
try { symlinkSync(outside, join(root, '.agent-tests'), 'dir'); }
|
|
506
|
+
catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sistema não permite criar symlinks de teste.'); return; } throw error; }
|
|
507
|
+
rejected(root, { force: true }, /Link simbolico/);
|
|
508
|
+
assert.match(read(root, '.claude/scripts/own.sh'), /preservar/);
|
|
509
|
+
assert.deepEqual(readdirSync(outside), []);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('frontmatter e reconhecimento de apontamentos não confundem instruções próprias', () => {
|
|
513
|
+
assert.equal(header('---\r\nname: test\r\n---\r\nbody\r\n'), '---\nname: test\n---\n');
|
|
514
|
+
assert.ok(isReference(pointer()));
|
|
515
|
+
assert.ok(isReference('# Git\n\nLeia [origem](x) ao realizar operações Git.\n'));
|
|
516
|
+
assert.equal(isReference(pointer() + '\nInstrução própria.\n'), false);
|
|
517
|
+
assert.equal(isReference(pointer() + '\nLeia [outra](x).\n'), false);
|
|
518
|
+
assert.equal(isReference('---\nsem fechamento\n# Titulo\nLeia [origem](x).\n'), false);
|
|
519
|
+
assert.throws(() => header('sem YAML'), /YAML/);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('CLI Node propaga códigos de check e rejeita opções sem invocar shell', t => {
|
|
523
|
+
const root = fixture(t);
|
|
524
|
+
write(root, '.pbai4devs/templates/gitignore/local.ignore', readFileSync(join(project, '.pbai4devs/templates/gitignore/local.ignore'), 'utf8'));
|
|
525
|
+
write(root, '.pbai4devs/templates/gitignore/agents.ignore', readFileSync(join(project, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8'));
|
|
526
|
+
assert.equal(spawnSync('git', ['init', '-q', root]).status, 0);
|
|
527
|
+
run(root, { localIgnores: true });
|
|
528
|
+
const before = snapshot(root);
|
|
529
|
+
const good = spawnSync(process.execPath, [cli, '--root', root, '--check'], { encoding: 'utf8' });
|
|
530
|
+
assert.equal(good.status, 0, good.stderr);
|
|
531
|
+
assert.match(good.stdout, /0 desatualizadas/);
|
|
532
|
+
for (const option of ['--documento', '--invalid', '--root']) {
|
|
533
|
+
const result = spawnSync(process.execPath, [cli, '--root', root, option], { encoding: 'utf8' });
|
|
534
|
+
assert.equal(result.status, 2, result.stderr);
|
|
535
|
+
}
|
|
536
|
+
assert.deepEqual(snapshot(root), before);
|
|
537
|
+
write(root, source, skill.replace('Exemplo.', 'Novo.'));
|
|
538
|
+
const changed = spawnSync(process.execPath, [cli, '--root', root, '--force', '--check'], { encoding: 'utf8' });
|
|
539
|
+
assert.equal(changed.status, 1, changed.stderr);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('gera manifesto e entradas sem estado e atualiza apenas entradas integralmente próprias', t => {
|
|
543
|
+
const root = fixture(t, true);
|
|
544
|
+
write(root, source, skill);
|
|
545
|
+
rmSync(join(root, '.pbai4devs/espelhos.conf'));
|
|
546
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) write(root, `.pbai4devs/templates/entrypoints/${name}`, `---\ndescription: Entrada.\n---\n\n# ${name}\n`);
|
|
547
|
+
write(root, 'CLAUDE.md', '# Instruções humanas\n');
|
|
548
|
+
assertCheckReadonly(root, { generateEntrypoints: true });
|
|
549
|
+
const first = run(root, { generateEntrypoints: true });
|
|
550
|
+
assert.ok(first.ownedOutputs.includes('AGENTS.md'));
|
|
551
|
+
assert.equal(first.ownedOutputs.includes('CLAUDE.md'), false);
|
|
552
|
+
assert.equal(read(root, 'CLAUDE.md'), '# Instruções humanas\n');
|
|
553
|
+
assert.ok(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')));
|
|
554
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
555
|
+
write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', '---\ndescription: Entrada.\n---\n\n# Atualizada\n');
|
|
556
|
+
run(root, { generateEntrypoints: true });
|
|
557
|
+
assert.match(read(root, 'AGENTS.md'), /# Atualizada/);
|
|
558
|
+
write(root, 'AGENTS.md', read(root, 'AGENTS.md') + '\nTexto humano.\n');
|
|
559
|
+
const second = run(root, { generateEntrypoints: true });
|
|
560
|
+
assert.equal(second.ownedOutputs.includes('AGENTS.md'), false);
|
|
561
|
+
assert.match(read(root, 'AGENTS.md'), /Texto humano/);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('entradas da CLI usam ownership integral sem criar registro derivado redundante', t => {
|
|
565
|
+
const root = fixture(t, true);
|
|
566
|
+
const template = '---\ndescription: Entrada.\n---\n\n# Agentes\n';
|
|
567
|
+
const entry = template + '\nResponda em português.\n';
|
|
568
|
+
write(root, source, skill);
|
|
569
|
+
write(root, '.pbai4devs/templates/entrypoints/AGENTS.md', template);
|
|
570
|
+
write(root, 'AGENTS.md', entry);
|
|
571
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ clients: ['codex'], files: { 'AGENTS.md': createHash('sha256').update(entry).digest('hex') } }));
|
|
572
|
+
const result = run(root, { generateEntrypoints: true });
|
|
573
|
+
assert.ok(result.ownedOutputs.includes('AGENTS.md'));
|
|
574
|
+
assert.equal(read(root, 'AGENTS.md'), entry);
|
|
575
|
+
assert.equal(existsSync(join(root, '.pbai4devs/estado/derived.json')), false);
|
|
576
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
function completeCatalogFixture(t) {
|
|
580
|
+
const root = fixture(t, true);
|
|
581
|
+
for (const [kind, file] of Object.entries({ skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md', templates: 'TEMPLATE.md' })) {
|
|
582
|
+
write(root, `.pbai4devs/${kind}/demo/${file}`, `---\nname: demo\n${kind === 'rules' ? 'trigger: always_on\n' : ''}description: ${kind} original.\n---\n\n# Demo ${kind}\n`);
|
|
583
|
+
}
|
|
584
|
+
write(root, '.pbai4devs/templates/demo/config.yaml.template', 'example: true\n');
|
|
585
|
+
write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify(toolConfig()));
|
|
586
|
+
write(root, '.pbai4devs/tools/demo/SCRIPT.mjs', toolWrapper('demo'));
|
|
587
|
+
write(root, '.pbai4devs/scripts/demo/SCRIPT.mjs', '#!/usr/bin/env node\nconsole.log("fixture");\n');
|
|
588
|
+
write(root, '.pbai4devs/scripts/demo/README.md', '---\ndescription: scripts original.\n---\n\n# Demo\n');
|
|
589
|
+
write(root, '.pbai4devs/scripts/demo/tests/TEST.mjs', '// Teste versionado.\n');
|
|
590
|
+
write(root, '.pbai4devs/scripts/demo/tests/SMOKE_DEMO.mjs', '// Smoke versionado.\n');
|
|
591
|
+
write(root, '.pbai4devs/scripts/lib/shared.mjs', 'export const shared = true;\n');
|
|
592
|
+
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
|
|
593
|
+
return root;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
test('nativos recusam principais dentro de estado ignorado e fora do catalogo', t => {
|
|
597
|
+
for (const directory of ['.local', '.cache', 'tmp', 'test-results', 'playwright-report']) {
|
|
598
|
+
const root = fixture(t, true);
|
|
599
|
+
const hidden = `.pbai4devs/skills/${directory}/oculto/SKILL.md`;
|
|
600
|
+
write(root, hidden, skill);
|
|
601
|
+
write(root, '.pbai4devs/espelhos.conf', read(root, '.pbai4devs/espelhos.conf') + `nativo|.codex/skills/oculto/SKILL.md|${hidden}\n`);
|
|
602
|
+
rejected(root, { force: true }, /fora do catalogo canonico/);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test('regras validam trigger e contexto antes de gerar registros ou espelhos', t => {
|
|
607
|
+
const root = fixture(t, true); const path = '.pbai4devs/rules/demo/RULE.md';
|
|
608
|
+
const rule = fields => `---\ndescription: Regra de teste.\n${fields}---\n\n# Regra\n`;
|
|
609
|
+
for (const [fields, expected] of [
|
|
610
|
+
['trigger: always_on\n', { trigger: 'always_on' }],
|
|
611
|
+
['trigger: model_decision\n', { trigger: 'model_decision' }],
|
|
612
|
+
['trigger: manual\n', { trigger: 'manual' }],
|
|
613
|
+
['trigger: glob\nglobs: "**/*.ts"\n', { trigger: 'glob', globs: '**/*.ts' }],
|
|
614
|
+
['trigger: glob\nglob: "src/**"\n', { trigger: 'glob', globs: 'src/**' }],
|
|
615
|
+
]) {
|
|
616
|
+
write(root, path, rule(fields));
|
|
617
|
+
const entry = buildCatalog(root).catalog.rules[0];
|
|
618
|
+
assert.equal(entry.trigger, expected.trigger); assert.equal(entry.globs, expected.globs);
|
|
619
|
+
}
|
|
620
|
+
for (const fields of ['', 'trigger: invalid\n', 'trigger: glob\n', 'trigger: glob\nglobs: []\n', 'trigger: glob\nglob: "a/**"\nglobs: "b/**"\n']) {
|
|
621
|
+
write(root, path, rule(fields)); rejected(root, { force: true }, /Trigger|globs|Metadado|conflitantes/);
|
|
622
|
+
}
|
|
623
|
+
write(root, path, rule('trigger: model_decision\n').replace('description: Regra de teste.\n', ''));
|
|
624
|
+
rejected(root, { force: true }, /Descricao ausente/);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test('outputs ignorados nao entram nos assets; fixtures versionaveis continuam', t => {
|
|
628
|
+
const root = completeCatalogFixture(t);
|
|
629
|
+
const hidden = ['scripts/demo/playwright-report/index.html', 'scripts/demo/.Playwright-MCP/session.json', 'scripts/demo/TEST-RESULTS/report.json', 'scripts/demo/TmP/README.md', 'scripts/demo/run.log', 'scripts/demo/Sessions/state.json', 'scripts/demo/auth.json', 'scripts/demo/CREDENTIALS.JSON', 'scripts/demo/history.jsonl', 'scripts/demo/settings.LOCAL.yaml', 'scripts/demo/.env', 'skills/demo/.agent-tests/trace.json', 'templates/demo/temp/data.json'];
|
|
630
|
+
for (const path of hidden) write(root, `.pbai4devs/${path}`, 'output temporario\n');
|
|
631
|
+
const kept = ['scripts/demo/tests/fixtures/cache/input.json', 'scripts/demo/tests/fixtures/playwright-response.json', 'scripts/demo/tests/fixtures/auth.sample.json', 'scripts/demo/.env.example', 'templates/demo/compose.local.yml.template'];
|
|
632
|
+
for (const path of kept) write(root, `.pbai4devs/${path}`, 'fixture versionavel\n');
|
|
633
|
+
const catalog = buildCatalog(root).catalog;
|
|
634
|
+
const assets = new Set(Object.values(catalog).flatMap(entries => Array.isArray(entries) ? entries.flatMap(entry => entry.assets) : []));
|
|
635
|
+
for (const path of hidden) assert.ok(!assets.has(`.pbai4devs/${path}`), path);
|
|
636
|
+
for (const path of kept) assert.ok(assets.has(`.pbai4devs/${path}`), path);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('catálogo completo deriva oito tipos, ações reais e assets sem estado local', t => {
|
|
640
|
+
const root = completeCatalogFixture(t);
|
|
641
|
+
write(root, '.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=playwright-mcp\n');
|
|
642
|
+
write(root, '.pbai4devs/scripts/browser/SCRIPT.js', '(page) => page.title();\n');
|
|
643
|
+
write(root, '.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Comando com referências.\n---\n\n# Demo\n\nLeia [tool](../../tools/demo/TOOL.json), [script][script], [pasta](../../scripts/demo/) e [template](../../templates/demo/TEMPLATE.md).\n\n[script]: ../../scripts/demo/SCRIPT.mjs\n');
|
|
644
|
+
for (const path of ['.pbai4devs/tools/demo/.local/node_modules/payload/SKILL.md', '.pbai4devs/tools/demo/versions/1.0.0/package.json', '.pbai4devs/external-skills/.local/upstream/SKILL.md', '.pbai4devs/scripts/demo/.cache/large.json']) write(root, path, 'conteúdo local inválido deliberado');
|
|
645
|
+
const before = snapshot(root);
|
|
646
|
+
const first = buildCatalog(root);
|
|
647
|
+
assert.deepEqual(snapshot(root), before, 'Construir o catálogo não pode escrever arquivos.');
|
|
648
|
+
assert.deepEqual(buildCatalog(root), first);
|
|
649
|
+
for (const type of ['skills', 'agents', 'commands', 'rules', 'templates', 'tools', 'scripts', 'externalSkills']) assert.ok(first.catalog[type].length, type);
|
|
650
|
+
assert.deepEqual([...first.outputs.keys()].sort(), ['.pbai4devs/agents.json', '.pbai4devs/catalog.json', '.pbai4devs/rules.json', '.pbai4devs/skills.json']);
|
|
651
|
+
assert.ok(first.catalog.templates[0].assets.includes('.pbai4devs/templates/demo/config.yaml.template'));
|
|
652
|
+
assert.deepEqual(first.catalog.commands[0].references, ['.pbai4devs/scripts/demo', '.pbai4devs/scripts/demo/SCRIPT.mjs', '.pbai4devs/templates/demo/TEMPLATE.md', '.pbai4devs/tools/demo/TOOL.json']);
|
|
653
|
+
const tool = first.catalog.tools[0];
|
|
654
|
+
for (const action of ['install', 'update', 'check', 'info', 'run']) assert.deepEqual(tool.actions[action].argv, ['node', '.pbai4devs/tools/demo/SCRIPT.mjs', action]);
|
|
655
|
+
assert.equal(tool.actions.run.requiresArguments, true);
|
|
656
|
+
const script = first.catalog.scripts.find(entry => entry.id === 'demo');
|
|
657
|
+
assert.deepEqual(script.argv, ['node', '.pbai4devs/scripts/demo/SCRIPT.mjs']);
|
|
658
|
+
assert.deepEqual(script.support.modules, ['.pbai4devs/scripts/lib/shared.mjs']);
|
|
659
|
+
assert.deepEqual(script.support.tests, ['.pbai4devs/scripts/demo/tests/TEST.mjs']);
|
|
660
|
+
assert.deepEqual(script.support.smokes, ['.pbai4devs/scripts/demo/tests/SMOKE_DEMO.mjs']);
|
|
661
|
+
const browser = first.catalog.scripts.find(entry => entry.id === 'browser');
|
|
662
|
+
assert.equal(browser.executor, 'playwright-mcp');
|
|
663
|
+
assert.equal(browser.path, '.pbai4devs/scripts/browser/SCRIPT.js');
|
|
664
|
+
assert.deepEqual(browser.argv, []);
|
|
665
|
+
assert.equal(first.catalog.externalSkills[0].path, '.pbai4devs/external-skills/CATALOG.json');
|
|
666
|
+
assert.equal(first.catalog.externalSkills[0].upstreamPath, 'skills/demo/SKILL.md');
|
|
667
|
+
assert.doesNotMatch(first.outputs.get('.pbai4devs/catalog.json'), /node_modules|\.local|versions|\.cache|installed|conteúdo local|updatedAt/);
|
|
668
|
+
run(root);
|
|
669
|
+
for (const [path, text] of first.outputs) assert.equal(read(root, path), text);
|
|
670
|
+
assertCheckReadonly(root, {}, 0);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test('criar editar e remover recursos atualiza todos os tipos e os índices nativos', t => {
|
|
674
|
+
const root = completeCatalogFixture(t);
|
|
675
|
+
run(root);
|
|
676
|
+
for (const [kind, file] of Object.entries({ skills: 'SKILL.md', agents: 'AGENT.md', commands: 'COMMAND.md', rules: 'RULE.md', templates: 'TEMPLATE.md' })) write(root, `.pbai4devs/${kind}/demo/${file}`, `---\nname: demo\n${kind === 'rules' ? 'trigger: always_on\n' : ''}description: ${kind} atualizado.\n---\n\n# Demo\n`);
|
|
677
|
+
write(root, '.pbai4devs/tools/demo/TOOL.json', JSON.stringify({ ...toolConfig(), description: 'tools atualizado.' }));
|
|
678
|
+
write(root, '.pbai4devs/scripts/demo/README.md', '---\ndescription: scripts atualizado.\n---\n\n# Demo\n');
|
|
679
|
+
const external = externalConfig(); external.skills[0].use = 'externalSkills atualizado.';
|
|
680
|
+
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(external));
|
|
681
|
+
assertCheckReadonly(root);
|
|
682
|
+
run(root);
|
|
683
|
+
const updated = JSON.parse(read(root, '.pbai4devs/catalog.json'));
|
|
684
|
+
for (const kind of ['skills', 'agents', 'commands', 'rules', 'templates', 'tools', 'scripts', 'externalSkills']) assert.match(updated[kind][0].description, /atualizado/);
|
|
685
|
+
for (const kind of ['skills', 'agents', 'commands', 'rules', 'templates/demo', 'tools', 'scripts', 'external-skills']) rmSync(join(root, '.pbai4devs', kind), { recursive: true });
|
|
686
|
+
assertCheckReadonly(root);
|
|
687
|
+
run(root);
|
|
688
|
+
const removed = JSON.parse(read(root, '.pbai4devs/catalog.json'));
|
|
689
|
+
for (const value of Object.values(removed)) if (Array.isArray(value)) assert.deepEqual(value, []);
|
|
690
|
+
for (const kind of ['skills', 'agents', 'rules']) assert.deepEqual(JSON.parse(read(root, `.pbai4devs/${kind}.json`)), { entries: [] });
|
|
691
|
+
assertCheckReadonly(root, {}, 0);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('JSON derivado corrompido é detectado e reparado com backup sem editar fontes', t => {
|
|
695
|
+
const root = completeCatalogFixture(t);
|
|
696
|
+
run(root);
|
|
697
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) write(root, path, '{corrompido');
|
|
698
|
+
assertCheckReadonly(root, { force: true });
|
|
699
|
+
const result = run(root, { force: true });
|
|
700
|
+
for (const path of ['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']) assert.equal(read(result.backupRoot, path), '{corrompido');
|
|
701
|
+
assert.equal(JSON.parse(read(root, '.pbai4devs/catalog.json')).schemaVersion, 1);
|
|
702
|
+
assertCheckReadonly(root, {}, 0);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test('catalogo e seletor aceitam o mesmo SCRIPT.conf e os tres formatos Node', t => {
|
|
706
|
+
const root = completeCatalogFixture(t);
|
|
707
|
+
write(root, '.pbai4devs/scripts/demo/SCRIPT.conf', '# Node explicito\n executor = node \n');
|
|
708
|
+
write(root, '.pbai4devs/scripts/browser/SCRIPT.conf', '# Conector\n executor = playwright-mcp \n');
|
|
709
|
+
write(root, '.pbai4devs/scripts/browser/SCRIPT.js', '(page) => page.title();\n');
|
|
710
|
+
write(root, '.pbai4devs/scripts/common/SCRIPT.cjs', 'module.exports = {};\n');
|
|
711
|
+
write(root, '.pbai4devs/scripts/plain/SCRIPT.js', 'console.log("Node");\n');
|
|
712
|
+
const scripts = buildCatalog(root).catalog.scripts;
|
|
713
|
+
assert.equal(scripts.find(entry => entry.id === 'demo').executor, 'node');
|
|
714
|
+
assert.equal(scripts.find(entry => entry.id === 'demo').configuration, '.pbai4devs/scripts/demo/SCRIPT.conf');
|
|
715
|
+
assert.equal(scripts.find(entry => entry.id === 'browser').executor, 'playwright-mcp');
|
|
716
|
+
assert.equal(scripts.find(entry => entry.id === 'common').path, '.pbai4devs/scripts/common/SCRIPT.cjs');
|
|
717
|
+
assert.equal(scripts.find(entry => entry.id === 'plain').path, '.pbai4devs/scripts/plain/SCRIPT.js');
|
|
718
|
+
for (const invalid of ['', '# vazio\n', 'executor=bash\n', 'executor=node\nexecutor=playwright-mcp\n']) {
|
|
719
|
+
write(root, '.pbai4devs/scripts/demo/SCRIPT.conf', invalid);
|
|
720
|
+
rejected(root, { force: true }, /SCRIPT.conf exige/);
|
|
721
|
+
}
|
|
722
|
+
rmSync(join(root, '.pbai4devs/scripts/demo/SCRIPT.conf'));
|
|
723
|
+
mkdirSync(join(root, '.pbai4devs/scripts/demo/SCRIPT.conf'));
|
|
724
|
+
rejected(root, { force: true }, /SCRIPT.conf deve ser um arquivo/);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test('fontes JSON, referências e entradas de scripts inválidas falham antes de mutações', t => {
|
|
728
|
+
const cases = [
|
|
729
|
+
['.pbai4devs/tools/demo/TOOL.json', '{invalid', /TOOL|JSON/],
|
|
730
|
+
['.pbai4devs/external-skills/CATALOG.json', '{invalid', /JSON/],
|
|
731
|
+
['.pbai4devs/external-skills/CATALOG.json', '{"schemaVersion":1,"skills":[]}', /externo/],
|
|
732
|
+
['.pbai4devs/commands/demo/COMMAND.md', '---\ndescription: Ref faltando.\n---\n\n# Demo\n\nLeia [ausente](../../scripts/missing/SCRIPT.mjs).\n', /ausente/],
|
|
733
|
+
['.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=python\n', /SCRIPT.conf exige/],
|
|
734
|
+
['.pbai4devs/scripts/browser/SCRIPT.conf', 'executor=playwright-mcp\n', /ausente/],
|
|
735
|
+
['.pbai4devs/templates/demo/TEMPLATE.md', '# Sem metadados\n', /Frontmatter/],
|
|
736
|
+
];
|
|
737
|
+
for (const [path, body, pattern] of cases) {
|
|
738
|
+
const root = completeCatalogFixture(t);
|
|
739
|
+
write(root, path, body);
|
|
740
|
+
rejected(root, { force: true }, pattern);
|
|
741
|
+
}
|
|
742
|
+
const root = completeCatalogFixture(t);
|
|
743
|
+
rmSync(join(root, '.pbai4devs/tools/demo/SCRIPT.mjs'));
|
|
744
|
+
rejected(root, { force: true }, /SCRIPT|script|ausente/);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test('descobre principais aninhados sem transformar auxiliares em recursos', t => {
|
|
748
|
+
const root = completeCatalogFixture(t);
|
|
749
|
+
write(root, '.pbai4devs/skills/demo/nested/SKILL.md', '---\nname: nested\ndescription: Skill aninhada.\n---\n\n# Nested\n');
|
|
750
|
+
write(root, '.pbai4devs/skills/demo/REFERENCE.md', '# Guia auxiliar\n');
|
|
751
|
+
write(root, '.pbai4devs/scripts/demo/tests/fixtures/SKILL.md', '# Fixture, não skill compartilhada\n');
|
|
752
|
+
const { catalog } = buildCatalog(root);
|
|
753
|
+
assert.deepEqual(catalog.skills.map(entry => entry.id), ['demo', 'demo/nested']);
|
|
754
|
+
assert.ok(catalog.skills[0].assets.includes('.pbai4devs/skills/demo/REFERENCE.md'));
|
|
755
|
+
assert.ok(catalog.scripts[0].assets.includes('.pbai4devs/scripts/demo/tests/fixtures/SKILL.md'));
|
|
756
|
+
run(root);
|
|
757
|
+
assert.ok(existsSync(join(root, '.codex/skills/nested/SKILL.md')));
|
|
758
|
+
assert.equal(existsSync(join(root, '.codex/skills/fixtures/SKILL.md')), false);
|
|
759
|
+
assertCheckReadonly(root, {}, 0);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('builder e gerenciador externo compartilham o mesmo contrato de validacao', t => {
|
|
763
|
+
const cases = [
|
|
764
|
+
[true, value => value],
|
|
765
|
+
[true, value => ({ ...value, allowlist: ['Fixture/Skills'] })],
|
|
766
|
+
[false, value => ({ ...value, allowlist: ['fixture/skills', 'Fixture/Skills'] })],
|
|
767
|
+
[false, value => ({ ...value, skills: [{ ...value.skills[0], name: 'Invalid Name' }] })],
|
|
768
|
+
[false, value => ({ ...value, allowlist: ['-fixture/skills'], skills: [{ ...value.skills[0], repository: '-fixture/skills' }] })],
|
|
769
|
+
[false, value => ({ ...value, skills: [{ ...value.skills[0], path: 'skills/demo:other/SKILL.md' }] })],
|
|
770
|
+
[false, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: 'https://example.com/LICENSE' }] })],
|
|
771
|
+
[false, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: 'https://github.com/fixture/skills/blob/main/LICENSE' }] })],
|
|
772
|
+
[true, value => ({ ...value, skills: [{ ...value.skills[0], licenseSource: `https://github.com/fixture/skills/blob/${'a'.repeat(40)}/LICENSE` }] })],
|
|
773
|
+
];
|
|
774
|
+
for (const [valid, update] of cases) {
|
|
775
|
+
const value = update(externalConfig());
|
|
776
|
+
const root = fixture(t, true);
|
|
777
|
+
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(value));
|
|
778
|
+
const before = snapshot(root);
|
|
779
|
+
if (valid) {
|
|
780
|
+
assert.doesNotThrow(() => validateExternalCatalog(value));
|
|
781
|
+
const built = buildCatalog(root);
|
|
782
|
+
assert.equal(built.catalog.externalSkills[0].name, value.skills[0].name);
|
|
783
|
+
assert.equal(built.catalog.externalSkills[0].repository, value.skills[0].repository);
|
|
784
|
+
} else {
|
|
785
|
+
assert.throws(() => validateExternalCatalog(value));
|
|
786
|
+
assert.throws(() => buildCatalog(root));
|
|
787
|
+
rejected(root, { force: true });
|
|
788
|
+
}
|
|
789
|
+
assert.deepEqual(snapshot(root), before);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test('formatos por tipo preservam grupos e geram apenas o formato nativo escolhido', t => {
|
|
794
|
+
const root = fixture(t, true);
|
|
795
|
+
write(root, source, skill);
|
|
796
|
+
write(root, '.pbai4devs/commands/projeto/check/COMMAND.md', '---\ndescription: Checagem.\n---\n\n# Checagem\n');
|
|
797
|
+
write(root, '.pbai4devs/rules/projeto/style/RULE.md', '---\ntrigger: glob\nglobs: "**/*.js"\ndescription: Estilo.\n---\n\n# Estilo\n');
|
|
798
|
+
write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
|
|
799
|
+
write(root, '.pbai4devs/espelhos.conf', [
|
|
800
|
+
'agente|.codex|agrupado', 'agente|.claude|agrupado', 'agente|.cursor|agrupado', 'agente|.github|agrupado', 'agente|.gemini|agrupado',
|
|
801
|
+
'formato|.claude|skills|plano', 'formato|.claude|commands|arquivo',
|
|
802
|
+
'formato|.cursor|rules|mdc', 'formato|.github|rules|instrucoes', 'formato|.github|commands|prompt', 'formato|.github|agents|agente',
|
|
803
|
+
'formato|.gemini|commands|arquivo',
|
|
804
|
+
].join('\n') + '\n');
|
|
805
|
+
run(root);
|
|
806
|
+
assert.ok(existsSync(join(root, '.codex/skills/projeto/demo/SKILL.md')));
|
|
807
|
+
assert.ok(existsSync(join(root, '.claude/skills/demo/SKILL.md')));
|
|
808
|
+
assert.ok(existsSync(join(root, '.claude/commands/projeto/check.md')));
|
|
809
|
+
assert.ok(existsSync(join(root, '.github/agents/review/security.agent.md')));
|
|
810
|
+
assert.ok(existsSync(join(root, '.github/prompts/projeto/check.prompt.md')));
|
|
811
|
+
assert.ok(existsSync(join(root, '.github/instructions/projeto/style.instructions.md')));
|
|
812
|
+
assert.ok(existsSync(join(root, '.gemini/commands/projeto/check.toml')));
|
|
813
|
+
assert.match(read(root, '.cursor/rules/projeto/style.mdc'), /globs: "\*\*\/\*\.js"/);
|
|
814
|
+
assert.equal(existsSync(join(root, '.github/commands/projeto/check/COMMAND.md')), false);
|
|
815
|
+
assert.equal(existsSync(join(root, '.cursor/rules/projeto/style/RULE.md')), false);
|
|
816
|
+
assertCheckReadonly(root, {}, 0);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t => {
|
|
820
|
+
const root = fixture(t, true);
|
|
821
|
+
write(root, source, skill);
|
|
822
|
+
write(root, '.pbai4devs/rules/projeto/style/RULE.md', '---\ntrigger: always_on\ndescription: Estilo.\n---\n\n# Estilo\n');
|
|
823
|
+
write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
|
|
824
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
825
|
+
const before = snapshot(root);
|
|
826
|
+
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
827
|
+
assert.deepEqual(snapshot(root), before);
|
|
828
|
+
run(root);
|
|
829
|
+
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/demo' }]);
|
|
830
|
+
assert.deepEqual(JSON.parse(read(root, '.agents/rules.json')).entries, [{ path: '.agents/rules/projeto/style/RULE.md' }]);
|
|
831
|
+
assert.deepEqual(JSON.parse(read(root, '.agents/agents.json')).entries, [{ path: '.agents/agents/review/security/AGENT.md' }]);
|
|
832
|
+
write(root, '.pbai4devs/skills/projeto/new-skill/SKILL.md', skill.replace('name: demo', 'name: new-skill'));
|
|
833
|
+
run(root);
|
|
834
|
+
assert.equal(JSON.parse(read(root, '.agents/skills.json')).entries.length, 2);
|
|
835
|
+
rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
|
|
836
|
+
run(root);
|
|
837
|
+
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
|
|
838
|
+
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
|
|
839
|
+
assertCheckReadonly(root, {}, 0);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test('manifesto de descoberta customizado exige force e recebe backup', t => {
|
|
843
|
+
const root = fixture(t, true);
|
|
844
|
+
write(root, source, skill);
|
|
845
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
846
|
+
run(root);
|
|
847
|
+
const custom = '{"entries":[{"path":"custom/skill"}]}\n';
|
|
848
|
+
write(root, '.agents/skills.json', custom);
|
|
849
|
+
rejected(root, {}, /force/);
|
|
850
|
+
const result = run(root, { force: true });
|
|
851
|
+
assert.equal(readFileSync(join(result.backupRoot, '.agents/skills.json'), 'utf8'), custom);
|
|
852
|
+
assertCheckReadonly(root, {}, 0);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
|
|
856
|
+
const root = fixture(t, true);
|
|
857
|
+
write(root, source, skill);
|
|
858
|
+
for (const row of [
|
|
859
|
+
'formato|.missing|skills|agrupado', 'formato|.codex|unknown|agrupado',
|
|
860
|
+
'formato|.codex|skills|mdc', 'formato|.codex|rules|prompt',
|
|
861
|
+
'formato|.codex|skills|plano\nformato|.codex|skills|agrupado',
|
|
862
|
+
'descoberta|.codex|antigravity', 'descoberta|.agents|antigravity',
|
|
863
|
+
]) {
|
|
864
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.codex|agrupado\n' + row + '\n');
|
|
865
|
+
rejected(root);
|
|
866
|
+
}
|
|
867
|
+
assert.equal(resourcePath('.pbai4devs/commands/team/check/COMMAND.md', 'commands', 'arquivo'), 'commands/team/check.md');
|
|
868
|
+
assert.throws(() => resourcePath(source, 'skills', 'arquivo'), /Skills/);
|
|
869
|
+
});
|