@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,122 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { resolveScript } from '../SCRIPT.mjs';
|
|
8
|
+
import { projectRoot, requireNode } from '../../lib/runtime.mjs';
|
|
9
|
+
|
|
10
|
+
const script = fileURLToPath(new URL('../SCRIPT.mjs', import.meta.url));
|
|
11
|
+
const run = (...args) => spawnSync(process.execPath, [script, ...args], { encoding: 'utf8' });
|
|
12
|
+
function createSymlink(t, target, path, type) {
|
|
13
|
+
try { symlinkSync(target, path, type); return true; }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (!['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
16
|
+
t.skip('Sistema sem permissao para criar symlinks de teste.');
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fixture(t) {
|
|
21
|
+
mkdirSync(join(projectRoot, '.agent-tests'), { recursive: true });
|
|
22
|
+
const root = mkdtempSync(join(projectRoot, '.agent-tests/executor-'));
|
|
23
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
|
+
const write = (path, content) => { const file = join(root, path); mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, content); return file; };
|
|
25
|
+
return { root, write };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('Node 24 e necessario e nao ha tentativa de runtime alternativo', () => {
|
|
29
|
+
assert.doesNotThrow(() => requireNode(24, '24.0.0'));
|
|
30
|
+
assert.throws(() => requireNode(24, '22.22.0'), /Node.js >=24/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('plan resolve pasta ou arquivo sem executar; argumentos e cwd sao preservados', t => {
|
|
34
|
+
const { root, write } = fixture(t); const marker = join(root, 'output.json');
|
|
35
|
+
const file = write('com espacos/SCRIPT.mjs', `import { writeFileSync } from 'node:fs'; writeFileSync(process.argv[2], JSON.stringify({args:process.argv.slice(3),cwd:process.cwd()}));`);
|
|
36
|
+
const planned = run('--plan', dirname(file), marker); assert.equal(planned.status, 0, planned.stderr); assert.match(planned.stdout, /runtime=node/); assert.equal(existsSync(marker), false);
|
|
37
|
+
const value = 'espacos;$(nao execute)'; const result = run(file, marker, value, '--option');
|
|
38
|
+
assert.equal(result.status, 0, result.stderr); assert.deepEqual(JSON.parse(readFileSync(marker, 'utf8')), { args: [value, '--option'], cwd: projectRoot });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('nomes encontram SCRIPT.mjs; falha nao executa outra variante', t => {
|
|
42
|
+
assert.match(run('--plan', 'preparar-ferramentas').stdout, /preparar-ferramentas\/SCRIPT.mjs/);
|
|
43
|
+
const { root, write } = fixture(t); const file = write('failed/SCRIPT.mjs', 'process.exit(23);');
|
|
44
|
+
write('failed/SCRIPT.js', `throw new Error('Nao deve executar variante alternativa');`);
|
|
45
|
+
const result = run(dirname(file)); assert.equal(result.status, 23); assert.doesNotMatch(result.stderr, /alternativa/);
|
|
46
|
+
assert.equal(resolveScript(file).file, file); assert.equal(existsSync(join(root, 'unused')), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('shell e Python sao recusados mesmo quando executaveis; js e cjs usam Node', t => {
|
|
50
|
+
const { write } = fixture(t);
|
|
51
|
+
for (const name of ['SCRIPT.sh', 'SCRIPT.ps1', 'SCRIPT.cmd', 'SCRIPT.bat', 'SCRIPT.py']) assert.equal(run(write(name, 'exit 0')).status, 2);
|
|
52
|
+
for (const name of ['file.js', 'file.cjs']) {
|
|
53
|
+
const result = run(write(name, "console.log('node=passed');")); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /node=passed/);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('caminhos fora da raiz, symlinks de arquivo, pasta ou conf nao escapam da contencao', async t => {
|
|
58
|
+
const { root, write } = fixture(t); const workspace = join(root, 'workspace'); mkdirSync(workspace);
|
|
59
|
+
const external = write('outside.mjs', 'process.exit(0);');
|
|
60
|
+
assert.throws(() => resolveScript(external, { root: workspace }), /fora do repositorio/);
|
|
61
|
+
const internal = write('workspace/inside/SCRIPT.mjs', 'process.exit(0);');
|
|
62
|
+
await t.test('arquivo simbolico', subtest => {
|
|
63
|
+
if (!createSymlink(subtest, internal, join(workspace, 'alias.mjs'))) return;
|
|
64
|
+
assert.throws(() => resolveScript(join(workspace, 'alias.mjs'), { root: workspace }), /simbolico/);
|
|
65
|
+
});
|
|
66
|
+
await t.test('diretorio simbolico', subtest => {
|
|
67
|
+
if (!createSymlink(subtest, dirname(internal), join(workspace, 'alias-dir'), 'dir')) return;
|
|
68
|
+
assert.throws(() => resolveScript(join(workspace, 'alias-dir/SCRIPT.mjs'), { root: workspace }), /simbolico/);
|
|
69
|
+
});
|
|
70
|
+
await t.test('configuracao simbolica', subtest => {
|
|
71
|
+
const config = write('real.conf', 'executor=playwright-mcp');
|
|
72
|
+
if (!createSymlink(subtest, config, join(dirname(internal), 'SCRIPT.conf'))) return;
|
|
73
|
+
assert.throws(() => resolveScript(internal, { root: workspace }), /simbolico/);
|
|
74
|
+
});
|
|
75
|
+
await t.test('configuracao simbolica sem alvo', subtest => {
|
|
76
|
+
const script = write('workspace/dangling/SCRIPT.mjs', 'process.exit(0);');
|
|
77
|
+
if (!createSymlink(subtest, join(workspace, 'missing.conf'), join(dirname(script), 'SCRIPT.conf'))) return;
|
|
78
|
+
assert.throws(() => resolveScript(script, { root: workspace }), /simbolico/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Playwright mantem plano e codigo 69 sem executar o arquivo de conector', t => {
|
|
83
|
+
const { root, write } = fixture(t); const marker = join(root, 'should-not-exist');
|
|
84
|
+
const file = write('browser/SCRIPT.js', `require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'bad');`);
|
|
85
|
+
write('browser/SCRIPT.conf', 'executor=playwright-mcp\n');
|
|
86
|
+
const plan = run('--plan', dirname(file)); assert.equal(plan.status, 0); assert.match(plan.stdout, /executor=playwright-mcp/);
|
|
87
|
+
const result = run(dirname(file)); assert.equal(result.status, 69); assert.match(result.stderr, /conector Playwright/); assert.equal(existsSync(marker), false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('configuracao invalida nunca recai em Node nem executa outro script', t => {
|
|
91
|
+
const { root, write } = fixture(t);
|
|
92
|
+
const marker = join(root, 'must-not-run');
|
|
93
|
+
const file = write('configured/SCRIPT.mjs', `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'bad');`);
|
|
94
|
+
for (const config of ['', '# comentario\n', 'executor=python\n', 'other=node\n', 'executor=node\nexecutor=node\n', 'executor=node\nexecutor=playwright-mcp\n']) {
|
|
95
|
+
write('configured/SCRIPT.conf', config);
|
|
96
|
+
const result = run(file);
|
|
97
|
+
assert.equal(result.status, 2, result.stderr);
|
|
98
|
+
assert.match(result.stderr, /SCRIPT.conf exige/);
|
|
99
|
+
assert.equal(existsSync(marker), false);
|
|
100
|
+
}
|
|
101
|
+
write('configured/SCRIPT.conf', '# Node explicito\n executor = node \n');
|
|
102
|
+
assert.equal(resolveScript(file).executor, 'node');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('configuracao e alvo do conector precisam ser arquivos regulares', t => {
|
|
106
|
+
const { root, write } = fixture(t);
|
|
107
|
+
const script = write('bad-config/SCRIPT.mjs', 'process.exit(0);');
|
|
108
|
+
mkdirSync(join(dirname(script), 'SCRIPT.conf'));
|
|
109
|
+
assert.throws(() => resolveScript(script), /SCRIPT.conf deve ser um arquivo/);
|
|
110
|
+
write('bad-connector/SCRIPT.conf', 'executor=playwright-mcp\n');
|
|
111
|
+
mkdirSync(join(root, 'bad-connector/SCRIPT.js'));
|
|
112
|
+
assert.throws(() => resolveScript(join(root, 'bad-connector')), /SCRIPT.js do conector deve ser um arquivo/);
|
|
113
|
+
const alternate = write('connector/other.mjs', 'process.exit(0);');
|
|
114
|
+
write('connector/SCRIPT.conf', 'executor=playwright-mcp\n');
|
|
115
|
+
write('connector/SCRIPT.js', 'page.title();');
|
|
116
|
+
assert.throws(() => resolveScript(alternate), /exige o arquivo SCRIPT.js configurado/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('ausencia ou caminho invalido falha sem procurar interpretes do sistema', () => {
|
|
120
|
+
assert.equal(run().status, 2); assert.equal(run('missing-agent-script').status, 2);
|
|
121
|
+
assert.equal(run('--help').status, 0);
|
|
122
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Instala e verifica skills externas do catálogo revisado.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Gerenciar skills externas
|
|
6
|
+
|
|
7
|
+
Use Node.js 24+ e prepare o CLI com
|
|
8
|
+
`node .pbai4devs/tools/skills/SCRIPT.mjs install`. Os comandos aceitam um ID do
|
|
9
|
+
[catálogo](../../external-skills/CATALOG.json):
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
node .pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs list
|
|
13
|
+
node .pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs discover cloudflare-web-perf
|
|
14
|
+
node .pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs install cloudflare-web-perf
|
|
15
|
+
node .pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs check cloudflare-web-perf
|
|
16
|
+
node .pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs read cloudflare-web-perf
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`list`, `check` e `read` não acessam a rede. `discover` lista a origem fixada
|
|
20
|
+
usando o CLI oficial. `install` reutiliza a cópia verificada; `update <id>`
|
|
21
|
+
aplica o pin revisado do catálogo ou restaura conteúdo local alterado. Revise
|
|
22
|
+
origem, licença e instruções antes de mudar uma entrada para `reviewed`.
|
|
23
|
+
|
|
24
|
+
Downloads usam `skills add` com `--agent universal --copy --yes`, em pasta
|
|
25
|
+
isolada. O gerenciador confere origem, SHA, caminho e conteúdo; preserva a
|
|
26
|
+
instalação anterior quando o preparo falha. Ele não chama `skills update`,
|
|
27
|
+
não executa scripts da skill e não altera o catálogo nem os espelhos.
|
|
28
|
+
A subpasta `.agents/skills` dentro do download é o formato do CLI universal;
|
|
29
|
+
ela permanece no cache de `.pbai4devs`, sem ocupar o espelho `.agents` do projeto.
|
|
30
|
+
|
|
31
|
+
Teste offline: `node --test .pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs`.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep, posix } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { projectRoot, requireNode, isMain } from '../lib/runtime.mjs';
|
|
7
|
+
import { ToolManager, binaryInvocation, toolEnvironment } from '../preparar-ferramentas/SCRIPT.mjs';
|
|
8
|
+
|
|
9
|
+
const fail = message => { throw new Error(message); };
|
|
10
|
+
const json = file => JSON.parse(readFileSync(file, 'utf8'));
|
|
11
|
+
const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
12
|
+
const repository = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*\/[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
13
|
+
const stat = file => { try { return lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
|
|
14
|
+
|
|
15
|
+
export function safePath(root, path) {
|
|
16
|
+
const full = resolve(root, path); const part = relative(root, full);
|
|
17
|
+
if (isAbsolute(part) || part === '..' || part.startsWith(`..${sep}`)) fail('Caminho fora da pasta permitida.');
|
|
18
|
+
let current = root;
|
|
19
|
+
for (const name of part.split(sep).filter(Boolean)) {
|
|
20
|
+
if (stat(current)?.isDirectory() && readdirSync(current).some(existing => existing !== name && existing.toLowerCase() === name.toLowerCase())) fail(`Caixa conflitante: ${full}`);
|
|
21
|
+
current = join(current, name);
|
|
22
|
+
if (stat(current)?.isSymbolicLink()) fail(`Link simbolico nao permitido: ${full}`);
|
|
23
|
+
}
|
|
24
|
+
return full;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateCatalog(value) {
|
|
28
|
+
if (value?.schemaVersion !== 1 || !Array.isArray(value.allowlist) || !Array.isArray(value.skills)) fail('Catalogo externo invalido.');
|
|
29
|
+
const allowed = new Set(); const ids = new Set();
|
|
30
|
+
for (const source of value.allowlist) {
|
|
31
|
+
if (typeof source !== 'string' || !repository.test(source) || allowed.has(source.toLowerCase())) fail('Origem duplicada ou invalida na allowlist.');
|
|
32
|
+
allowed.add(source.toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
for (const entry of value.skills) {
|
|
35
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) fail('Entrada de skill invalida.');
|
|
36
|
+
if (!slug.test(entry.id || '') || ids.has(entry.id)) fail('Identificador de skill invalido ou duplicado.');
|
|
37
|
+
ids.add(entry.id);
|
|
38
|
+
if (!repository.test(entry.repository || '') || !allowed.has(entry.repository.toLowerCase())) fail(`Origem fora da allowlist: ${entry.id}`);
|
|
39
|
+
if (!/^[a-f0-9]{40}$/.test(entry.commit || '')) fail(`Use SHA completo e imutavel: ${entry.id}`);
|
|
40
|
+
if (typeof entry.path !== 'string' || !entry.path.endsWith('/SKILL.md') || entry.path.includes('\\') || /[\x00-\x1f]/.test(entry.path) || entry.path.split('/').some(part => !part || part === '.' || part === '..' || part.includes(':'))) fail(`Caminho de skill invalido: ${entry.id}`);
|
|
41
|
+
if (!slug.test(entry.name || '') || typeof entry.license !== 'string' || !entry.license.trim() || typeof entry.use !== 'string' || !entry.use.trim() || !['candidate', 'reviewed'].includes(entry.status)) fail(`Metadados incompletos: ${entry.id}`);
|
|
42
|
+
if (entry.notes !== undefined && typeof entry.notes !== 'string') fail(`Notas invalidas: ${entry.id}`);
|
|
43
|
+
if (entry.licenseSource !== undefined) {
|
|
44
|
+
const license = new URL(entry.licenseSource);
|
|
45
|
+
if (license.origin !== 'https://github.com' || !license.pathname.startsWith(`/${entry.repository}/blob/${entry.commit}/`)) fail(`Referencia de licenca deve usar a mesma origem e pin: ${entry.id}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadCatalog(root = projectRoot) {
|
|
52
|
+
return validateCatalog(json(safePath(realpathSync(root), '.pbai4devs/external-skills/CATALOG.json')));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sourceUrl(entry) {
|
|
56
|
+
return `https://github.com/${entry.repository}/tree/${entry.commit}/${posix.dirname(entry.path).split('/').map(encodeURIComponent).join('/')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function skillsArguments(entry, list = false) {
|
|
60
|
+
return ['add', sourceUrl(entry), '--skill', entry.name, ...(list ? ['--list'] : ['--agent', 'universal', '--copy', '--yes'])];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function runSkills(root, cwd, args) {
|
|
64
|
+
const manager = new ToolManager({ root }); const entry = manager.entry('skills'); const installed = manager.check('skills'); const runtime = manager.runtime('skills');
|
|
65
|
+
const [file, argv] = binaryInvocation(installed.prefix, entry.download.package, args, entry.binary || entry.name, runtime.file);
|
|
66
|
+
const environment = toolEnvironment(entry, manager.local('skills'), runtime);
|
|
67
|
+
environment.DISABLE_TELEMETRY = '1'; environment.CI = '1'; environment.XDG_STATE_HOME = join(cwd, '.state');
|
|
68
|
+
const result = spawnSync(file, argv, { cwd, env: environment, encoding: 'utf8', timeout: 180000, windowsHide: true });
|
|
69
|
+
if (result.error || result.status !== 0) fail(`skills: ${result.error?.message || result.stderr?.trim().slice(0, 600) || result.stdout?.trim().slice(0, 600) || result.status}`);
|
|
70
|
+
return result.stdout || '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function descriptor(entry) {
|
|
74
|
+
return { id: entry.id, name: entry.name, repository: entry.repository, commit: entry.commit, path: entry.path, license: entry.license };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function payloadHash(folder) {
|
|
78
|
+
const hash = createHash('sha256');
|
|
79
|
+
function visit(path) {
|
|
80
|
+
for (const name of readdirSync(path).sort()) {
|
|
81
|
+
const file = safePath(folder, relative(folder, join(path, name))); const info = lstatSync(file);
|
|
82
|
+
if (info.isDirectory()) visit(file);
|
|
83
|
+
else if (info.isFile()) {
|
|
84
|
+
const label = relative(folder, file).split(sep).join('/'); const content = readFileSync(file);
|
|
85
|
+
hash.update(`${Buffer.byteLength(label)}:${label}:${content.length}:`); hash.update(content);
|
|
86
|
+
} else fail(`Arquivo de skill nao regular: ${file}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
visit(folder);
|
|
90
|
+
return hash.digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class ExternalSkills {
|
|
94
|
+
constructor({ root = projectRoot, catalog, run = runSkills } = {}) {
|
|
95
|
+
requireNode(); this.root = realpathSync(root); this.catalog = validateCatalog(catalog ?? loadCatalog(this.root)); this.run = run;
|
|
96
|
+
this.home = safePath(this.root, '.pbai4devs/external-skills');
|
|
97
|
+
}
|
|
98
|
+
entry(id, reviewed = false) {
|
|
99
|
+
const entry = this.catalog.skills.find(item => item.id === id);
|
|
100
|
+
if (!entry) fail(`Skill externa desconhecida: ${id}`);
|
|
101
|
+
if (reviewed && entry.status !== 'reviewed') fail(`Revise origem, pin, licenca e instrucoes antes de instalar: ${id}`);
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
local(id) { this.entry(id); return safePath(this.home, `.local/${id}`); }
|
|
105
|
+
validatePayload(folder, entry) {
|
|
106
|
+
safePath(this.home, relative(this.home, folder));
|
|
107
|
+
// O CLI skills usa .agents no staging universal, independentemente do hub.
|
|
108
|
+
const skills = safePath(folder, '.agents/skills');
|
|
109
|
+
const names = readdirSync(skills).sort();
|
|
110
|
+
if (names.length !== 1 || names[0] !== entry.name) fail(`Esperada somente a skill ${entry.name}.`);
|
|
111
|
+
const payload = safePath(folder, `.agents/skills/${entry.name}`); const file = safePath(payload, 'SKILL.md');
|
|
112
|
+
const text = readFileSync(file, 'utf8'); const lines = text.replaceAll('\r', '').split('\n'); const closing = lines.indexOf('---', 1);
|
|
113
|
+
if (lines[0] !== '---' || closing < 1 || !lines.slice(1, closing).some(line => new RegExp(`^name:\\s*['\"]?${entry.name}['\"]?\\s*$`).test(line))) fail('Nome ou frontmatter da skill instalado divergente.');
|
|
114
|
+
const lock = json(safePath(folder, 'skills-lock.json')); const source = lock.skills?.[entry.name];
|
|
115
|
+
if (lock.version !== 2 || Object.keys(lock.skills || {}).length !== 1 || source?.sourceType !== 'github' || source.source?.toLowerCase() !== entry.repository.toLowerCase() || source.ref !== entry.commit || source.skillPath !== entry.path || !/^[a-f0-9]{64}$/.test(source.computedHash || '')) fail('Proveniencia do lock diverge do catalogo revisado.');
|
|
116
|
+
for (const name of readdirSync(folder)) if (!['.agents', '.state', 'skills-lock.json', 'CURRENT.json'].includes(name)) fail(`Saida inesperada do CLI: ${name}`);
|
|
117
|
+
if (readdirSync(join(folder, '.agents')).some(name => name !== 'skills')) fail('Saida inesperada fora da origem unica da skill.');
|
|
118
|
+
return { file, hash: payloadHash(payload) };
|
|
119
|
+
}
|
|
120
|
+
check(id) {
|
|
121
|
+
const entry = this.entry(id, true); const folder = this.local(id);
|
|
122
|
+
const receiptPath = safePath(folder, 'CURRENT.json');
|
|
123
|
+
if (!existsSync(receiptPath)) fail(`Skill ausente: ${id}; use install ${id}.`);
|
|
124
|
+
const receipt = json(receiptPath);
|
|
125
|
+
if (JSON.stringify(receipt.source) !== JSON.stringify(descriptor(entry))) fail(`Pin ou origem mudou; revise o catalogo e use update ${id}.`);
|
|
126
|
+
const payload = this.validatePayload(folder, entry);
|
|
127
|
+
if (payload.hash !== receipt.payloadHash) fail(`Conteudo local alterado: ${id}; use update ${id} para restaurar o pin revisado.`);
|
|
128
|
+
return { id, commit: entry.commit, file: payload.file, license: entry.license };
|
|
129
|
+
}
|
|
130
|
+
read(id) { const info = this.check(id); return { ...info, content: readFileSync(info.file, 'utf8') }; }
|
|
131
|
+
async lock(id, action) {
|
|
132
|
+
this.entry(id, true); const local = safePath(this.home, '.local'); mkdirSync(local, { recursive: true });
|
|
133
|
+
const lock = safePath(local, `.${id}.lock`);
|
|
134
|
+
try { mkdirSync(lock); } catch (error) { if (error.code === 'EEXIST') fail(`Operacao em andamento: ${id}`); throw error; }
|
|
135
|
+
try { return await action(local); } finally { rmSync(lock, { recursive: true, force: true }); }
|
|
136
|
+
}
|
|
137
|
+
async install(id) {
|
|
138
|
+
this.entry(id, true);
|
|
139
|
+
if (stat(this.local(id))) return { ...this.check(id), changed: false };
|
|
140
|
+
return this.replace(id);
|
|
141
|
+
}
|
|
142
|
+
async update(id) {
|
|
143
|
+
this.entry(id, true);
|
|
144
|
+
try { return { ...this.check(id), changed: false }; } catch { return this.replace(id); }
|
|
145
|
+
}
|
|
146
|
+
async replace(id) {
|
|
147
|
+
const entry = this.entry(id, true);
|
|
148
|
+
return this.lock(id, async local => {
|
|
149
|
+
const stage = mkdtempSync(join(local, `.stage-${id}-`)); const target = this.local(id); let previous;
|
|
150
|
+
try {
|
|
151
|
+
await this.run(this.root, stage, skillsArguments(entry));
|
|
152
|
+
const payload = this.validatePayload(stage, entry);
|
|
153
|
+
writeFileSync(join(stage, 'CURRENT.json'), JSON.stringify({ source: descriptor(entry), payloadHash: payload.hash, installedAt: new Date().toISOString() }, null, 2) + '\n');
|
|
154
|
+
if (stat(target)) { previous = join(local, `.previous-${id}-${randomUUID()}`); renameSync(target, previous); }
|
|
155
|
+
try { renameSync(stage, target); }
|
|
156
|
+
catch (error) { if (previous) renameSync(previous, target); throw error; }
|
|
157
|
+
if (previous) rmSync(previous, { recursive: true, force: true });
|
|
158
|
+
return { ...this.check(id), changed: true };
|
|
159
|
+
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async discover(id) {
|
|
163
|
+
const entry = this.entry(id); const local = safePath(this.home, '.local'); mkdirSync(local, { recursive: true });
|
|
164
|
+
const stage = mkdtempSync(join(local, `.discover-${id}-`));
|
|
165
|
+
try { return await this.run(this.root, stage, skillsArguments(entry, true)); }
|
|
166
|
+
finally { rmSync(stage, { recursive: true, force: true }); }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function main(input = process.argv.slice(2), manager) {
|
|
171
|
+
requireNode(); const [mode = 'help', id, ...extra] = input;
|
|
172
|
+
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: list | discover <id> | install <id> | check <id> | update <id> | read <id>. Atualizacoes exigem pin revisado no CATALOG.json.'); return 0; }
|
|
173
|
+
manager ??= new ExternalSkills();
|
|
174
|
+
if (mode === 'list' && !id && !extra.length) { for (const entry of manager.catalog.skills) console.log(JSON.stringify(entry)); return 0; }
|
|
175
|
+
if (!['discover', 'install', 'check', 'update', 'read'].includes(mode) || !id || extra.length) fail('Use uma acao e um unico id do catalogo; consulte --help.');
|
|
176
|
+
const result = await manager[mode](id); console.log(typeof result === 'string' ? result.trim() : JSON.stringify(result)); return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isMain(import.meta.url)) {
|
|
180
|
+
try { process.exitCode = await main(); }
|
|
181
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
182
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { ExternalSkills, loadCatalog, main, runSkills, safePath, skillsArguments, sourceUrl, validateCatalog } from '../SCRIPT.mjs';
|
|
7
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
8
|
+
|
|
9
|
+
function write(file, text) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, text); }
|
|
10
|
+
const sample = () => ({ id: 'vendor-demo', repository: 'vendor/skills', commit: 'a'.repeat(40), path: 'skills/demo/SKILL.md', name: 'demo', license: 'MIT', use: 'Teste offline.', status: 'reviewed' });
|
|
11
|
+
const contents = entry => `---\nname: ${entry.name}\ndescription: Fixture.\n---\n\n# Demo\n\nConteudo ${entry.commit}.\n`;
|
|
12
|
+
function fixture(t) {
|
|
13
|
+
mkdirSync(join(projectRoot, '.agent-tests'), { recursive: true });
|
|
14
|
+
const root = mkdtempSync(join(projectRoot, '.agent-tests/external-skills-'));
|
|
15
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
16
|
+
const entry = sample(); const catalog = { schemaVersion: 1, allowlist: ['vendor/skills'], skills: [entry] };
|
|
17
|
+
write(join(root, '.pbai4devs/external-skills/CATALOG.json'), JSON.stringify(catalog));
|
|
18
|
+
const state = { calls: [], fail: false, corrupt: false, extra: false };
|
|
19
|
+
const manager = new ExternalSkills({ root, catalog, run: async (_root, cwd, args) => {
|
|
20
|
+
state.calls.push({ cwd, args });
|
|
21
|
+
if (state.fail) throw new Error('download interrompido');
|
|
22
|
+
if (args.includes('--list')) return 'demo: Fixture.';
|
|
23
|
+
const base = join(cwd, '.agents/skills', entry.name);
|
|
24
|
+
write(join(base, 'SKILL.md'), contents(entry));
|
|
25
|
+
write(join(base, 'references/REFERENCE.md'), '# Referencia preservada\n');
|
|
26
|
+
write(join(base, 'LICENSE.txt'), 'MIT fixture\n');
|
|
27
|
+
const lock = { version: 2, skills: { [entry.name]: { source: state.corrupt ? 'other/repo' : entry.repository, ref: entry.commit, skillPath: entry.path, sourceType: 'github', computedHash: 'a'.repeat(64) } } };
|
|
28
|
+
write(join(cwd, 'skills-lock.json'), JSON.stringify(lock));
|
|
29
|
+
if (state.extra) write(join(cwd, '.codex/skills/demo/SKILL.md'), contents(entry));
|
|
30
|
+
} });
|
|
31
|
+
return { root, catalog, entry, state, manager, local: join(root, '.pbai4devs/external-skills/.local') };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('catalogo aceita somente origens permitidas, pins completos e metadados de revisao', t => {
|
|
35
|
+
const { root, catalog } = fixture(t);
|
|
36
|
+
assert.equal(loadCatalog(root).skills[0].id, 'vendor-demo');
|
|
37
|
+
for (const change of [{ commit: 'main' }, { commit: 'a'.repeat(39) }, { repository: 'evil/skills' }, { path: '../SKILL.md' }, { path: 'skills\\demo\\SKILL.md' }, { name: '../demo' }, { id: '../demo' }, { status: 'approved' }, { license: '' }, { use: '' }]) {
|
|
38
|
+
assert.throws(() => validateCatalog({ ...catalog, skills: [{ ...sample(), ...change }] }));
|
|
39
|
+
}
|
|
40
|
+
assert.throws(() => validateCatalog({ ...catalog, skills: [sample(), sample()] }), /duplicado/);
|
|
41
|
+
assert.throws(() => validateCatalog({ ...catalog, allowlist: ['vendor/skills', 'VENDOR/skills'] }), /duplicada/);
|
|
42
|
+
assert.throws(() => validateCatalog({ ...catalog, skills: [{ ...sample(), notes: {} }] }), /Notas invalidas/);
|
|
43
|
+
assert.throws(() => validateCatalog({ ...catalog, skills: [{ ...sample(), licenseSource: 'https://github.com/vendor/skills/blob/main/LICENSE' }] }), /mesma origem e pin/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('ajuda e listagem sao locais e nao criam estado', async t => {
|
|
47
|
+
const { manager, state, local } = fixture(t);
|
|
48
|
+
await main([], manager); await main(['list'], manager);
|
|
49
|
+
assert.equal(state.calls.length, 0); assert.equal(existsSync(local), false);
|
|
50
|
+
await assert.rejects(main(['install', 'unknown'], manager), /desconhecida/);
|
|
51
|
+
await assert.rejects(main(['install', 'vendor-demo', 'extra'], manager), /unico id/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('candidatos nao sao instalados; descoberta usa somente origem fixada sem importar', async t => {
|
|
55
|
+
const { manager, entry, state, local } = fixture(t); entry.status = 'candidate';
|
|
56
|
+
await assert.rejects(manager.install(entry.id), /Revise origem/); assert.equal(existsSync(local), false);
|
|
57
|
+
const result = await manager.discover(entry.id); assert.equal(result, 'demo: Fixture.');
|
|
58
|
+
assert.deepEqual(state.calls[0].args, ['add', sourceUrl(entry), '--skill', 'demo', '--list']);
|
|
59
|
+
assert.deepEqual(readdirSync(local), []);
|
|
60
|
+
assert.throws(() => manager.read(entry.id), /Revise origem/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('instala por CLI com copy universal e preserva referencias/licenca em origem isolada', async t => {
|
|
64
|
+
const { manager, entry, state, root } = fixture(t);
|
|
65
|
+
const installed = await manager.install(entry.id); assert.equal(installed.changed, true);
|
|
66
|
+
assert.deepEqual(state.calls[0].args, skillsArguments(entry));
|
|
67
|
+
assert.ok(state.calls[0].args.includes('--copy')); assert.ok(state.calls[0].args.includes('universal')); assert.ok(!state.calls[0].args.includes('--global'));
|
|
68
|
+
assert.equal(readFileSync(installed.file, 'utf8'), contents(entry));
|
|
69
|
+
assert.match(readFileSync(join(dirname(installed.file), 'references/REFERENCE.md'), 'utf8'), /preservada/);
|
|
70
|
+
assert.match(readFileSync(join(dirname(installed.file), 'LICENSE.txt'), 'utf8'), /MIT/);
|
|
71
|
+
for (const path of ['.pbai4devs/skills', '.agents/skills', '.codex', '.claude', '.github']) assert.equal(existsSync(join(root, path)), false);
|
|
72
|
+
assert.equal((await manager.install(entry.id)).changed, false); assert.equal(state.calls.length, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('check e read sao offline; alteracao local exige restauracao do pin revisado', async t => {
|
|
76
|
+
const { manager, entry, state } = fixture(t); await manager.install(entry.id);
|
|
77
|
+
const info = manager.check(entry.id); assert.equal(manager.read(entry.id).content, contents(entry)); assert.equal(state.calls.length, 1);
|
|
78
|
+
write(info.file, contents(entry) + '\nAlterado.\n'); assert.throws(() => manager.check(entry.id), /Conteudo local alterado/);
|
|
79
|
+
assert.throws(() => manager.read(entry.id), /Conteudo local alterado/);
|
|
80
|
+
const result = await manager.update(entry.id); assert.equal(result.changed, true); assert.equal(state.calls.length, 2);
|
|
81
|
+
assert.equal(manager.read(entry.id).content, contents(entry));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('update aplica somente mudanca explicita para outro pin reviewed do catalogo', async t => {
|
|
85
|
+
const { manager, entry, state, root } = fixture(t); await manager.install(entry.id);
|
|
86
|
+
const catalogFile = join(root, '.pbai4devs/external-skills/CATALOG.json'); const catalogBefore = readFileSync(catalogFile, 'utf8');
|
|
87
|
+
assert.equal((await manager.update(entry.id)).changed, false); assert.equal(state.calls.length, 1);
|
|
88
|
+
entry.commit = 'b'.repeat(40); assert.throws(() => manager.check(entry.id), /Pin ou origem mudou/);
|
|
89
|
+
await assert.rejects(manager.install(entry.id), /Pin ou origem mudou/);
|
|
90
|
+
entry.status = 'candidate'; await assert.rejects(manager.update(entry.id), /Revise origem/);
|
|
91
|
+
entry.status = 'reviewed'; await manager.update(entry.id);
|
|
92
|
+
assert.equal(manager.check(entry.id).commit, entry.commit); assert.ok(state.calls[1].args[1].includes(entry.commit));
|
|
93
|
+
assert.equal(readFileSync(catalogFile, 'utf8'), catalogBefore);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('falhas de download, proveniencia ou destinos extras preservam a copia anterior', async t => {
|
|
97
|
+
const { manager, entry, state, local } = fixture(t); const first = await manager.install(entry.id);
|
|
98
|
+
const original = readFileSync(first.file, 'utf8'); const receipt = readFileSync(join(manager.local(entry.id), 'CURRENT.json'), 'utf8'); entry.commit = 'b'.repeat(40);
|
|
99
|
+
for (const mode of ['fail', 'corrupt', 'extra']) {
|
|
100
|
+
state[mode] = true; await assert.rejects(manager.update(entry.id), /interrompido|Proveniencia|inesperada/); state[mode] = false;
|
|
101
|
+
assert.equal(readFileSync(first.file, 'utf8'), original); assert.equal(readFileSync(join(manager.local(entry.id), 'CURRENT.json'), 'utf8'), receipt);
|
|
102
|
+
assert.deepEqual(readdirSync(local), [entry.id]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('locks impedem duas instalacoes simultaneas sem remover lock alheio', async t => {
|
|
107
|
+
const { manager, entry, state, local } = fixture(t); const originalRun = manager.run; let entered; let proceed;
|
|
108
|
+
const started = new Promise(resolve => { entered = resolve; }); const ready = new Promise(resolve => { proceed = resolve; });
|
|
109
|
+
manager.run = async (...args) => { entered(); await ready; return originalRun(...args); };
|
|
110
|
+
const first = manager.install(entry.id); await started;
|
|
111
|
+
await assert.rejects(manager.install(entry.id), /Operacao em andamento/);
|
|
112
|
+
assert.ok(existsSync(join(local, `.${entry.id}.lock`))); proceed(); await first;
|
|
113
|
+
assert.equal(state.calls.length, 1); assert.deepEqual(readdirSync(local), [entry.id]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('estado simbolico nao redireciona escrita para outra pasta', async t => {
|
|
117
|
+
const { manager, state, local, root } = fixture(t); const outside = join(root, 'outside'); mkdirSync(outside);
|
|
118
|
+
try { symlinkSync(outside, local, 'dir'); } catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) { t.skip('Sistema sem permissao para symlinks.'); return; } throw error; }
|
|
119
|
+
assert.throws(() => safePath(manager.home, '.local'), /simbolico/);
|
|
120
|
+
await assert.rejects(manager.install('vendor-demo'), /simbolico/); assert.equal(state.calls.length, 0); assert.deepEqual(readdirSync(outside), []);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('CLI skills real copia fixture local sem escrita nas origens canonicas ou globais', async t => {
|
|
124
|
+
if (!existsSync(join(projectRoot, '.pbai4devs/tools/skills/.local/CURRENT.json'))) { t.skip('CLI skills ainda nao preparado.'); return; }
|
|
125
|
+
const { root } = fixture(t); const name = `external-proof-${process.pid}`; const source = join(root, 'source'); const target = join(root, 'target'); mkdirSync(target);
|
|
126
|
+
const entry = { ...sample(), name }; write(join(source, 'SKILL.md'), contents(entry)); write(join(source, 'LICENSE.txt'), 'MIT fixture\n');
|
|
127
|
+
const canonical = join(projectRoot, '.pbai4devs/skills', name); const global = join(homedir(), '.agents/skills', name);
|
|
128
|
+
assert.equal(existsSync(canonical), false); assert.equal(existsSync(global), false);
|
|
129
|
+
runSkills(projectRoot, target, ['add', source, '--skill', name, '--agent', 'universal', '--copy', '--yes']);
|
|
130
|
+
const file = join(target, '.agents/skills', name, 'SKILL.md'); assert.equal(readFileSync(file, 'utf8'), contents(entry));
|
|
131
|
+
assert.equal(readFileSync(join(dirname(file), 'LICENSE.txt'), 'utf8'), 'MIT fixture\n');
|
|
132
|
+
assert.deepEqual(readdirSync(join(target, '.agents/skills')), [name]); assert.equal(existsSync(join(target, '.codex')), false);
|
|
133
|
+
assert.equal(existsSync(canonical), false); assert.equal(existsSync(global), false);
|
|
134
|
+
});
|