@pathbit/agentdevkit 0.0.7 → 0.0.9
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 +4 -2
- package/.pbai4devs/INSTRUCTIONS.md +4 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
- package/.pbai4devs/rules/documentacao/RULE.md +4 -0
- package/.pbai4devs/rules/qualidade/RULE.md +4 -2
- package/.pbai4devs/scripts/README.md +3 -3
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
- package/.pbai4devs/scripts/lib/gitignore.mjs +84 -15
- package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +193 -49
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +151 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +145 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +30 -29
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
- package/.pbai4devs/templates/README.md +4 -1
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
- package/CHANGELOG.md +19 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +11 -0
- package/docs/guides/documentation.md +63 -0
- package/docs/guides/installation.md +18 -5
- package/docs/guides/resources.md +6 -5
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/guides/troubleshooting.md +6 -6
- package/docs/guides/workflow.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/design.md +12 -0
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +2 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/docs/specs/v0.0.9/README.md +9 -0
- package/docs/specs/v0.0.9/SPEC.md +68 -0
- package/package.json +3 -2
- package/src/cli.mjs +13 -4
- package/src/i18n.mjs +3 -0
- package/src/install.mjs +145 -38
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
10
|
+
import { isReference, synchronize } from '../SCRIPT.mjs';
|
|
11
|
+
|
|
12
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const source = '.pbai4devs/skills/demo/SKILL.md';
|
|
14
|
+
const generated = '.claude/skills/demo/SKILL.md';
|
|
15
|
+
const human = '# Título\n\nLeia [manual](https://example.com/manual).\n';
|
|
16
|
+
function write(root, path, data) { fs.mkdirSync(dirname(join(root, path)), { recursive: true }); fs.writeFileSync(join(root, path), data); }
|
|
17
|
+
function fixture(t) {
|
|
18
|
+
const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'agentdevkit-ownership-')));
|
|
19
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
20
|
+
write(root, '.pbai4devs/espelhos.conf', 'agente|.claude|plano\n');
|
|
21
|
+
write(root, source, '---\nname: demo\ndescription: Exemplo.\n---\n\n# Demo\n');
|
|
22
|
+
for (const name of ['agents.ignore', 'shared.ignore', 'local.ignore', 'clients.json']) write(root, `.pbai4devs/templates/gitignore/${name}`, fs.readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore', name)));
|
|
23
|
+
const initialized = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
24
|
+
assert.equal(initialized.status, 0, initialized.stderr);
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
const run = (root, options = {}) => synchronize({ root, generateEntrypoints: false, localIgnores: false, output() {}, ...options });
|
|
28
|
+
function snapshot(root) {
|
|
29
|
+
const files = {};
|
|
30
|
+
function visit(path, prefix = '') {
|
|
31
|
+
for (const entry of fs.readdirSync(path, { withFileTypes: true })) {
|
|
32
|
+
if (entry.name === '.git') continue;
|
|
33
|
+
const next = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
34
|
+
if (entry.isDirectory()) { files[next] = 'directory'; visit(join(path, entry.name), next); }
|
|
35
|
+
else files[next] = hash(fs.readFileSync(join(path, entry.name)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
visit(root); return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('texto humano com formato de referência nunca comprova propriedade', t => {
|
|
42
|
+
for (const path of [generated, '.claude/skills/human.md']) {
|
|
43
|
+
const root = fixture(t); write(root, path, human);
|
|
44
|
+
const before = snapshot(root);
|
|
45
|
+
assert.equal(isReference(human), true, 'O parser de formato continua independente da propriedade.');
|
|
46
|
+
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
47
|
+
const result = run(root);
|
|
48
|
+
assert.ok(result.preserved.some(item => item.path === path));
|
|
49
|
+
assert.ok(!result.ownedOutputs.includes(path));
|
|
50
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), human);
|
|
51
|
+
assert.equal(snapshot(root)[path], before[path]);
|
|
52
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
53
|
+
const stable = snapshot(root);
|
|
54
|
+
run(root, { force: true });
|
|
55
|
+
assert.deepEqual(snapshot(root), stable);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('hash registrado comprova referência legada sem marcador e diretório vazio humano permanece', t => {
|
|
60
|
+
const root = fixture(t), stale = '.claude/skills/legacy.md';
|
|
61
|
+
write(root, stale, human);
|
|
62
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
|
|
63
|
+
fs.mkdirSync(join(root, '.claude/skills/user-empty'), { recursive: true });
|
|
64
|
+
run(root);
|
|
65
|
+
assert.equal(fs.existsSync(join(root, stale)), false);
|
|
66
|
+
assert.equal(fs.existsSync(join(root, '.claude/skills/user-empty')), true);
|
|
67
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('conflito de ignore é recusado antes de criar recursos ou catálogos', t => {
|
|
71
|
+
const root = fixture(t); write(root, '.gitignore', '!/.agent-tests/\n!/.agent-tests/**\n');
|
|
72
|
+
const before = snapshot(root), exclude = fs.readFileSync(join(root, '.git/info/exclude'));
|
|
73
|
+
assert.throws(() => run(root, { localIgnores: true, force: true }), /Regras humanas/);
|
|
74
|
+
assert.deepEqual(snapshot(root), before);
|
|
75
|
+
assert.deepEqual(fs.readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
76
|
+
assert.equal(fs.existsSync(join(root, generated)), false);
|
|
77
|
+
assert.equal(fs.existsSync(join(root, '.pbai4devs/catalog.json')), false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('atualização de referência própria conserva permissões', t => {
|
|
81
|
+
const root = fixture(t); run(root);
|
|
82
|
+
fs.chmodSync(join(root, generated), 0o600);
|
|
83
|
+
const mode = fs.statSync(join(root, generated)).mode & 0o777;
|
|
84
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
85
|
+
run(root);
|
|
86
|
+
assert.equal(fs.statSync(join(root, generated)).mode & 0o777, mode);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('mudança concorrente de ignore após gravação reverte recursos e preserva a regra humana', t => {
|
|
90
|
+
const root = fixture(t); run(root);
|
|
91
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
92
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
93
|
+
let injected = false;
|
|
94
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
95
|
+
rename(from, to);
|
|
96
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
97
|
+
});
|
|
98
|
+
syncBuiltinESMExports();
|
|
99
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
100
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
101
|
+
assert.equal(injected, true);
|
|
102
|
+
assert.deepEqual(snapshot(root), before);
|
|
103
|
+
assert.equal(fs.readFileSync(join(root, '.git/info/exclude'), 'utf8'), '# concurrent human change\n');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('conteúdo humano concorrente não é apagado durante rollback', t => {
|
|
107
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
108
|
+
let injected = false;
|
|
109
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
110
|
+
rename(from, to);
|
|
111
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); }
|
|
112
|
+
});
|
|
113
|
+
syncBuiltinESMExports();
|
|
114
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
115
|
+
assert.throws(() => run(root), /Mudanca concorrente preservada/);
|
|
116
|
+
assert.equal(injected, true);
|
|
117
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('conteúdo humano é preservado quando a escrita lança erro após promover o arquivo', t => {
|
|
121
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
122
|
+
const failure = new Error('falha depois de promover');
|
|
123
|
+
let injected = false;
|
|
124
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
125
|
+
rename(from, to);
|
|
126
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); throw failure; }
|
|
127
|
+
});
|
|
128
|
+
syncBuiltinESMExports();
|
|
129
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
130
|
+
assert.throws(() => run(root), error => error instanceof AggregateError && error.errors.includes(failure));
|
|
131
|
+
assert.equal(injected, true);
|
|
132
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('rollback recria ancestral podado com suas permissões originais', { skip: process.platform === 'win32' }, t => {
|
|
136
|
+
const root = fixture(t); run(root);
|
|
137
|
+
const parent = join(root, '.claude/skills'); fs.chmodSync(parent, 0o700);
|
|
138
|
+
fs.rmSync(join(root, '.pbai4devs/skills/demo'), { recursive: true });
|
|
139
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
140
|
+
let injected = false;
|
|
141
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
142
|
+
rename(from, to);
|
|
143
|
+
if (!injected && to === join(root, '.pbai4devs/catalog.json')) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
144
|
+
});
|
|
145
|
+
syncBuiltinESMExports();
|
|
146
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
147
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
148
|
+
assert.equal(injected, true);
|
|
149
|
+
assert.deepEqual(snapshot(root), before);
|
|
150
|
+
assert.equal(fs.statSync(parent).mode & 0o777, 0o700);
|
|
151
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
8
10
|
import { projectRoot } from '../../lib/runtime.mjs';
|
|
9
11
|
import { planLocalIgnore, applyLocalIgnore, checkLocalIgnore, planUntrack, applyUntrack, restoreUntrack, assertLocalIgnoreUnchanged } from '../../lib/gitignore.mjs';
|
|
10
12
|
|
|
@@ -354,3 +356,145 @@ test('worktree usa a raiz nativa, exclude comum e seu próprio índice', t => {
|
|
|
354
356
|
assert.equal(git(linked, ['show', ':generated.md']), 'gerado\n');
|
|
355
357
|
assert.equal(git(root, ['ls-files', 'generated.md']), '');
|
|
356
358
|
});
|
|
359
|
+
|
|
360
|
+
test('exceções com nomes arbitrários de artefatos não passam por amostragem', t => {
|
|
361
|
+
for (const [name, directory] of [['.playwright-custom', true], ['.playwright-report.json', false], ['playwright-e2e', true]]) {
|
|
362
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
363
|
+
const human = `!/${name}${directory ? '/' : ''}\n${directory ? `!/${name}/**\n` : ''}`;
|
|
364
|
+
put(root, '.gitignore', human);
|
|
365
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
366
|
+
const plan = planLocalIgnore(root);
|
|
367
|
+
assert.ok(checkLocalIgnore(plan).missing.some(path => path.startsWith(name)), name);
|
|
368
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
369
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), human);
|
|
370
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('arquivo real reaberto por glob é detectado também se surgir após o plano', t => {
|
|
375
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
376
|
+
put(root, '.gitignore', '!/.playwright-report-*.json\n');
|
|
377
|
+
const plan = planLocalIgnore(root), path = '.playwright-report-real.json';
|
|
378
|
+
put(root, path, 'artefato local\n');
|
|
379
|
+
assert.ok(checkLocalIgnore(plan).missing.includes(path));
|
|
380
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
381
|
+
assert.equal(readFileSync(join(root, path), 'utf8'), 'artefato local\n');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('regras customizadas aninhadas continuam válidas e incluem pais ocultos', t => {
|
|
385
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
386
|
+
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
387
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/reports/cache*/\n`);
|
|
388
|
+
const path = '.hidden/reports/cache-custom/out.json';
|
|
389
|
+
put(root, path, 'local\n');
|
|
390
|
+
put(root, '.gitignore', '!/.hidden/reports/cache-custom/\n!/.hidden/reports/cache-custom/**\n');
|
|
391
|
+
const plan = planLocalIgnore(root);
|
|
392
|
+
assert.ok(checkLocalIgnore(plan).missing.includes(path));
|
|
393
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
394
|
+
assert.equal(readFileSync(join(root, path), 'utf8'), 'local\n');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('chaves literais em padrões Git não viram expansão de glob', t => {
|
|
398
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
399
|
+
const policy = readFileSync(join(projectRoot, '.pbai4devs/templates/gitignore/agents.ignore'), 'utf8');
|
|
400
|
+
put(root, '.pbai4devs/templates/gitignore/agents.ignore', `${policy}\n**/cache{keep,archive}/\n`);
|
|
401
|
+
put(root, 'cachekeep/source.txt', 'código humano\n');
|
|
402
|
+
put(root, '.gitignore', '!cachekeep/**\n');
|
|
403
|
+
assert.doesNotThrow(() => applyLocalIgnore(planLocalIgnore(root)));
|
|
404
|
+
assert.equal(readFileSync(join(root, 'cachekeep/source.txt'), 'utf8'), 'código humano\n');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('exceção nominal de um ignore ancestral é conferida no subprojeto', t => {
|
|
408
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
409
|
+
put(root, '.gitignore', '!/modules/app/.playwright-personal/\n!/modules/app/.playwright-personal/**\n');
|
|
410
|
+
const child = join(root, 'modules/app'); mkdirSync(child, { recursive: true });
|
|
411
|
+
const plan = planLocalIgnore(child);
|
|
412
|
+
assert.ok(checkLocalIgnore(plan).missing.some(path => path.startsWith('.playwright-personal/')));
|
|
413
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('escrita de exclusões preserva permissões e detecta alteração concorrente', { skip: process.platform === 'win32' }, t => {
|
|
417
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
418
|
+
const exclude = join(root, '.git/info/exclude'); chmodSync(exclude, 0o600);
|
|
419
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
420
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o600);
|
|
421
|
+
const changed = planLocalIgnore(root, { outputs: ['new-generated.md'] });
|
|
422
|
+
chmodSync(exclude, 0o640);
|
|
423
|
+
assert.throws(() => applyLocalIgnore(changed), /alterada apos planejar/);
|
|
424
|
+
assert.equal(statSync(exclude).mode & 0o777, 0o640);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('fallback e rollback de exclusões mantêm o modo original', { skip: process.platform === 'win32' }, t => {
|
|
428
|
+
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
429
|
+
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o600);
|
|
430
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
431
|
+
assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
432
|
+
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
433
|
+
const exclude = join(root, '.git/info/exclude'), before = readFileSync(exclude);
|
|
434
|
+
chmodSync(exclude, 0o600);
|
|
435
|
+
const plan = planLocalIgnore(root); assert.equal(plan.edits.length, 2);
|
|
436
|
+
Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('injected rollback failure'); } });
|
|
437
|
+
assert.throws(() => applyLocalIgnore(plan), /injected rollback failure/);
|
|
438
|
+
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o600);
|
|
439
|
+
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o600);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('rollback restaura exclusão somente leitura com escrita atômica', { skip: process.platform === 'win32' }, t => {
|
|
443
|
+
const root = fixture(t), ignore = join(root, '.gitignore');
|
|
444
|
+
put(root, '.gitignore', '# humano\n'); chmodSync(ignore, 0o444);
|
|
445
|
+
applyLocalIgnore(planLocalIgnore(root));
|
|
446
|
+
const original = readFileSync(ignore); git(root, ['init', '-q']);
|
|
447
|
+
const exclude = join(root, '.git/info/exclude'), before = readFileSync(exclude);
|
|
448
|
+
chmodSync(exclude, 0o444);
|
|
449
|
+
const plan = planLocalIgnore(root);
|
|
450
|
+
Object.defineProperty(plan.edits[1], 'after', { get() { throw new Error('readonly rollback probe'); } });
|
|
451
|
+
assert.throws(() => applyLocalIgnore(plan), /readonly rollback probe/);
|
|
452
|
+
assert.deepEqual(readFileSync(exclude), before); assert.equal(statSync(exclude).mode & 0o777, 0o444);
|
|
453
|
+
assert.deepEqual(readFileSync(ignore), original); assert.equal(statSync(ignore).mode & 0o777, 0o444);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('projeto aninhado não lê gitignore inacessível do projeto irmão', { skip: process.platform === 'win32' }, t => {
|
|
457
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
458
|
+
const other = join(root, 'other/.gitignore'); put(root, 'other/.gitignore', 'tmp/\n');
|
|
459
|
+
git(root, ['add', 'other/.gitignore']); chmodSync(other, 0o000);
|
|
460
|
+
t.after(() => { try { chmodSync(other, 0o600); } catch {} });
|
|
461
|
+
const child = join(root, 'app'); mkdirSync(child);
|
|
462
|
+
const plan = planLocalIgnore(child);
|
|
463
|
+
applyLocalIgnore(plan);
|
|
464
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(child)).ok, true);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('falha no cleanup após promoção ainda reverte a exclusão publicada', t => {
|
|
468
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
469
|
+
const file = join(root, '.git/info/exclude'), original = readFileSync(file);
|
|
470
|
+
const plan = planLocalIgnore(root), remove = fs.rmSync;
|
|
471
|
+
const injected = new Error('published cleanup failure'); let failed = false;
|
|
472
|
+
try {
|
|
473
|
+
fs.rmSync = (path, options) => {
|
|
474
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) { failed = true; throw injected; }
|
|
475
|
+
return remove(path, options);
|
|
476
|
+
};
|
|
477
|
+
syncBuiltinESMExports();
|
|
478
|
+
assert.throws(() => applyLocalIgnore(plan), error => error === injected);
|
|
479
|
+
} finally { fs.rmSync = remove; syncBuiltinESMExports(); }
|
|
480
|
+
assert.equal(failed, true);
|
|
481
|
+
assert.deepEqual(readFileSync(file), original);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('falha irrecuperável de rollback informa também o erro original', t => {
|
|
485
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
486
|
+
const file = join(root, '.git/info/exclude'), plan = planLocalIgnore(root);
|
|
487
|
+
const remove = fs.rmSync, rename = fs.renameSync;
|
|
488
|
+
const original = new Error('original cleanup failure'), rollback = new Error('restore unavailable');
|
|
489
|
+
let failed = false;
|
|
490
|
+
try {
|
|
491
|
+
fs.rmSync = (path, options) => {
|
|
492
|
+
if (!failed && String(path).startsWith(`${file}.agentdevkit-`) && !existsSync(path)) { failed = true; throw original; }
|
|
493
|
+
return remove(path, options);
|
|
494
|
+
};
|
|
495
|
+
fs.renameSync = (from, to) => { if (failed && to === file) throw rollback; return rename(from, to); };
|
|
496
|
+
syncBuiltinESMExports();
|
|
497
|
+
assert.throws(() => applyLocalIgnore(plan), error => error instanceof AggregateError
|
|
498
|
+
&& error.cause === original && error.errors.includes(rollback));
|
|
499
|
+
} finally { fs.rmSync = remove; fs.renameSync = rename; syncBuiltinESMExports(); }
|
|
500
|
+
});
|
|
@@ -10,6 +10,8 @@ import { buildCatalog } from '../../lib/catalog.mjs';
|
|
|
10
10
|
import { toolWrapper } from '../../preparar-ferramentas/SCRIPT.mjs';
|
|
11
11
|
import { mergeGitignore, ignoreBegin, ignoreEnd, validateIgnoreClients } from '../../lib/gitignore.mjs';
|
|
12
12
|
import './LOCAL_IGNORE.mjs';
|
|
13
|
+
import './HUMAN_OWNERSHIP.mjs';
|
|
14
|
+
import './COEXISTENCE.mjs';
|
|
13
15
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
14
16
|
|
|
15
17
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
@@ -20,6 +22,7 @@ const source = '.pbai4devs/skills/projeto/demo/SKILL.md';
|
|
|
20
22
|
const read = (root, path) => readFileSync(join(root, path), 'utf8');
|
|
21
23
|
const write = (root, path, content) => { mkdirSync(dirname(join(root, path)), { recursive: true }); writeFileSync(join(root, path), content); };
|
|
22
24
|
const pointer = (origin = 'origem.md', title = 'Referencia') => `---\ndescription: Referencia.\n---\n\n# ${title}\n\n${marker}\nLeia e siga [origem](${origin}).\n`;
|
|
25
|
+
const recordOwned = (root, paths) => write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: Object.fromEntries(paths.map(path => [path, createHash('sha256').update(readFileSync(join(root, path))).digest('hex')])), blocks: {} }));
|
|
23
26
|
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
27
|
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
28
|
const run = (root, options = {}) => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {}, ...options });
|
|
@@ -58,7 +61,7 @@ function fixture(t, minimal = false) {
|
|
|
58
61
|
write(root, '.pbai4devs/agents/revisor/kimi-code.yaml', '---\nname: revisor\ntools: [Read, Grep, Glob]\n---\n');
|
|
59
62
|
write(root, '.pbai4devs/agents/revisor/opencode.yaml', '---\nmode: subagent\npermission:\n edit: deny\n---\n');
|
|
60
63
|
write(root, '.pbai4devs/adapters/antigravity/agents/revisor.md', pointer('../../../agents/revisor/AGENT.md', 'Revisor'));
|
|
61
|
-
write(root, '.opencode/agents/revisor/AGENT.md',
|
|
64
|
+
write(root, '.opencode/agents/revisor/AGENT.md', `---\nmode: subagent\npermission:\n edit: deny\n---\n\n# Revisor\n\n${marker}\nLeia e siga [origem](../../../.pbai4devs/agents/revisor/AGENT.md).\n`);
|
|
62
65
|
write(root, '.github/workflows/ci.yml', 'workflow-preservado\n');
|
|
63
66
|
write(root, '.vscode/settings.json', '{"fixture":"config-preservada"}\n');
|
|
64
67
|
write(root, '.vscode/extensions.json', '{"recommendations":[]}\n');
|
|
@@ -333,7 +336,7 @@ test('registros são derivados, determinísticos e --check nunca os corrige', t
|
|
|
333
336
|
assertCheckReadonly(empty, {}, 0);
|
|
334
337
|
});
|
|
335
338
|
|
|
336
|
-
test('skills externas baixadas não entram nos índices;
|
|
339
|
+
test('skills externas baixadas não entram nos índices; cópias humanas permanecem intactas', t => {
|
|
337
340
|
const root = fixture(t);
|
|
338
341
|
write(root, '.pbai4devs/external-skills/README.md', '# Catálogo externo\n');
|
|
339
342
|
write(root, '.pbai4devs/external-skills/CATALOG.json', JSON.stringify(externalConfig()));
|
|
@@ -347,38 +350,34 @@ test('skills externas baixadas não entram nos índices; --force salva e limpa c
|
|
|
347
350
|
write(root, '.codex/external-skills/.local/upstream-0/SKILL.md', '# Cópia indevida\n');
|
|
348
351
|
write(root, '.claude/external-skills/README.md', pointer('../../.pbai4devs/external-skills/README.md'));
|
|
349
352
|
assertCheckReadonly(root);
|
|
350
|
-
rejected(root, {}, /--force/);
|
|
351
353
|
const result = run(root, { force: true });
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
assert.ok(existsSync(join(result.backupRoot, client, 'external-skills')));
|
|
355
|
-
}
|
|
354
|
+
assert.equal(existsSync(join(root, '.claude/external-skills')), false);
|
|
355
|
+
assert.ok(result.preserved.some(item => item.path === '.codex/external-skills'));
|
|
356
356
|
assert.equal(read(root, '.pbai4devs/external-skills/.local/upstream-0/SKILL.md'), sourceBefore);
|
|
357
|
-
assert.equal(read(
|
|
357
|
+
assert.equal(read(root, '.codex/external-skills/.local/upstream-0/SKILL.md'), '# Cópia indevida\n');
|
|
358
358
|
assertCheckReadonly(root, {}, 0);
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
test('--force
|
|
361
|
+
test('--force repara conteúdo registrado e preserva recursos humanos sem propriedade', t => {
|
|
362
362
|
const root = fixture(t);
|
|
363
363
|
run(root);
|
|
364
|
+
recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md']);
|
|
364
365
|
write(root, '.claude/scripts/local.sh', 'script humano\n');
|
|
365
366
|
write(root, '.claude/tools/program.exe', Buffer.from([1, 2, 3]));
|
|
366
367
|
write(root, '.claude/docs/plain.md', '# Documento auxiliar\n');
|
|
367
368
|
write(root, '.claude/skills/local.bin', 'custom\n');
|
|
368
369
|
write(root, '.claude/skills/projeto/demo/SKILL.md', 'corrompido\n');
|
|
369
370
|
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
371
|
write(root, '.github/prompts/edited.prompt.md', pointer() + '\nRevisar [manual](bar).\n');
|
|
373
372
|
assertCheckReadonly(root);
|
|
374
373
|
assertCheckReadonly(root, { force: true });
|
|
375
374
|
rejected(root, {}, /--force/);
|
|
376
375
|
const result = run(root, { force: true });
|
|
377
376
|
assert.ok(result.backupRoot.startsWith(join(root, '.agent-tests')));
|
|
378
|
-
for (const path of ['.claude/
|
|
379
|
-
assert.deepEqual(readFileSync(join(
|
|
380
|
-
assert.equal(
|
|
381
|
-
assert.equal(
|
|
377
|
+
for (const path of ['.claude/skills/projeto/demo/SKILL.md', '.kimi-code/agents/revisor.md', '.github/prompts/edited.prompt.md']) assert.ok(existsSync(join(result.backupRoot, path)), `Backup ausente: ${path}`);
|
|
378
|
+
assert.deepEqual(readFileSync(join(root, '.claude/tools/program.exe')), Buffer.from([1, 2, 3]));
|
|
379
|
+
assert.equal(read(root, '.claude/scripts/local.sh'), 'script humano\n');
|
|
380
|
+
assert.equal(read(root, '.claude/skills/local.bin'), 'custom\n');
|
|
382
381
|
assert.equal(existsSync(join(root, '.github/prompts/edited.prompt.md')), false);
|
|
383
382
|
assert.match(read(root, '.codex/skills/outro/SKILL.md'), /name: outro/);
|
|
384
383
|
assert.match(read(root, '.github/workflows/ci.yml'), /workflow-preservado/);
|
|
@@ -465,8 +464,8 @@ test('metadados incompletos, ausentes e com corpo são rejeitados antes de alter
|
|
|
465
464
|
rejected(root, { force: true }, /Titulo/);
|
|
466
465
|
});
|
|
467
466
|
|
|
468
|
-
test('symlinks são rejeitados sem efeitos quando o sistema permite criá-los', t => {
|
|
469
|
-
for (const location of ['.
|
|
467
|
+
test('symlinks canônicos são rejeitados sem efeitos quando o sistema permite criá-los', t => {
|
|
468
|
+
for (const location of ['.pbai4devs/skills/projeto/demo/external.md', '.pbai4devs/skills.json', '.pbai4devs/espelhos.conf']) {
|
|
470
469
|
const root = fixture(t);
|
|
471
470
|
rmSync(join(root, location), { recursive: true, force: true });
|
|
472
471
|
try { symlinkSync(join(root, 'missing-target'), join(root, location)); }
|
|
@@ -499,6 +498,8 @@ test('registros não regulares são rejeitados sem efeitos', t => {
|
|
|
499
498
|
test('backup recusa raiz simbólica antes de remover conteúdo próprio', t => {
|
|
500
499
|
const root = fixture(t);
|
|
501
500
|
run(root);
|
|
501
|
+
recordOwned(root, ['.claude/skills/projeto/demo/SKILL.md']);
|
|
502
|
+
write(root, '.claude/skills/projeto/demo/SKILL.md', 'registrado alterado\n');
|
|
502
503
|
write(root, '.claude/scripts/own.sh', 'preservar\n');
|
|
503
504
|
const outside = join(root, 'outside');
|
|
504
505
|
mkdirSync(outside);
|
|
@@ -823,33 +824,33 @@ test('manifestos Antigravity acompanham skills, regras e agentes agrupados', t =
|
|
|
823
824
|
write(root, '.pbai4devs/agents/review/security/AGENT.md', '---\nname: security\ndescription: Perfil.\n---\n\n# Perfil\n');
|
|
824
825
|
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
825
826
|
const before = snapshot(root);
|
|
826
|
-
assert.equal(run(root, { check: true }).exitCode, 1);
|
|
827
|
+
assert.equal(run(root, { check: true, generateEntrypoints: true }).exitCode, 1);
|
|
827
828
|
assert.deepEqual(snapshot(root), before);
|
|
828
|
-
run(root);
|
|
829
|
+
run(root, { generateEntrypoints: true });
|
|
829
830
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/demo' }]);
|
|
830
831
|
assert.deepEqual(JSON.parse(read(root, '.agents/rules.json')).entries, [{ path: '.agents/rules/projeto/style/RULE.md' }]);
|
|
831
832
|
assert.deepEqual(JSON.parse(read(root, '.agents/agents.json')).entries, [{ path: '.agents/agents/review/security/AGENT.md' }]);
|
|
832
833
|
write(root, '.pbai4devs/skills/projeto/new-skill/SKILL.md', skill.replace('name: demo', 'name: new-skill'));
|
|
833
|
-
run(root);
|
|
834
|
+
run(root, { generateEntrypoints: true });
|
|
834
835
|
assert.equal(JSON.parse(read(root, '.agents/skills.json')).entries.length, 2);
|
|
835
836
|
rmSync(join(root, '.pbai4devs/skills/projeto/demo'), { recursive: true });
|
|
836
|
-
run(root);
|
|
837
|
+
run(root, { generateEntrypoints: true });
|
|
837
838
|
assert.deepEqual(JSON.parse(read(root, '.agents/skills.json')).entries, [{ path: '.agents/skills/projeto/new-skill' }]);
|
|
838
839
|
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo')), false);
|
|
839
|
-
assertCheckReadonly(root, {}, 0);
|
|
840
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
840
841
|
});
|
|
841
842
|
|
|
842
|
-
test('manifesto de descoberta
|
|
843
|
+
test('manifesto de descoberta humano permanece intacto mesmo com force', t => {
|
|
843
844
|
const root = fixture(t, true);
|
|
844
845
|
write(root, source, skill);
|
|
845
846
|
write(root, '.pbai4devs/espelhos.conf', 'agente|.agents|agrupado\ndescoberta|.agents|antigravity\n');
|
|
846
|
-
|
|
847
|
-
const custom = '{"entries":[{"path":"custom/skill"}]}\n';
|
|
847
|
+
const custom = '{"entries":[{"path":".agents/skills/custom"}]}\n';
|
|
848
848
|
write(root, '.agents/skills.json', custom);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
assert.
|
|
852
|
-
|
|
849
|
+
const result = run(root, { force: true, generateEntrypoints: true });
|
|
850
|
+
assert.equal(read(root, '.agents/skills.json'), custom);
|
|
851
|
+
assert.ok(result.preserved.some(item => item.path === '.agents/skills.json'));
|
|
852
|
+
assert.ok(!result.ownedOutputs.includes('.agents/skills.json'));
|
|
853
|
+
assertCheckReadonly(root, { generateEntrypoints: true }, 0);
|
|
853
854
|
});
|
|
854
855
|
|
|
855
856
|
test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
|
|
@@ -20,6 +20,39 @@ function checkedStat(root, location) {
|
|
|
20
20
|
return item;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function checkedPath(root, path) {
|
|
24
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || /[\\\x00-\x1f\x7f:]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) fail(`Caminho gerenciado invalido: ${path}`);
|
|
25
|
+
let location = root;
|
|
26
|
+
for (const part of path.split('/')) { location = join(location, part); checkedStat(root, location); }
|
|
27
|
+
return location;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ownership(root) {
|
|
31
|
+
const file = checkedPath(root, '.pbai4devs/estado/agentdevkit.json');
|
|
32
|
+
if (!checkedStat(root, file)) return null;
|
|
33
|
+
const readState = location => {
|
|
34
|
+
try { return JSON.parse(readFileSync(location, 'utf8')); }
|
|
35
|
+
catch { fail(`Estado invalido: ${normalize(relative(root, location))}`); }
|
|
36
|
+
};
|
|
37
|
+
const state = readState(file);
|
|
38
|
+
const validMap = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
39
|
+
&& Object.values(value).every(hash => typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash));
|
|
40
|
+
if (state?.schemaVersion !== 1 || !validMap(state.files) || !validMap(state.blocks)) fail('Estado de propriedade invalido.');
|
|
41
|
+
const files = { ...state.files };
|
|
42
|
+
const derivedFile = checkedPath(root, '.pbai4devs/estado/derived.json');
|
|
43
|
+
if (checkedStat(root, derivedFile)) {
|
|
44
|
+
const derived = readState(derivedFile);
|
|
45
|
+
if (derived?.schemaVersion !== 1 || !validMap(derived.entries) || Object.keys(derived.entries).some(path => !['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(path))
|
|
46
|
+
|| !validMap(derived.outputs ?? {}) || Object.keys(derived.outputs ?? {}).some(path => !/^\.[a-z0-9][a-z0-9-]*\/(?:skills|rules|agents)\.json$/.test(path))) fail('Estado de entradas derivadas invalido.');
|
|
47
|
+
Object.assign(files, derived.entries, derived.outputs);
|
|
48
|
+
}
|
|
49
|
+
for (const path of [...Object.keys(files), ...Object.keys(state.blocks)]) {
|
|
50
|
+
const location = checkedPath(root, path);
|
|
51
|
+
if (!checkedStat(root, location)?.isFile()) fail(`Arquivo gerenciado ausente: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
return { files, blocks: state.blocks };
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
export function registryRoots(root) {
|
|
24
57
|
checkedStat(root, join(root, '.pbai4devs'));
|
|
25
58
|
const file = join(root, '.pbai4devs/espelhos.conf');
|
|
@@ -44,7 +77,7 @@ function walk(root, location, files, { skipTools = true } = {}) {
|
|
|
44
77
|
} else if (item.isFile() && ['.md', '.mdc'].includes(extname(location).toLowerCase())) files.add(location);
|
|
45
78
|
}
|
|
46
79
|
|
|
47
|
-
export function collectMarkdown(root, registry = registryRoots(root)) {
|
|
80
|
+
export function collectMarkdown(root, registry = registryRoots(root), { installed = ownership(root) } = {}) {
|
|
48
81
|
const agents = join(root, '.pbai4devs');
|
|
49
82
|
checkedStat(root, agents);
|
|
50
83
|
for (const directory of readdirSync(agents, { withFileTypes: true })) {
|
|
@@ -54,10 +87,18 @@ export function collectMarkdown(root, registry = registryRoots(root)) {
|
|
|
54
87
|
if (item?.isDirectory() && !checkedStat(root, join(location, 'README.md'))?.isFile()) fail(`README ausente: .pbai4devs/${directory.name}/`);
|
|
55
88
|
}
|
|
56
89
|
const files = new Set();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
if (installed) {
|
|
91
|
+
walk(root, agents, files);
|
|
92
|
+
for (const path of Object.keys(installed.files)) {
|
|
93
|
+
const file = checkedPath(root, path);
|
|
94
|
+
if (['.md', '.mdc'].includes(extname(path).toLowerCase())) files.add(file);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const rootMarkdown = readdirSync(root).filter(name => ['.md', '.mdc'].includes(extname(name).toLowerCase()));
|
|
98
|
+
const sources = ['.pbai4devs', 'docs', 'src', 'bin', 'tests', 'scripts', 'examples',
|
|
99
|
+
'.github/PULL_REQUEST_TEMPLATE.md', '.github/ISSUE_TEMPLATE', ...rootMarkdown];
|
|
100
|
+
for (const name of [...sources, ...registry.paths]) walk(root, resolve(root, name), files);
|
|
101
|
+
}
|
|
61
102
|
const tools = join(agents, 'tools');
|
|
62
103
|
if (checkedStat(root, tools)) {
|
|
63
104
|
const guide = join(tools, 'README.md');
|
|
@@ -154,8 +195,14 @@ export function checkCatalog(root, build = buildCatalog) {
|
|
|
154
195
|
return result.catalog;
|
|
155
196
|
}
|
|
156
197
|
|
|
157
|
-
export function checkAgentExtras(root, agents) {
|
|
198
|
+
export function checkAgentExtras(root, agents, { installed = null } = {}) {
|
|
158
199
|
for (const agent of agents) {
|
|
200
|
+
if (installed) {
|
|
201
|
+
const files = Object.keys(installed.files).filter(path => path.startsWith(`${agent}/`));
|
|
202
|
+
if (files.some(path => auxiliary.some(name => path === `${agent}/${name}` || path.startsWith(`${agent}/${name}/`)))) fail(`Auxiliar gerenciado indevido em ${agent}`);
|
|
203
|
+
if (files.some(path => ['README.md', 'INSTRUCTIONS.md'].includes(basename(path)))) fail(`Guia gerenciado indevido em ${agent}`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
159
206
|
for (const name of auxiliary) if (existsSync(join(root, agent, name))) fail(`Auxiliar indevido em ${agent}/${name}`);
|
|
160
207
|
const files = new Set();
|
|
161
208
|
walk(root, join(root, agent), files);
|
|
@@ -183,14 +230,15 @@ export function checkLint(root, files, run = spawnSync) {
|
|
|
183
230
|
|
|
184
231
|
export function verifyResources({ root = projectRoot, sourceOnly = false, synchronize = checkSynchronization, lint = checkLint, catalogCheck = checkCatalog } = {}) {
|
|
185
232
|
requireNode();
|
|
233
|
+
const installed = sourceOnly ? null : ownership(root);
|
|
186
234
|
const built = sourceOnly ? buildCatalog(root) : null;
|
|
187
235
|
if (!sourceOnly) { synchronize(root); catalogCheck(root); }
|
|
188
236
|
const registry = sourceOnly ? { agents: [], paths: [] } : registryRoots(root);
|
|
189
|
-
const files = collectMarkdown(root, registry);
|
|
237
|
+
const files = collectMarkdown(root, registry, { installed });
|
|
190
238
|
const virtualPaths = new Set(sourceOnly ? [...built.outputs.keys(), '.pbai4devs/espelhos.conf'] : []);
|
|
191
239
|
checkMarkdown(root, files, { virtualPaths });
|
|
192
240
|
checkResourceReferences(root, files);
|
|
193
|
-
checkAgentExtras(root, registry.agents);
|
|
241
|
+
checkAgentExtras(root, registry.agents, { installed });
|
|
194
242
|
return { files, messages: [...lint(root, files), 'recursos=passed'] };
|
|
195
243
|
}
|
|
196
244
|
|