@pathbit/agentdevkit 0.0.10 → 0.0.12
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 +6 -0
- package/.pbai4devs/INSTRUCTIONS.md +3 -0
- package/.pbai4devs/agents/README.md +19 -0
- package/.pbai4devs/agents/auditor-seguranca/AGENT.md +20 -0
- package/.pbai4devs/agents/auditor-seguranca/antigravity.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/claude.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/cursor.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/gemini.yaml +6 -0
- package/.pbai4devs/agents/auditor-seguranca/github.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/auditor-seguranca/mimocode.yaml +8 -0
- package/.pbai4devs/agents/auditor-seguranca/opencode.yaml +11 -0
- package/.pbai4devs/agents/documentador/AGENT.md +19 -0
- package/.pbai4devs/agents/documentador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/documentador/claude.yaml +5 -0
- package/.pbai4devs/agents/documentador/cursor.yaml +5 -0
- package/.pbai4devs/agents/documentador/gemini.yaml +6 -0
- package/.pbai4devs/agents/documentador/github.yaml +5 -0
- package/.pbai4devs/agents/documentador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/documentador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/documentador/opencode.yaml +11 -0
- package/.pbai4devs/agents/implementador/AGENT.md +20 -0
- package/.pbai4devs/agents/implementador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/implementador/claude.yaml +5 -0
- package/.pbai4devs/agents/implementador/cursor.yaml +5 -0
- package/.pbai4devs/agents/implementador/gemini.yaml +6 -0
- package/.pbai4devs/agents/implementador/github.yaml +5 -0
- package/.pbai4devs/agents/implementador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/implementador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/implementador/opencode.yaml +11 -0
- package/.pbai4devs/agents/planejador/AGENT.md +19 -0
- package/.pbai4devs/agents/planejador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/planejador/claude.yaml +5 -0
- package/.pbai4devs/agents/planejador/cursor.yaml +5 -0
- package/.pbai4devs/agents/planejador/gemini.yaml +6 -0
- package/.pbai4devs/agents/planejador/github.yaml +5 -0
- package/.pbai4devs/agents/planejador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/planejador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/planejador/opencode.yaml +11 -0
- package/.pbai4devs/agents/validador/AGENT.md +19 -0
- package/.pbai4devs/agents/validador/antigravity.yaml +8 -0
- package/.pbai4devs/agents/validador/claude.yaml +5 -0
- package/.pbai4devs/agents/validador/cursor.yaml +5 -0
- package/.pbai4devs/agents/validador/gemini.yaml +6 -0
- package/.pbai4devs/agents/validador/github.yaml +5 -0
- package/.pbai4devs/agents/validador/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/validador/mimocode.yaml +8 -0
- package/.pbai4devs/agents/validador/opencode.yaml +11 -0
- package/.pbai4devs/commands/README.md +13 -0
- package/.pbai4devs/commands/atualizar-documentacao/COMMAND.md +17 -0
- package/.pbai4devs/commands/auditar-seguranca/COMMAND.md +25 -0
- package/.pbai4devs/commands/implementar-entrega/COMMAND.md +20 -0
- package/.pbai4devs/commands/planejar-entrega/COMMAND.md +17 -0
- package/.pbai4devs/commands/preparar-seguranca/COMMAND.md +22 -0
- package/.pbai4devs/commands/validar-entrega/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-dependencias/COMMAND.md +18 -0
- package/.pbai4devs/commands/verificar-segredos/COMMAND.md +19 -0
- package/.pbai4devs/external-skills/CATALOG.json +39 -1
- package/.pbai4devs/external-skills/README.md +13 -0
- package/.pbai4devs/rules/README.md +4 -0
- package/.pbai4devs/rules/documentacao/RULE.md +2 -0
- package/.pbai4devs/rules/execucao-local/RULE.md +20 -0
- package/.pbai4devs/rules/execucao-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/seguranca/RULE.md +19 -0
- package/.pbai4devs/rules/seguranca/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/auditar-seguranca/README.md +57 -0
- package/.pbai4devs/scripts/auditar-seguranca/SCRIPT.mjs +411 -0
- package/.pbai4devs/scripts/auditar-seguranca/database-zip.mjs +143 -0
- package/.pbai4devs/scripts/auditar-seguranca/rules/javascript.yml +50 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/SMOKE.mjs +74 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/TEST.mjs +247 -0
- package/.pbai4devs/scripts/auditar-seguranca/tests/zip-fixture.mjs +45 -0
- package/.pbai4devs/scripts/lib/README.md +4 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +41 -4
- package/.pbai4devs/scripts/lib/managed-content.mjs +86 -0
- package/.pbai4devs/scripts/lib/managed-toml.mjs +192 -0
- package/.pbai4devs/scripts/lib/native-release.mjs +193 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +8 -2
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +46 -16
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/native-release.test.mjs +103 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +9 -7
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +133 -38
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +25 -15
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +7 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +3 -3
- package/.pbai4devs/scripts/sincronizar-agentes/tests/MANAGED_CONTENT.mjs +318 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +47 -26
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +43 -7
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +3 -2
- package/.pbai4devs/skills/README.md +10 -2
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +5 -1
- package/.pbai4devs/skills/seguranca/REFERENCE.md +104 -0
- package/.pbai4devs/skills/seguranca/corrigir-vulnerabilidade/SKILL.md +27 -0
- package/.pbai4devs/skills/seguranca/modelar-ameacas/SKILL.md +25 -0
- package/.pbai4devs/skills/seguranca/revisar-autorizacao/SKILL.md +29 -0
- package/.pbai4devs/skills/seguranca/revisar-seguranca/SKILL.md +27 -0
- package/.pbai4devs/templates/README.md +2 -0
- package/.pbai4devs/templates/revisao-seguranca/TEMPLATE.md +33 -0
- package/.pbai4devs/tools/README.md +11 -3
- package/.pbai4devs/tools/gitleaks/README.md +22 -0
- package/.pbai4devs/tools/gitleaks/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/gitleaks/TOOL.json +67 -0
- package/.pbai4devs/tools/opengrep/README.md +22 -0
- package/.pbai4devs/tools/opengrep/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opengrep/TOOL.json +71 -0
- package/.pbai4devs/tools/osv-scanner/README.md +23 -0
- package/.pbai4devs/tools/osv-scanner/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/osv-scanner/TOOL.json +70 -0
- package/CHANGELOG.md +17 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +8 -3
- package/docs/README.md +6 -3
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +25 -8
- package/docs/guides/resources.md +48 -1
- package/docs/guides/security-references.md +97 -0
- package/docs/guides/security.md +32 -0
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +12 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +5 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +4 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/docs/specs/security-ecosystem/README.md +9 -0
- package/docs/specs/security-ecosystem/SPEC.md +53 -0
- package/package.json +1 -1
- package/src/install.mjs +139 -63
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { gzipSync, deflateRawSync } from 'node:zlib';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, symlinkSync, existsSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { nativeHash, nativeRelease, validateNativeDownload, validateNativeRelease, downloadNative, extractNative, installNative, checkNative, assertNativeVersion } from '../../lib/native-release.mjs';
|
|
8
|
+
import { ToolManager, platformKey, toolEnvironment } from '../SCRIPT.mjs';
|
|
9
|
+
|
|
10
|
+
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
11
|
+
function fixture(t) { const dir = join(project, '.agent-tests'); mkdirSync(dir, { recursive: true }); const root = mkdtempSync(join(dir, 'native tools ')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; }
|
|
12
|
+
function config(bytes = Buffer.from('#!/bin/sh\necho 1.2.3\n'), version = '1.2.3') {
|
|
13
|
+
return { name: 'native-test', description: 'Native fixture.', group: 'security', source: 'https://github.com/owner/repo',
|
|
14
|
+
platforms: [platformKey()], scripts: { entry: 'SCRIPT.mjs' }, probe: ['--version'],
|
|
15
|
+
download: { provider: 'github-release', repository: 'owner/repo', channel: 'reviewed', version, tag: `v${version}`,
|
|
16
|
+
assets: { [platformKey()]: { file: 'tool.bin', format: 'raw', binary: 'tool', sha256: nativeHash(bytes) } } } };
|
|
17
|
+
}
|
|
18
|
+
function tar(entries) {
|
|
19
|
+
const chunks = [];
|
|
20
|
+
for (const { name, bytes = Buffer.from('binary'), type = '0' } of entries) {
|
|
21
|
+
const header = Buffer.alloc(512); header.write(name, 0, 100); header.write('0000755\0', 100); header.write('0000000\0', 108); header.write('0000000\0', 116);
|
|
22
|
+
header.write(bytes.length.toString(8).padStart(11, '0') + '\0', 124); header.write('00000000000\0', 136); header.fill(32, 148, 156); header.write(type, 156); header.write('ustar\0', 257);
|
|
23
|
+
header.write([...header].reduce((a, b) => a + b, 0).toString(8).padStart(6, '0') + '\0 ', 148);
|
|
24
|
+
chunks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
25
|
+
}
|
|
26
|
+
return gzipSync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
27
|
+
}
|
|
28
|
+
function crc32(bytes) { let crc = 0xffffffff; for (const b of bytes) { crc ^= b; for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); } return (crc ^ 0xffffffff) >>> 0; }
|
|
29
|
+
function zip(entries) {
|
|
30
|
+
const locals = [], centrals = []; let cursor = 0;
|
|
31
|
+
for (const { name, bytes = Buffer.from('binary'), mode = 0o100755, method = 8 } of entries) {
|
|
32
|
+
const filename = Buffer.from(name), compressed = method === 8 ? deflateRawSync(bytes) : bytes, crc = crc32(bytes);
|
|
33
|
+
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50); local.writeUInt16LE(20, 4); local.writeUInt16LE(method, 8); local.writeUInt32LE(crc, 14); local.writeUInt32LE(compressed.length, 18); local.writeUInt32LE(bytes.length, 22); local.writeUInt16LE(filename.length, 26);
|
|
34
|
+
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50); central.writeUInt16LE(3 << 8 | 20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(method, 10); central.writeUInt32LE(crc, 16); central.writeUInt32LE(compressed.length, 20); central.writeUInt32LE(bytes.length, 24); central.writeUInt16LE(filename.length, 28); central.writeUInt32LE((mode << 16) >>> 0, 38); central.writeUInt32LE(cursor, 42);
|
|
35
|
+
locals.push(local, filename, compressed); centrals.push(central, filename); cursor += local.length + filename.length + compressed.length;
|
|
36
|
+
}
|
|
37
|
+
const central = Buffer.concat(centrals), end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10); end.writeUInt32LE(central.length, 12); end.writeUInt32LE(cursor, 16);
|
|
38
|
+
return Buffer.concat([...locals, central, end]);
|
|
39
|
+
}
|
|
40
|
+
const asset = (bytes, format, binary = 'tool') => ({ format, binary, sha256: nativeHash(bytes) });
|
|
41
|
+
|
|
42
|
+
test('native release requires reviewed pins and exact platform assets', () => {
|
|
43
|
+
const entry = config(); assert.equal(validateNativeDownload(entry.download, entry.platforms), entry.download);
|
|
44
|
+
const release = nativeRelease(entry, platformKey()); assert.equal(validateNativeRelease(entry, release, platformKey()), release);
|
|
45
|
+
for (const change of [{ channel: 'latest' }, { tag: 'master' }, { version: '1.2.3-rc.1' }, { repository: '../evil' }, { assets: {} }]) assert.throws(() => validateNativeDownload({ ...entry.download, ...change }, entry.platforms));
|
|
46
|
+
assert.throws(() => validateNativeRelease(entry, { ...release, url: 'https://evil.example/tool' }, platformKey()));
|
|
47
|
+
const invalid = config(); invalid.download.assets[platformKey()].binary = '../escape'; assert.throws(() => validateNativeDownload(invalid.download, invalid.platforms));
|
|
48
|
+
assert.throws(() => assertNativeVersion('version 1.2.30', '1.2.3')); assert.throws(() => assertNativeVersion('version 1.2.3-rc.1', '1.2.3')); assertNativeVersion('Tool v1.2.3\n', '1.2.3');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('native download follows only anonymous official asset hosts with limits', async () => {
|
|
52
|
+
const seen = [];
|
|
53
|
+
const fetcher = async (url, options) => { seen.push({ url, options }); return seen.length === 1 ? new Response(null, { status: 302, headers: { location: 'https://release-assets.githubusercontent.com/file?signature=x' } }) : new Response('data'); };
|
|
54
|
+
assert.equal((await downloadNative('https://github.com/owner/repo/releases/download/v1/tool', { fetcher })).toString(), 'data');
|
|
55
|
+
assert.ok(seen.every(({ options }) => !Object.keys(options.headers).some(key => /authorization|token/i.test(key))));
|
|
56
|
+
await assert.rejects(downloadNative('http://github.com/file', { fetcher }), /Host/);
|
|
57
|
+
await assert.rejects(downloadNative('https://evil.example/file', { fetcher }), /GitHub/);
|
|
58
|
+
await assert.rejects(downloadNative('https://github.com/file', { fetcher: async () => new Response(null, { status: 302, headers: { location: 'https://evil.example/file' } }) }), /Host/);
|
|
59
|
+
await assert.rejects(downloadNative('https://github.com/file', { maxBytes: 2, fetcher: async () => new Response('four') }), /limite/);
|
|
60
|
+
await assert.rejects(downloadNative('https://github.com/file', { fetcher: async () => new Response('missing', { status: 404 }) }), /404/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('native extraction accepts exact raw/tar/zip executable and rejects traversal and links', () => {
|
|
64
|
+
const raw = Buffer.from('raw binary'); assert.deepEqual(extractNative(raw, asset(raw, 'raw')), raw);
|
|
65
|
+
for (const format of ['tar.gz', 'zip']) {
|
|
66
|
+
const pack = format === 'tar.gz' ? tar : zip;
|
|
67
|
+
const archive = pack([{ name: 'LICENSE', bytes: Buffer.from('license') }, { name: 'tool', bytes: raw }]);
|
|
68
|
+
assert.deepEqual(extractNative(archive, asset(archive, format)), raw);
|
|
69
|
+
for (const name of ['../escape', '/absolute', 'C:/drive', 'folder\\escape']) {
|
|
70
|
+
const bad = pack([{ name }, { name: 'tool' }]); assert.throws(() => extractNative(bad, asset(bad, format)), /Caminho/);
|
|
71
|
+
}
|
|
72
|
+
const duplicate = pack([{ name: 'tool' }, { name: 'TOOL' }]); assert.throws(() => extractNative(duplicate, asset(duplicate, format)), /duplicada/);
|
|
73
|
+
const link = pack([{ name: 'link', type: '2', mode: 0o120777 }, { name: 'tool' }]); assert.throws(() => extractNative(link, asset(link, format)), /Links|Link/);
|
|
74
|
+
assert.throws(() => extractNative(archive, { ...asset(archive, format), sha256: '0'.repeat(64) }), /SHA-256/);
|
|
75
|
+
assert.throws(() => extractNative(archive, asset(archive, format, 'absent')), /ausente/);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('native receipts reject tampering and symbolic files without following them', async t => {
|
|
80
|
+
const root = fixture(t), bytes = Buffer.from('binary'), entry = config(bytes); const prefix = join(root, 'stage'); mkdirSync(prefix);
|
|
81
|
+
const receipt = await installNative(prefix, entry, platformKey(), { download: async () => bytes }); assert.equal(checkNative(prefix, entry, platformKey()).binarySha256, receipt.binarySha256);
|
|
82
|
+
const executable = join(prefix, receipt.executable); writeFileSync(executable, 'tampered'); assert.throws(() => checkNative(prefix, entry, platformKey()), /alterado/);
|
|
83
|
+
writeFileSync(executable, bytes); const outside = join(root, 'outside'); writeFileSync(outside, 'preserve'); rmSync(executable);
|
|
84
|
+
try { symlinkSync(outside, executable); } catch (error) { if (['EPERM', 'EACCES'].includes(error.code)) return; throw error; }
|
|
85
|
+
assert.throws(() => checkNative(prefix, entry, platformKey()), /simbolico/); assert.equal(readFileSync(outside, 'utf8'), 'preserve');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('native manager installs, reuses without network, updates reviewed pins and rolls back', { skip: process.platform === 'win32' }, async t => {
|
|
89
|
+
const root = fixture(t); let bytes = Buffer.from('#!/bin/sh\nif [ "$1" = "exit" ]; then exit 37; fi\necho 1.2.3\n'), installs = 0, fail = false;
|
|
90
|
+
let entry = config(bytes); const manager = new ToolManager({ root, catalog: { 'native-test': entry }, install: async prefix => { installs++; if (fail) throw Error('download failed'); await installNative(prefix, entry, platformKey(), { download: async () => bytes }); } });
|
|
91
|
+
const installed = await manager.install(entry.name); assert.equal(installed.version, '1.2.3'); assert.equal(manager.check(entry.name).version, '1.2.3'); assert.equal(manager.run(entry.name, ['exit']), 37);
|
|
92
|
+
manager.fetchRelease = async () => { throw Error('unexpected network'); }; assert.equal((await manager.install(entry.name)).changed, false); assert.equal(installs, 1);
|
|
93
|
+
const old = readFileSync(join(manager.local(entry.name), 'CURRENT.json'));
|
|
94
|
+
bytes = Buffer.from('#!/bin/sh\necho 2.0.0\n'); entry = config(bytes, '2.0.0'); manager.catalog[entry.name] = entry; manager.fetchRelease = async () => nativeRelease(entry, platformKey()); fail = true;
|
|
95
|
+
await assert.rejects(manager.update(entry.name), /download failed/); assert.deepEqual(readFileSync(join(manager.local(entry.name), 'CURRENT.json')), old);
|
|
96
|
+
fail = false; assert.equal((await manager.update(entry.name)).version, '2.0.0'); assert.equal(manager.check(entry.name).version, '2.0.0');
|
|
97
|
+
assert.ok(!existsSync(join(manager.local(entry.name), '.update-lock')));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('native environment removes unrelated credentials while preserving host settings', () => {
|
|
101
|
+
const saved = process.env.NATIVE_TEST_TOKEN; process.env.NATIVE_TEST_TOKEN = 'secret';
|
|
102
|
+
try { assert.equal(toolEnvironment(config(), '/tmp/local').NATIVE_TEST_TOKEN, undefined); } finally { if (saved === undefined) delete process.env.NATIVE_TEST_TOKEN; else process.env.NATIVE_TEST_TOKEN = saved; }
|
|
103
|
+
});
|
|
@@ -12,9 +12,11 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
|
|
|
12
12
|
|
|
13
13
|
`--check` só confere; a execução normal atualiza os recursos. `--force` salva
|
|
14
14
|
conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
|
|
15
|
-
Recursos nativos humanos
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Recursos nativos humanos recebem um bloco curto com a referência ao hub.
|
|
16
|
+
Texto, frontmatter e nomes existentes são preservados. O estado guarda só o
|
|
17
|
+
hash desse bloco; edições externas continuam permitidas. Um bloco alterado
|
|
18
|
+
não é sobrescrito por `--force`. Links humanos continuam opacos.
|
|
19
|
+
Comandos Gemini recebem o complemento dentro do `prompt` TOML.
|
|
18
20
|
|
|
19
21
|
Metadados vêm da fonte ou do YAML nativo declarado no manifesto. Regras exigem
|
|
20
22
|
`trigger` válido; `glob` exige `globs`. Origens nativas precisam pertencer ao
|
|
@@ -36,9 +38,9 @@ a seleção, use `agentdevkit agents <lista>`; não edite o manifesto gerado.
|
|
|
36
38
|
Os derivados ficam locais: catálogos, manifesto, estado e apontamentos são
|
|
37
39
|
excluídos pelo Git. O bloco em `.git/info/exclude` preserva regras próprias;
|
|
38
40
|
sem Git, usa um bloco na raiz que migra ao executar depois de `git init`.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
Somente arquivos integralmente gerados são excluídos; pastas de clientes podem
|
|
42
|
+
conter arquivos humanos versionados. Conteúdo próprio não é desindexado pela
|
|
43
|
+
presença de um bloco gerenciado.
|
|
42
44
|
Entradas novas usam os templates; hashes em `estado/derived.json` permitem
|
|
43
|
-
atualizar
|
|
45
|
+
atualizar arquivos gerados e blocos separadamente, preservando alterações humanas.
|
|
44
46
|
`--check` também acusa derivados ainda rastreados; não altera o índice.
|
|
@@ -8,6 +8,8 @@ import { buildCatalog } from '../lib/catalog.mjs';
|
|
|
8
8
|
import { resourcePath } from '../lib/resource-path.mjs';
|
|
9
9
|
import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
|
|
10
10
|
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
11
|
+
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
12
|
+
import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
11
13
|
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
12
14
|
|
|
13
15
|
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
@@ -72,7 +74,7 @@ function link(target, source) {
|
|
|
72
74
|
|
|
73
75
|
export { resourcePath } from '../lib/resource-path.mjs';
|
|
74
76
|
|
|
75
|
-
export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, output = console.log } = {}) {
|
|
77
|
+
export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, recordDerivedState = true, output = console.log } = {}) {
|
|
76
78
|
requireNode();
|
|
77
79
|
root = realpathSync(resolve(root));
|
|
78
80
|
const absolute = value => join(root, ...value.split('/'));
|
|
@@ -95,7 +97,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
95
97
|
const path = safe(value);
|
|
96
98
|
const item = stat(path);
|
|
97
99
|
if (!item) return [];
|
|
98
|
-
if (item.isFile()) return !markdownOnly ||
|
|
100
|
+
if (item.isFile()) return !markdownOnly || /\.md$/i.test(value) ? [value] : [];
|
|
99
101
|
if (!item.isDirectory()) fail(`Tipo de arquivo nao suportado: ${value}`);
|
|
100
102
|
const files = [];
|
|
101
103
|
function visit(folder) {
|
|
@@ -107,22 +109,27 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
107
109
|
if (info.isDirectory()) {
|
|
108
110
|
files.push(...scan(child, { markdownOnly, prune }));
|
|
109
111
|
} else if (info.isFile()) {
|
|
110
|
-
if (!markdownOnly ||
|
|
112
|
+
if (!markdownOnly || /\.md$/i.test(name)) files.push(child);
|
|
111
113
|
} else fail(`Tipo de arquivo nao suportado: ${child}`);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
visit(value);
|
|
115
117
|
return files;
|
|
116
118
|
}
|
|
117
|
-
const
|
|
119
|
+
const mergedInputs = new Map();
|
|
120
|
+
const captureInput = (path, bytes) => { if (!mergedInputs.has(path)) mergedInputs.set(path, bytes); };
|
|
121
|
+
const referenceFile = (value, supplied) => {
|
|
118
122
|
const info = stat(safe(value));
|
|
119
123
|
if (!info?.isFile() || info.size > 1024 * 1024) return false;
|
|
120
|
-
const bytes = readFileSync(absolute(value)), text = normalize(bytes.toString('utf8'));
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
+
const bytes = supplied ?? readFileSync(absolute(value)), text = normalize(bytes.toString('utf8'));
|
|
125
|
+
captureInput(value, bytes);
|
|
126
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
127
|
+
const recordedHash = priorFiles[value];
|
|
128
|
+
if (recordedHash && recordedHash !== digest) return false;
|
|
129
|
+
const recorded = recordedHash === digest;
|
|
130
|
+
return isReference(text) && recorded;
|
|
124
131
|
};
|
|
125
|
-
const onlyReferences = value => { const files = scan(value); return files.length > 0 && ![...preserved.keys()].some(path => path === value || path.startsWith(`${value}/`)) && files.every(file => /\.(md|mdc|toml)
|
|
132
|
+
const onlyReferences = value => { const files = scan(value); return files.length > 0 && ![...preserved.keys()].some(path => path === value || path.startsWith(`${value}/`)) && files.every(file => /\.(md|mdc|toml)$/i.test(file) && referenceFile(file)); };
|
|
126
133
|
const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
|
|
127
134
|
const agents = new Map();
|
|
128
135
|
const formats = new Map();
|
|
@@ -132,21 +139,40 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
132
139
|
const legacy = [];
|
|
133
140
|
const config = '.pbai4devs/espelhos.conf';
|
|
134
141
|
const configExists = stat(safe(config));
|
|
142
|
+
captureInput(config, configExists ? readFileSync(absolute(config)) : null);
|
|
135
143
|
const stateFile = '.pbai4devs/estado/agentdevkit.json';
|
|
136
144
|
const installed = stat(safe(stateFile)) ? JSON.parse(read(safe(stateFile))) : null;
|
|
137
145
|
const configText = configExists ? read(safe(config)) : adapterManifest(root, { clients: installed?.clients });
|
|
138
146
|
const rows = configText.split('\n');
|
|
139
147
|
const entryRegistry = '.pbai4devs/estado/derived.json';
|
|
140
|
-
const
|
|
141
|
-
|
|
148
|
+
const registryBytes = stat(safe(entryRegistry)) ? readFileSync(absolute(entryRegistry)) : null;
|
|
149
|
+
captureInput(entryRegistry, registryBytes);
|
|
150
|
+
const previousEntries = registryBytes ? JSON.parse(registryBytes.toString('utf8')) : { schemaVersion: 1, entries: {} };
|
|
151
|
+
if (previousEntries?.schemaVersion !== 1 || !previousEntries.entries || typeof previousEntries.entries !== 'object' || Array.isArray(previousEntries.entries) || Object.entries(previousEntries.entries).some(([name, hash]) => !canonicalEntrypoint(name) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de entradas derivadas invalido.');
|
|
142
152
|
const previousOutputs = previousEntries.outputs ?? {};
|
|
143
153
|
if (!previousOutputs || typeof previousOutputs !== 'object' || Array.isArray(previousOutputs) || Object.entries(previousOutputs).some(([path, hash]) => !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/(skills|rules|agents)\.json$/.test(path) || reserved.has(path.split('/')[0].toLowerCase()) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de saidas derivadas invalido.');
|
|
144
|
-
const
|
|
154
|
+
const validResourcePath = path => {
|
|
155
|
+
if (typeof path !== 'string' || !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/.+/.test(path)) return false;
|
|
156
|
+
const [root, scope] = path.split('/');
|
|
157
|
+
return !reserved.has(root.toLowerCase()) && ![...auxiliary, 'workflows'].includes(scope.toLowerCase())
|
|
158
|
+
&& !['readme.md', 'instructions.md'].includes(posix.basename(path).toLowerCase())
|
|
159
|
+
&& (/\.(md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'));
|
|
160
|
+
};
|
|
161
|
+
for (const [name, values] of [['files', previousEntries.files ?? {}], ['blocks', previousEntries.blocks ?? {}]]) {
|
|
162
|
+
if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && canonicalEntrypoint(path)) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
|
|
163
|
+
for (const path of Object.keys(values)) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
164
|
+
}
|
|
165
|
+
const removedBlocks = new Set(previousEntries.removedBlocks ?? []);
|
|
166
|
+
if (!Array.isArray(previousEntries.removedBlocks ?? []) || [...removedBlocks].some(path => typeof path !== 'string' || !(validResourcePath(path) || canonicalEntrypoint(path)))) fail('Registro de blocos removidos invalido.');
|
|
167
|
+
for (const path of removedBlocks) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
168
|
+
const priorFiles = { ...installed?.files, ...previousEntries.files };
|
|
169
|
+
const priorBlocks = { ...Object.fromEntries(Object.entries(installed?.blocks ?? {}).filter(([path]) => !removedBlocks.has(path))), ...previousEntries.blocks };
|
|
170
|
+
const ownedPaths = new Set([...Object.keys(priorFiles), ...Object.keys(priorBlocks), ...Object.keys(previousEntries.entries), ...Object.keys(previousOutputs)]);
|
|
145
171
|
const nativeRoots = new Set(rows.map(row => row.split('|')).filter(([type]) => ['agente', 'gitignore'].includes(type)).map(([, target]) => target));
|
|
146
172
|
const preserved = new Map();
|
|
147
173
|
const remember = (path, reason = 'user-owned') => { preserved.set(path, { path, reason }); return true; };
|
|
148
174
|
function preserveBoundary(value) {
|
|
149
|
-
if (!nativeRoots.has(value.split('/')[0]) && !
|
|
175
|
+
if (!nativeRoots.has(value.split('/')[0]) && !canonicalEntrypoint(value) && value !== 'README.md') return false;
|
|
150
176
|
const boundary = nativePathBoundary(root, value, ownedPaths, { preserveRootAliases: true });
|
|
151
177
|
return boundary ? remember(boundary.path, boundary.reason) : false;
|
|
152
178
|
}
|
|
@@ -160,6 +186,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
160
186
|
function preserveUnknown(value) {
|
|
161
187
|
if (preserveBoundary(value)) return true;
|
|
162
188
|
const info = stat(safe(value));
|
|
189
|
+
if (info?.isFile() && (/\.(md|mdc)$/i.test(value) || value.startsWith('.gemini/commands/') && value.endsWith('.toml'))) return false;
|
|
163
190
|
return info && !referenceFile(value) && !claimed(value) ? remember(value) : false;
|
|
164
191
|
}
|
|
165
192
|
function preserveNativeType(value, info, type) {
|
|
@@ -267,6 +294,13 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
267
294
|
const backup = new Set();
|
|
268
295
|
const conflicts = new Set();
|
|
269
296
|
const expected = new Map();
|
|
297
|
+
const managedBlocks = {};
|
|
298
|
+
const generatedFiles = {};
|
|
299
|
+
const rememberGenerated = (target, content) => {
|
|
300
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
301
|
+
if (installed?.files?.[target] !== hash || Object.hasOwn(previousEntries.files ?? {}, target)) generatedFiles[target] = hash;
|
|
302
|
+
};
|
|
303
|
+
const retiredBlocks = new Set();
|
|
270
304
|
const changed = [];
|
|
271
305
|
function planRemove(value, custom = false) {
|
|
272
306
|
safe(value);
|
|
@@ -278,8 +312,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
278
312
|
if (preserveBoundary(value)) return;
|
|
279
313
|
if (!stat(safe(value))) return;
|
|
280
314
|
if (stat(safe(value)).isDirectory() && !scan(value).length) return;
|
|
315
|
+
if (Object.keys(priorBlocks).some(path => path === value || path.startsWith(value + '/'))) return;
|
|
281
316
|
if (onlyReferences(value)) planRemove(value);
|
|
282
|
-
else if (claimed(value))
|
|
317
|
+
else if (claimed(value)) remember(value, 'edited-generated-file');
|
|
283
318
|
else remember(value);
|
|
284
319
|
}
|
|
285
320
|
const registries = new Map();
|
|
@@ -287,7 +322,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
287
322
|
for (const [target, content] of built.outputs) {
|
|
288
323
|
const info = stat(safe(target));
|
|
289
324
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${target}`);
|
|
290
|
-
|
|
325
|
+
const current = info ? readFileSync(absolute(target)) : null;
|
|
326
|
+
captureInput(target, current);
|
|
327
|
+
if (!current || current.toString('utf8') !== content) {
|
|
291
328
|
registries.set(target, content);
|
|
292
329
|
if (force && info) backup.add(target);
|
|
293
330
|
}
|
|
@@ -304,10 +341,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
304
341
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${manifest}`);
|
|
305
342
|
if (info && !ownedPaths.has(manifest)) { remember(manifest); continue; }
|
|
306
343
|
const current = info ? readFileSync(absolute(manifest)) : null;
|
|
344
|
+
captureInput(manifest, current);
|
|
307
345
|
const priorHashes = [previousOutputs[manifest], installed?.files?.[manifest]];
|
|
308
346
|
const unchanged = current === null || priorHashes.includes(createHash('sha256').update(current).digest('hex'));
|
|
309
|
-
if (!unchanged
|
|
310
|
-
if (!unchanged && force) backup.add(manifest);
|
|
347
|
+
if (!unchanged) { remember(manifest, 'edited-generated-file'); continue; }
|
|
311
348
|
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
312
349
|
if (installed?.files?.[manifest] !== nextHash) discoveryOutputs[manifest] = nextHash;
|
|
313
350
|
if (!info || readFileSync(absolute(manifest), 'utf8') !== content) {
|
|
@@ -332,6 +369,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
332
369
|
if (info && !info.isFile()) fail(`.gitignore nao e arquivo: ${target}`);
|
|
333
370
|
let merged;
|
|
334
371
|
const current = info ? readFileSync(absolute(target), 'utf8') : '';
|
|
372
|
+
captureInput(target, info ? Buffer.from(current) : null);
|
|
335
373
|
if (target !== '.pbai4devs/.gitignore' && info && !ownedPaths.has(target) && !current.split(/\r?\n/).some(line => [ignoreBegin, ignoreEnd].includes(line))) { remember(target); continue; }
|
|
336
374
|
try {
|
|
337
375
|
let patterns = readFileSync(absolute(template), 'utf8');
|
|
@@ -355,7 +393,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
355
393
|
ancestor = posix.dirname(ancestor);
|
|
356
394
|
}
|
|
357
395
|
const info = stat(safe(target));
|
|
358
|
-
|
|
396
|
+
const targetBytes = info?.isFile() ? readFileSync(absolute(target)) : null;
|
|
397
|
+
captureInput(target, targetBytes);
|
|
398
|
+
const reference = targetBytes && referenceFile(target, targetBytes);
|
|
399
|
+
const mixed = info?.isFile() && /\.(md|mdc|toml)$/i.test(target) && (priorBlocks[target] || !reference);
|
|
400
|
+
if (info && !mixed && (!info.isFile() || !reference)) {
|
|
359
401
|
scan(target);
|
|
360
402
|
if (force) planRemove(target, true); else conflicts.add(target);
|
|
361
403
|
}
|
|
@@ -364,7 +406,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
364
406
|
if (/[{}]/.test(source)) fail(`Caminho incompativel com inclusao Gemini: ${source}`);
|
|
365
407
|
const command = built.catalog.commands.find(entry => entry.path === source);
|
|
366
408
|
if (!command) fail(`Comando Gemini exige COMMAND.md: ${source}`);
|
|
367
|
-
const
|
|
409
|
+
const current = mixed ? targetBytes : null;
|
|
410
|
+
const merged = mixed ? mergeTomlContent(current, `@{${source}}`, { expectedHash: priorBlocks[target] }) : null;
|
|
411
|
+
const content = merged ? merged.text : `${tomlMarker}\ndescription = ${JSON.stringify(command.description)}\nprompt = ${JSON.stringify(`@{${source}}\n\n{{args}}`)}\n`;
|
|
412
|
+
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
413
|
+
else rememberGenerated(target, content);
|
|
368
414
|
expected.set(target, content);
|
|
369
415
|
if (!info?.isFile() || readFileSync(absolute(target), 'utf8') !== content) changed.push(target);
|
|
370
416
|
continue;
|
|
@@ -380,7 +426,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
380
426
|
if (!title) fail(`Titulo ausente: ${source}`);
|
|
381
427
|
const ruleInclusion = /^\.(?:agents|agent)\/rules\//.test(target);
|
|
382
428
|
const inclusion = ruleInclusion ? `@[origem](${link(target, source)})` : `Leia e siga [origem](${link(target, source)}).`;
|
|
383
|
-
const
|
|
429
|
+
const current = mixed ? targetBytes : null;
|
|
430
|
+
const merged = mixed ? mergeManagedContent(current, inclusion, { expectedHash: priorBlocks[target] }) : null;
|
|
431
|
+
const content = merged ? merged.text : `${frontmatter}\n${title}\n\n${marker}\n${inclusion}\n`;
|
|
432
|
+
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
433
|
+
else rememberGenerated(target, content);
|
|
384
434
|
expected.set(target, content);
|
|
385
435
|
if (!info?.isFile() || readFileSync(absolute(target), 'utf8') !== content) changed.push(target);
|
|
386
436
|
}
|
|
@@ -394,8 +444,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
394
444
|
const files = scan(scope);
|
|
395
445
|
if (!info.isDirectory()) { if (claimed(scope)) { if (force) planRemove(scope, true); else conflicts.add(scope); } else remember(scope); continue; }
|
|
396
446
|
for (const file of files) if (!expected.has(file)) {
|
|
447
|
+
if (priorBlocks[file]) continue;
|
|
397
448
|
if (referenceFile(file)) planRemove(file);
|
|
398
|
-
else if (claimed(file))
|
|
449
|
+
else if (claimed(file)) remember(file, 'edited-generated-file');
|
|
399
450
|
else remember(file);
|
|
400
451
|
}
|
|
401
452
|
function directories(value) {
|
|
@@ -410,40 +461,77 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
410
461
|
directories(scope);
|
|
411
462
|
}
|
|
412
463
|
for (const file of scan(agent, { markdownOnly: true, prune: true })) {
|
|
413
|
-
if (protectedPath(file) || expected.has(file) || !read(absolute(file)).includes(marker)) continue;
|
|
464
|
+
if (protectedPath(file) || expected.has(file) || priorBlocks[file] || !read(absolute(file)).includes(marker)) continue;
|
|
414
465
|
if (referenceFile(file)) planRemove(file);
|
|
415
|
-
else
|
|
416
|
-
else conflicts.add(file);
|
|
466
|
+
else remember(file, 'edited-generated-file');
|
|
417
467
|
}
|
|
418
468
|
}
|
|
419
469
|
for (const { target } of missing) cleanScope(target);
|
|
420
470
|
const rootEntries = new Map();
|
|
471
|
+
const nextEntries = generateEntrypoints ? {} : { ...previousEntries.entries };
|
|
421
472
|
if (generateEntrypoints) {
|
|
422
473
|
const previous = previousEntries;
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const template = `.pbai4devs/templates/entrypoints/${name}`;
|
|
474
|
+
for (const canonical of resolveAdapters(installed?.clients).entrypoints) {
|
|
475
|
+
const name = resolveEntrypoint(root, canonical);
|
|
476
|
+
const template = `.pbai4devs/templates/entrypoints/${canonical}`;
|
|
427
477
|
if (!stat(safe(template))?.isFile()) continue;
|
|
428
478
|
if (preserveBoundary(name)) continue;
|
|
429
479
|
const info = stat(safe(name));
|
|
430
480
|
if (preserveNativeType(name, info, 'isFile')) continue;
|
|
431
481
|
const current = info?.isFile() ? readFileSync(absolute(name)) : null;
|
|
482
|
+
captureInput(name, current);
|
|
432
483
|
const previousHash = previous.entries[name] ?? installed?.files?.[name];
|
|
433
|
-
|
|
484
|
+
const previousBlockHash = priorBlocks[name] ?? Object.entries(priorBlocks).find(([path]) => canonicalEntrypoint(path) === canonical)?.[1];
|
|
485
|
+
if (current && (previousBlockHash || createHash('sha256').update(current).digest('hex') !== previousHash)) {
|
|
486
|
+
const merged = mergeManagedContent(current, entrypointReference(root, canonical), { expectedHash: previousBlockHash });
|
|
487
|
+
captureInput(name, current);
|
|
488
|
+
managedBlocks[name] = merged.sha256;
|
|
489
|
+
rootEntries.set(name, merged.text);
|
|
490
|
+
remember(name, 'existing-file-extended');
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
434
493
|
if (current && createHash('sha256').update(current).digest('hex') === installed?.files?.[name]) {
|
|
435
|
-
|
|
494
|
+
const content = renderEntrypointImports(root, current.toString('utf8'));
|
|
495
|
+
rootEntries.set(name, content);
|
|
496
|
+
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
497
|
+
if (nextHash !== installed.files[name]) nextEntries[name] = nextHash;
|
|
436
498
|
continue;
|
|
437
499
|
}
|
|
438
|
-
const content = readFileSync(absolute(template), 'utf8');
|
|
500
|
+
const content = renderEntrypointImports(root, readFileSync(absolute(template), 'utf8'));
|
|
439
501
|
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
440
|
-
|
|
502
|
+
nextEntries[name] = nextHash;
|
|
441
503
|
rootEntries.set(name, content);
|
|
442
504
|
}
|
|
443
|
-
const content = JSON.stringify(next, null, 2) + '\n';
|
|
444
|
-
if ((Object.keys(next.entries).length || Object.keys(discoveryOutputs).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== content)) registries.set(entryRegistry, content);
|
|
445
505
|
}
|
|
446
|
-
const
|
|
506
|
+
for (const [target, digest] of Object.entries(priorBlocks)) {
|
|
507
|
+
if (!(validResourcePath(target) || canonicalEntrypoint(target))) continue;
|
|
508
|
+
if (managedBlocks[target]) continue;
|
|
509
|
+
if (canonicalEntrypoint(target) && Object.keys(managedBlocks).some(path => canonicalEntrypoint(path) === canonicalEntrypoint(target))) { removedBlocks.add(target); continue; }
|
|
510
|
+
if (!generateEntrypoints && canonicalEntrypoint(target)) { managedBlocks[target] = digest; continue; }
|
|
511
|
+
if (generatedFiles[target] || nextEntries[target]) { removedBlocks.add(target); continue; }
|
|
512
|
+
if (expected.has(target) || rootEntries.has(target) || preserveBoundary(target)) continue;
|
|
513
|
+
const info = stat(safe(target));
|
|
514
|
+
if (!info) continue;
|
|
515
|
+
if (!info.isFile()) fail(`Tipo alterado em caminho gerenciado: ${target}`);
|
|
516
|
+
const current = readFileSync(absolute(target));
|
|
517
|
+
const content = target.endsWith('.toml') ? stripTomlContent(current, digest) : stripManagedContent(current, digest);
|
|
518
|
+
captureInput(target, current);
|
|
519
|
+
expected.set(target, content);
|
|
520
|
+
if (readFileSync(absolute(target), 'utf8') !== content) changed.push(target);
|
|
521
|
+
retiredBlocks.add(target);
|
|
522
|
+
removedBlocks.add(target);
|
|
523
|
+
remember(target, 'existing-file-extended');
|
|
524
|
+
}
|
|
525
|
+
const next = { schemaVersion: 1, entries: nextEntries };
|
|
526
|
+
if (Object.keys(discoveryOutputs).length) next.outputs = discoveryOutputs;
|
|
527
|
+
if (Object.keys(generatedFiles).length) next.files = generatedFiles;
|
|
528
|
+
const derivedBlocks = Object.fromEntries(Object.entries(managedBlocks).filter(([path, hash]) => installed?.blocks?.[path] !== hash || Object.hasOwn(previousEntries.blocks ?? {}, path)));
|
|
529
|
+
if (Object.keys(derivedBlocks).length) next.blocks = derivedBlocks;
|
|
530
|
+
for (const path of Object.keys(managedBlocks)) removedBlocks.delete(path);
|
|
531
|
+
if (removedBlocks.size) next.removedBlocks = sorted(removedBlocks);
|
|
532
|
+
const registryContent = JSON.stringify(next, null, 2) + '\n';
|
|
533
|
+
if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
|
|
534
|
+
const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(recordDerivedState && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
|
|
447
535
|
...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
|
|
448
536
|
...[...ignoreTemplates.keys()].filter(target => {
|
|
449
537
|
if (target === '.pbai4devs/.gitignore') return false;
|
|
@@ -452,7 +540,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
452
540
|
const start = content.indexOf(ignoreBegin), finish = content.indexOf(ignoreEnd);
|
|
453
541
|
return start >= 0 && finish > start && !(content.slice(0, start) + content.slice(finish + ignoreEnd.length)).trim();
|
|
454
542
|
}),
|
|
455
|
-
]).filter(path => ![...preserved.keys()].some(value => path === value || path.startsWith(`${value}/`)));
|
|
543
|
+
]).filter(path => !managedBlocks[path] && !retiredBlocks.has(path) && ![...preserved.keys()].some(value => path === value || path.startsWith(`${value}/`)));
|
|
456
544
|
const localPlan = localIgnores ? planLocalIgnore(root, { outputs: ownedOutputs }) : null;
|
|
457
545
|
const localStatus = localPlan ? checkLocalIgnore(localPlan) : { changed: 0, missing: [], trackedOutputs: [] };
|
|
458
546
|
const removals = topPaths(remove);
|
|
@@ -473,6 +561,13 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
473
561
|
}
|
|
474
562
|
const transactionRoots = topPaths([...removals, ...changed, ...registries.keys(), ...ignoreFiles.keys(), ...rootEntryChanges.map(([name]) => name), ...(configChanged ? [config] : [])]);
|
|
475
563
|
const originals = new Map(transactionRoots.map(value => [value, capture(value)]));
|
|
564
|
+
for (const [path, bytes] of mergedInputs) {
|
|
565
|
+
const boundary = transactionRoots.find(root => path === root || path.startsWith(root + '/'));
|
|
566
|
+
if (!boundary) continue;
|
|
567
|
+
let snapshot = originals.get(boundary);
|
|
568
|
+
for (const segment of path.slice(boundary.length).split('/').filter(Boolean)) snapshot = snapshot?.children?.[segment];
|
|
569
|
+
if (bytes === null ? snapshot != null : !snapshot?.data?.equals(bytes)) fail(`Arquivo mudou apos planejar: ${path}`);
|
|
570
|
+
}
|
|
476
571
|
const currentHashes = new Map([...originals].map(([value, content]) => [value, fingerprint(content)]));
|
|
477
572
|
const originalDirectories = new Set();
|
|
478
573
|
for (const value of transactionRoots) {
|
|
@@ -486,7 +581,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
486
581
|
const dirty = Object.values(summary).some(Boolean);
|
|
487
582
|
output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
|
|
488
583
|
const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
|
|
489
|
-
if (check) return { ...summary, ownedOutputs, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
584
|
+
if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
490
585
|
if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
|
|
491
586
|
if (localStatus.missing.length) fail(`Regras humanas reabrem derivados locais: ${localStatus.missing.join(', ')}`);
|
|
492
587
|
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
@@ -592,7 +687,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
592
687
|
if (failures.length) throw new AggregateError([error, ...failures], `${error.message}; rollback incompleto: ${failures.map(failure => failure.message).join('; ')}`);
|
|
593
688
|
throw error;
|
|
594
689
|
}
|
|
595
|
-
return { ...summary, ownedOutputs, preserved: preservedPaths, exitCode: 0, ...(backupRoot ? { backupRoot } : {}) };
|
|
690
|
+
return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: 0, ...(backupRoot ? { backupRoot } : {}) };
|
|
596
691
|
}
|
|
597
692
|
|
|
598
693
|
export function main(args = process.argv.slice(2)) {
|
|
@@ -39,7 +39,7 @@ function snapshot(root) {
|
|
|
39
39
|
visit(); return result;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
test('coexiste com aliases nativos
|
|
42
|
+
test('coexiste com aliases nativos e complementa colisões sem perder conteúdo humano', t => {
|
|
43
43
|
const root = fixture(t);
|
|
44
44
|
put(root, '.claude/skills/existing/SKILL.md', '# Human skill\n');
|
|
45
45
|
put(root, '.claude/agents/revisor.md', '# Human reviewer\n');
|
|
@@ -48,7 +48,12 @@ test('coexiste com aliases nativos, configuração e colisão humana sem alterar
|
|
|
48
48
|
put(root, 'external/config.json', '{"human":true}\n');
|
|
49
49
|
if (!link(t, root, '../.claude/skills', '.codex/skills', 'dir') || !link(t, root, 'AGENTS.md', 'CLAUDE.md') || !link(t, root, '../external/config.json', '.claude/settings.local.json')) return;
|
|
50
50
|
const original = snapshot(root), result = run(root, { force: true });
|
|
51
|
-
for (const [path, before] of Object.entries(original))
|
|
51
|
+
for (const [path, before] of Object.entries(original)) {
|
|
52
|
+
if (['AGENTS.md', '.claude/agents/revisor.md'].includes(path)) {
|
|
53
|
+
assert.ok(fs.readFileSync(join(root, path), 'utf8').startsWith(path === 'AGENTS.md' ? '# Human instructions\n' : '# Human reviewer\n'));
|
|
54
|
+
assert.match(fs.readFileSync(join(root, path), 'utf8'), /<!-- BEGIN AGENTDEVKIT -->/);
|
|
55
|
+
} else assert.deepEqual(snapshot(root)[path], before, path);
|
|
56
|
+
}
|
|
52
57
|
for (const path of ['.codex/skills', 'CLAUDE.md', '.claude/agents/revisor.md', '.claude/.gitignore', '.claude/settings.local.json']) assert.ok(result.preserved.some(item => item.path === path), path);
|
|
53
58
|
assert.ok(result.ownedOutputs.includes('.agents/skills/demo/SKILL.md'));
|
|
54
59
|
assert.ok(result.ownedOutputs.every(path => !path.startsWith('.codex/skills/') && path !== '.claude/agents/revisor.md' && path !== 'CLAUDE.md'));
|
|
@@ -98,25 +103,24 @@ test('link em caminho registrado continua sendo erro antes de qualquer escrita',
|
|
|
98
103
|
}
|
|
99
104
|
});
|
|
100
105
|
|
|
101
|
-
test('manifesto standalone
|
|
106
|
+
test('manifesto standalone editado pelo usuário passa a ser preservado', t => {
|
|
102
107
|
const root = fixture(t); run(root);
|
|
103
108
|
const path = '.agents/skills.json', original = fs.readFileSync(join(root, path), 'utf8');
|
|
104
109
|
const derived = JSON.parse(fs.readFileSync(join(root, '.pbai4devs/estado/derived.json')));
|
|
105
110
|
assert.equal(derived.outputs[path], hash(original));
|
|
106
111
|
put(root, path, '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
107
|
-
const before = snapshot(root);
|
|
108
|
-
assert.throws(() => run(root), /--force/);
|
|
109
|
-
assert.deepEqual(snapshot(root), before);
|
|
110
112
|
const result = run(root, { force: true });
|
|
111
|
-
assert.equal(fs.readFileSync(join(root, path), 'utf8'),
|
|
112
|
-
assert.
|
|
113
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), '{"entries":[{"path":".agents/skills/custom"}]}\n');
|
|
114
|
+
assert.ok(result.preserved.some(item => item.path === path));
|
|
115
|
+
assert.ok(!result.ownedOutputs.includes(path));
|
|
116
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
113
117
|
});
|
|
114
118
|
|
|
115
|
-
test('manifesto nativo
|
|
119
|
+
test('manifesto nativo inclui recursos humanos complementados com apontamento', t => {
|
|
116
120
|
const root = fixture(t), collision = '.agents/skills/demo/SKILL.md';
|
|
117
121
|
put(root, collision, '# Human demo\n');
|
|
118
122
|
const result = run(root);
|
|
119
|
-
assert.deepEqual(JSON.parse(fs.readFileSync(join(root, '.agents/skills.json'))).entries, []);
|
|
123
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(join(root, '.agents/skills.json'))).entries, [{ path: '.agents/skills/demo' }]);
|
|
120
124
|
assert.ok(result.preserved.some(item => item.path === collision));
|
|
121
125
|
assert.ok(!result.ownedOutputs.includes(collision));
|
|
122
126
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
@@ -166,7 +170,12 @@ test('variantes humanas de entradas da raiz são preservadas sem criar duplicata
|
|
|
166
170
|
const root = fixture(t);
|
|
167
171
|
for (const path of ['Readme.md', 'agents.md', 'claude.md', 'gemini.md']) put(root, path, `# Human ${path}\n`);
|
|
168
172
|
const before = snapshot(root), result = run(root, { force: true });
|
|
169
|
-
for (const [path, content] of Object.entries(before))
|
|
173
|
+
for (const [path, content] of Object.entries(before)) {
|
|
174
|
+
if (['agents.md', 'claude.md', 'gemini.md'].includes(path)) {
|
|
175
|
+
assert.ok(fs.readFileSync(join(root, path), 'utf8').startsWith(`# Human ${path}\n`));
|
|
176
|
+
assert.match(fs.readFileSync(join(root, path), 'utf8'), /<!-- BEGIN AGENTDEVKIT -->/);
|
|
177
|
+
} else assert.deepEqual(snapshot(root)[path], content, path);
|
|
178
|
+
}
|
|
170
179
|
for (const [canonical, alias] of [['README.md', 'Readme.md'], ['AGENTS.md', 'agents.md'], ['CLAUDE.md', 'claude.md'], ['GEMINI.md', 'gemini.md']]) {
|
|
171
180
|
assert.deepEqual(nativePathBoundary(root, canonical, [], { preserveRootAliases: true }), { path: alias, reason: 'case-variant' });
|
|
172
181
|
if (canonical !== 'README.md') assert.ok(result.preserved.some(item => item.path === alias));
|
|
@@ -178,13 +187,14 @@ test('variantes humanas de entradas da raiz são preservadas sem criar duplicata
|
|
|
178
187
|
assert.deepEqual(snapshot(root), stable);
|
|
179
188
|
});
|
|
180
189
|
|
|
181
|
-
test('variante
|
|
190
|
+
test('variante única de entrada registrada conserva conteúdo como arquivo misto', t => {
|
|
182
191
|
for (const owned of ['AGENTS.md', 'agents.md']) {
|
|
183
192
|
const root = fixture(t); put(root, 'agents.md', '# Renamed human entry\n');
|
|
184
193
|
assert.throws(() => nativePathBoundary(root, 'AGENTS.md', [owned], { preserveRootAliases: true }), /maiusculas\/minusculas/);
|
|
185
194
|
put(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [owned]: 'a'.repeat(64) }, blocks: {} }));
|
|
186
|
-
|
|
187
|
-
assert.
|
|
188
|
-
assert.
|
|
195
|
+
run(root, { force: true });
|
|
196
|
+
assert.ok(fs.readFileSync(join(root, 'agents.md'), 'utf8').startsWith('# Renamed human entry\n'));
|
|
197
|
+
assert.ok(!fs.readdirSync(root).includes('AGENTS.md'));
|
|
198
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
189
199
|
}
|
|
190
200
|
});
|
|
@@ -47,8 +47,13 @@ test('texto humano com formato de referência nunca comprova propriedade', t =>
|
|
|
47
47
|
const result = run(root);
|
|
48
48
|
assert.ok(result.preserved.some(item => item.path === path));
|
|
49
49
|
assert.ok(!result.ownedOutputs.includes(path));
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (path === generated) {
|
|
51
|
+
assert.ok(fs.readFileSync(join(root, path), 'utf8').startsWith(human));
|
|
52
|
+
assert.match(fs.readFileSync(join(root, path), 'utf8'), /<!-- BEGIN AGENTDEVKIT -->/);
|
|
53
|
+
} else {
|
|
54
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), human);
|
|
55
|
+
assert.equal(snapshot(root)[path], before[path]);
|
|
56
|
+
}
|
|
52
57
|
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
53
58
|
const stable = snapshot(root);
|
|
54
59
|
run(root, { force: true });
|