@pathbit/agentdevkit 0.0.6 → 0.0.8
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/scripts/lib/gitignore.mjs +142 -25
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +120 -20
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +145 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +211 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +2 -1
- package/CHANGELOG.md +16 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +2 -2
- package/docs/guides/cli.md +9 -0
- package/docs/guides/installation.md +17 -2
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +2 -0
- package/docs/specs/v0.0.7/README.md +10 -0
- package/docs/specs/v0.0.7/SPEC.md +59 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/package.json +3 -2
- package/src/cli.mjs +10 -4
- package/src/install.mjs +61 -22
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Planeja em memória; somente os caminhos gerenciados pelo manifesto são alterados.
|
|
3
|
-
import { cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, cpSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, posix, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID, createHash } from 'node:crypto';
|
|
6
6
|
import { requireNode, projectRoot, isMain } from '../lib/runtime.mjs';
|
|
7
7
|
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
|
-
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
10
|
+
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
11
11
|
|
|
12
12
|
const marker = '<!-- gerado: sincronizar-agentes -->';
|
|
13
13
|
const tomlMarker = '# gerado: sincronizar-agentes';
|
|
@@ -113,9 +113,13 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
113
113
|
}
|
|
114
114
|
const referenceFile = value => {
|
|
115
115
|
const info = stat(safe(value));
|
|
116
|
-
|
|
116
|
+
if (!info?.isFile() || info.size > 1024 * 1024) return false;
|
|
117
|
+
const bytes = readFileSync(absolute(value)), text = normalize(bytes.toString('utf8'));
|
|
118
|
+
const recorded = installed?.files?.[value] === createHash('sha256').update(bytes).digest('hex');
|
|
119
|
+
const marked = text.split('\n').includes(marker) || text.startsWith(tomlMarker + '\n');
|
|
120
|
+
return isReference(text) && (marked || recorded);
|
|
117
121
|
};
|
|
118
|
-
const onlyReferences = value => scan(value).every(file => /\.(md|mdc|toml)$/.test(file) && referenceFile(file));
|
|
122
|
+
const onlyReferences = value => { const files = scan(value); return files.length > 0 && files.every(file => /\.(md|mdc|toml)$/.test(file) && referenceFile(file)); };
|
|
119
123
|
const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
|
|
120
124
|
const agents = new Map();
|
|
121
125
|
const formats = new Map();
|
|
@@ -237,6 +241,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
237
241
|
}
|
|
238
242
|
function cleanScope(value) {
|
|
239
243
|
if (!stat(safe(value))) return;
|
|
244
|
+
if (stat(safe(value)).isDirectory() && !scan(value).length) return;
|
|
240
245
|
if (force && value.split('/')[1] === 'external-skills') planRemove(value, true);
|
|
241
246
|
else if (onlyReferences(value)) planRemove(value);
|
|
242
247
|
else if (force) planRemove(value, true);
|
|
@@ -410,16 +415,72 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
410
415
|
const removals = topPaths(remove);
|
|
411
416
|
const backups = topPaths(backup);
|
|
412
417
|
const rootEntryChanges = [...rootEntries].filter(([name, content]) => !stat(absolute(name)) || readFileSync(absolute(name), 'utf8') !== content);
|
|
418
|
+
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
419
|
+
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
420
|
+
const configChanged = !configExists || configText !== finalConfig;
|
|
421
|
+
function capture(value) {
|
|
422
|
+
const file = safe(value), info = stat(file);
|
|
423
|
+
if (!info) return null;
|
|
424
|
+
if (info.isFile()) return { mode: info.mode & 0o777, data: readFileSync(file) };
|
|
425
|
+
if (!info.isDirectory()) fail(`Tipo de arquivo nao suportado: ${value}`);
|
|
426
|
+
return { mode: info.mode & 0o777, children: Object.fromEntries(readdirSync(file).sort(compare).map(name => [name, capture(`${value}/${name}`)])) };
|
|
427
|
+
}
|
|
428
|
+
function fingerprint(value) {
|
|
429
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
430
|
+
}
|
|
431
|
+
const transactionRoots = topPaths([...removals, ...changed, ...registries.keys(), ...ignoreFiles.keys(), ...rootEntryChanges.map(([name]) => name), ...(configChanged ? [config] : [])]);
|
|
432
|
+
const originals = new Map(transactionRoots.map(value => [value, capture(value)]));
|
|
433
|
+
const currentHashes = new Map([...originals].map(([value, content]) => [value, fingerprint(content)]));
|
|
434
|
+
const originalDirectories = new Set();
|
|
435
|
+
for (const value of transactionRoots) {
|
|
436
|
+
let parent = posix.dirname(value);
|
|
437
|
+
while (parent !== '.') { if (stat(safe(parent))?.isDirectory()) originalDirectories.add(parent); parent = posix.dirname(parent); }
|
|
438
|
+
}
|
|
439
|
+
const touched = new Set(), createdDirectories = new Set(), restoredDirectories = new Set();
|
|
440
|
+
const prunedDirectories = new Map();
|
|
441
|
+
let restoring = false;
|
|
413
442
|
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length };
|
|
414
443
|
const dirty = Object.values(summary).some(Boolean);
|
|
415
444
|
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.`);
|
|
416
445
|
if (check) return { ...summary, ownedOutputs, exitCode: dirty ? 1 : 0 };
|
|
417
446
|
if (conflicts.size) fail(`Use --force para reconstruir somente os caminhos gerenciados: ${sorted(conflicts).join(', ')}`);
|
|
447
|
+
if (localStatus.missing.length) fail(`Regras humanas reabrem derivados locais: ${localStatus.missing.join(', ')}`);
|
|
448
|
+
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
449
|
+
function ensureDirectory(value) {
|
|
450
|
+
let parent = posix.dirname(value);
|
|
451
|
+
const missing = [];
|
|
452
|
+
while (parent !== '.' && !stat(safe(parent))) { missing.push(parent); parent = posix.dirname(parent); }
|
|
453
|
+
for (const directory of missing.reverse()) {
|
|
454
|
+
mkdirSync(safe(directory)); createdDirectories.add(directory);
|
|
455
|
+
if (restoring) restoredDirectories.add(directory);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function mutate(value, action, text) {
|
|
459
|
+
const boundary = transactionRoots.find(root => value === root || value.startsWith(root + '/'));
|
|
460
|
+
if (!boundary || fingerprint(capture(boundary)) !== currentHashes.get(boundary)) fail(`Arquivo mudou apos planejar: ${value}`);
|
|
461
|
+
const beforeHash = currentHashes.get(boundary);
|
|
462
|
+
touched.add(boundary);
|
|
463
|
+
let failure;
|
|
464
|
+
try { action(); } catch (error) { failure = error; }
|
|
465
|
+
let afterHash;
|
|
466
|
+
try { afterHash = fingerprint(capture(boundary)); }
|
|
467
|
+
catch (error) { currentHashes.set(boundary, 'conteudo-concorrente'); throw failure || error; }
|
|
468
|
+
const matches = text === undefined || (text === null ? !stat(safe(value)) : stat(safe(value))?.isFile() && readFileSync(absolute(value)).equals(Buffer.from(text)));
|
|
469
|
+
if (!matches && afterHash !== beforeHash) {
|
|
470
|
+
currentHashes.set(boundary, 'conteudo-concorrente');
|
|
471
|
+
if (failure) throw failure;
|
|
472
|
+
fail(`Mudanca concorrente preservada: ${value}`);
|
|
473
|
+
}
|
|
474
|
+
currentHashes.set(boundary, afterHash);
|
|
475
|
+
if (failure) throw failure;
|
|
476
|
+
if (!matches) fail(`Arquivo nao corresponde a gravacao planejada: ${value}`);
|
|
477
|
+
}
|
|
418
478
|
function atomic(value, text) {
|
|
419
479
|
const target = safe(value);
|
|
420
|
-
|
|
480
|
+
const mode = stat(target)?.isFile() ? stat(target).mode & 0o777 : undefined;
|
|
481
|
+
ensureDirectory(value);
|
|
421
482
|
const temporary = `${target}.sync.${randomUUID()}`;
|
|
422
|
-
try { writeFileSync(temporary, text, { flag: 'wx' }); renameSync(temporary, target); }
|
|
483
|
+
try { writeFileSync(temporary, text, { flag: 'wx' }); if (mode !== undefined) chmodSync(temporary, mode); renameSync(temporary, target); }
|
|
423
484
|
finally { rmSync(temporary, { force: true }); }
|
|
424
485
|
}
|
|
425
486
|
let backupRoot;
|
|
@@ -433,21 +494,60 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
433
494
|
}
|
|
434
495
|
output(`Backup local: ${posix.join('.agent-tests', backupRoot.slice(backupRoot.lastIndexOf('/') + 1).split('\\').at(-1))}`);
|
|
435
496
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
497
|
+
function restore(value, content) {
|
|
498
|
+
rmSync(safe(value), { recursive: true, force: true });
|
|
499
|
+
if (!content) return;
|
|
500
|
+
ensureDirectory(value);
|
|
501
|
+
if (content.data) writeFileSync(safe(value), content.data);
|
|
502
|
+
else {
|
|
503
|
+
mkdirSync(safe(value));
|
|
504
|
+
for (const [name, child] of Object.entries(content.children)) restore(`${value}/${name}`, child);
|
|
505
|
+
}
|
|
506
|
+
chmodSync(safe(value), content.mode);
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
510
|
+
for (const value of removals) mutate(value, () => rmSync(safe(value), { recursive: true, force: true }), null);
|
|
511
|
+
for (const value of changed) mutate(value, () => atomic(value, expected.get(value)), expected.get(value));
|
|
512
|
+
for (const [value, text] of registries) mutate(value, () => atomic(value, text), text);
|
|
513
|
+
for (const [value, text] of ignoreFiles) mutate(value, () => atomic(value, text), text);
|
|
514
|
+
for (const [value, text] of rootEntryChanges) mutate(value, () => atomic(value, text), text);
|
|
515
|
+
if (configChanged) mutate(config, () => atomic(config, finalConfig), finalConfig);
|
|
516
|
+
// Só os ancestrais de remoções conhecidas são candidatos à limpeza.
|
|
517
|
+
for (const value of removals) {
|
|
518
|
+
let parent = posix.dirname(value);
|
|
519
|
+
const boundary = value.split('/').slice(0, 2).join('/');
|
|
520
|
+
while (parent.startsWith(boundary + '/') || parent === boundary) {
|
|
521
|
+
const info = stat(safe(parent));
|
|
522
|
+
if (!info?.isDirectory() || readdirSync(absolute(parent)).length) break;
|
|
523
|
+
rmdirSync(safe(parent)); prunedDirectories.set(parent, info.mode & 0o777); parent = posix.dirname(parent);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (localPlan) applyLocalIgnore(localPlan);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
const failures = [];
|
|
529
|
+
restoring = true;
|
|
530
|
+
for (const value of [...touched].reverse()) {
|
|
531
|
+
try {
|
|
532
|
+
if (fingerprint(capture(value)) !== currentHashes.get(value)) fail(`Mudanca concorrente preservada: ${value}`);
|
|
533
|
+
restore(value, originals.get(value));
|
|
534
|
+
} catch (failure) { failures.push(failure); }
|
|
535
|
+
}
|
|
536
|
+
for (const [directory, mode] of [...prunedDirectories].sort(([left], [right]) => right.length - left.length)) {
|
|
537
|
+
try {
|
|
538
|
+
if (!stat(safe(directory))) ensureDirectory(`${directory}/.restore-probe`);
|
|
539
|
+
const info = stat(safe(directory));
|
|
540
|
+
if (!info?.isDirectory() || !restoredDirectories.has(directory) && (info.mode & 0o777) !== mode) fail(`Diretorio concorrente preservado: ${directory}`);
|
|
541
|
+
chmodSync(safe(directory), mode);
|
|
542
|
+
} catch (failure) { failures.push(failure); }
|
|
543
|
+
}
|
|
544
|
+
for (const directory of sorted(createdDirectories).sort((a, b) => b.length - a.length)) {
|
|
545
|
+
try { if (!originalDirectories.has(directory) && stat(safe(directory))?.isDirectory() && !readdirSync(absolute(directory)).length) rmdirSync(safe(directory)); }
|
|
546
|
+
catch (failure) { failures.push(failure); }
|
|
547
|
+
}
|
|
548
|
+
if (failures.length) throw new AggregateError([error, ...failures], `${error.message}; rollback incompleto: ${failures.map(failure => failure.message).join('; ')}`);
|
|
549
|
+
throw error;
|
|
446
550
|
}
|
|
447
|
-
for (const agent of agents.keys()) for (const kind of [...Object.keys(principals), 'instructions', 'prompts']) pruneEmpty(`${agent}/${kind}`);
|
|
448
|
-
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
449
|
-
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
450
|
-
if (!configExists || configText !== finalConfig) atomic(config, finalConfig);
|
|
451
551
|
return { ...summary, ownedOutputs, exitCode: 0, ...(backupRoot ? { backupRoot } : {}) };
|
|
452
552
|
}
|
|
453
553
|
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
assert.throws(() => run(root), /--force/);
|
|
48
|
+
assert.deepEqual(snapshot(root), before);
|
|
49
|
+
assert.equal(fs.readFileSync(join(root, path), 'utf8'), human);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('hash registrado comprova referência legada sem marcador e diretório vazio humano permanece', t => {
|
|
54
|
+
const root = fixture(t), stale = '.claude/skills/legacy.md';
|
|
55
|
+
write(root, stale, human);
|
|
56
|
+
write(root, '.pbai4devs/estado/agentdevkit.json', JSON.stringify({ schemaVersion: 1, clients: ['claude'], files: { [stale]: hash(human) }, blocks: {} }));
|
|
57
|
+
fs.mkdirSync(join(root, '.claude/skills/user-empty'), { recursive: true });
|
|
58
|
+
run(root);
|
|
59
|
+
assert.equal(fs.existsSync(join(root, stale)), false);
|
|
60
|
+
assert.equal(fs.existsSync(join(root, '.claude/skills/user-empty')), true);
|
|
61
|
+
assert.equal(run(root, { check: true }).exitCode, 0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('conflito de ignore é recusado antes de criar recursos ou catálogos', t => {
|
|
65
|
+
const root = fixture(t); write(root, '.gitignore', '!/.agent-tests/\n!/.agent-tests/**\n');
|
|
66
|
+
const before = snapshot(root), exclude = fs.readFileSync(join(root, '.git/info/exclude'));
|
|
67
|
+
assert.throws(() => run(root, { localIgnores: true, force: true }), /Regras humanas/);
|
|
68
|
+
assert.deepEqual(snapshot(root), before);
|
|
69
|
+
assert.deepEqual(fs.readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
70
|
+
assert.equal(fs.existsSync(join(root, generated)), false);
|
|
71
|
+
assert.equal(fs.existsSync(join(root, '.pbai4devs/catalog.json')), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('atualização de referência própria conserva permissões', t => {
|
|
75
|
+
const root = fixture(t); run(root);
|
|
76
|
+
fs.chmodSync(join(root, generated), 0o600);
|
|
77
|
+
const mode = fs.statSync(join(root, generated)).mode & 0o777;
|
|
78
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
79
|
+
run(root);
|
|
80
|
+
assert.equal(fs.statSync(join(root, generated)).mode & 0o777, mode);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('mudança concorrente de ignore após gravação reverte recursos e preserva a regra humana', t => {
|
|
84
|
+
const root = fixture(t); run(root);
|
|
85
|
+
write(root, source, '---\nname: demo\ndescription: Atualizado.\n---\n\n# Demo\n');
|
|
86
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
87
|
+
let injected = false;
|
|
88
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
89
|
+
rename(from, to);
|
|
90
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
91
|
+
});
|
|
92
|
+
syncBuiltinESMExports();
|
|
93
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
94
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
95
|
+
assert.equal(injected, true);
|
|
96
|
+
assert.deepEqual(snapshot(root), before);
|
|
97
|
+
assert.equal(fs.readFileSync(join(root, '.git/info/exclude'), 'utf8'), '# concurrent human change\n');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('conteúdo humano concorrente não é apagado durante rollback', t => {
|
|
101
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
102
|
+
let injected = false;
|
|
103
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
104
|
+
rename(from, to);
|
|
105
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); }
|
|
106
|
+
});
|
|
107
|
+
syncBuiltinESMExports();
|
|
108
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
109
|
+
assert.throws(() => run(root), /Mudanca concorrente preservada/);
|
|
110
|
+
assert.equal(injected, true);
|
|
111
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('conteúdo humano é preservado quando a escrita lança erro após promover o arquivo', t => {
|
|
115
|
+
const root = fixture(t), rename = fs.renameSync;
|
|
116
|
+
const failure = new Error('falha depois de promover');
|
|
117
|
+
let injected = false;
|
|
118
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
119
|
+
rename(from, to);
|
|
120
|
+
if (!injected && to === join(root, generated)) { injected = true; write(root, generated, human); throw failure; }
|
|
121
|
+
});
|
|
122
|
+
syncBuiltinESMExports();
|
|
123
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
124
|
+
assert.throws(() => run(root), error => error instanceof AggregateError && error.errors.includes(failure));
|
|
125
|
+
assert.equal(injected, true);
|
|
126
|
+
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('rollback recria ancestral podado com suas permissões originais', { skip: process.platform === 'win32' }, t => {
|
|
130
|
+
const root = fixture(t); run(root);
|
|
131
|
+
const parent = join(root, '.claude/skills'); fs.chmodSync(parent, 0o700);
|
|
132
|
+
fs.rmSync(join(root, '.pbai4devs/skills/demo'), { recursive: true });
|
|
133
|
+
const before = snapshot(root), rename = fs.renameSync;
|
|
134
|
+
let injected = false;
|
|
135
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
136
|
+
rename(from, to);
|
|
137
|
+
if (!injected && to === join(root, '.pbai4devs/catalog.json')) { injected = true; write(root, '.git/info/exclude', '# concurrent human change\n'); }
|
|
138
|
+
});
|
|
139
|
+
syncBuiltinESMExports();
|
|
140
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
141
|
+
assert.throws(() => run(root, { localIgnores: true }), /Exclusao alterada apos planejar/);
|
|
142
|
+
assert.equal(injected, true);
|
|
143
|
+
assert.deepEqual(snapshot(root), before);
|
|
144
|
+
assert.equal(fs.statSync(parent).mode & 0o777, 0o700);
|
|
145
|
+
});
|
|
@@ -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
|
|
|
@@ -129,6 +131,72 @@ test('raízes dedicadas são locais, enquanto configurações de GitHub e VS Cod
|
|
|
129
131
|
assert.ok(tracked.includes('.github/settings.json')); assert.ok(tracked.includes('.vscode/settings.json'));
|
|
130
132
|
});
|
|
131
133
|
|
|
134
|
+
test('artefatos vêm da política compartilhada e não entram na lista de outputs', t => {
|
|
135
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
136
|
+
put(root, '.agent-tests/human.txt', 'evidência humana'); git(root, ['add', '-f', '.agent-tests/human.txt']);
|
|
137
|
+
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
138
|
+
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agent-tests/')));
|
|
139
|
+
assert.ok(plan.artifactProbes.some(path => path.startsWith('.agentdevkit-ignore-probes/')));
|
|
140
|
+
assert.ok(plan.paths.every(path => !plan.artifactProbes.includes(path)));
|
|
141
|
+
assert.ok(!plan.trackedOutputs.includes('.agent-tests/human.txt'));
|
|
142
|
+
applyLocalIgnore(plan);
|
|
143
|
+
for (const path of ['.agent-tests/x', 'TMP/x', 'nested/TeMp/x', '.PlAyWrIgHt-McP/x', 'nested/playwright-report/x', 'TEST-RESULTS/x', 'nested/TestResults/x', 'BLOB-REPORT/x']) {
|
|
144
|
+
assert.equal(git(root, ['check-ignore', '--no-index', path]).trim(), path);
|
|
145
|
+
}
|
|
146
|
+
assert.equal(git(root, ['ls-files', '.agent-tests/human.txt']).trim(), '.agent-tests/human.txt');
|
|
147
|
+
assert.equal(readFileSync(join(root, '.agent-tests/human.txt'), 'utf8'), 'evidência humana');
|
|
148
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok, true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('regras de artefatos são limitadas ao projeto dentro do monorepo', t => {
|
|
152
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
153
|
+
const child = join(root, 'apps', 'my project'); mkdirSync(child, { recursive: true });
|
|
154
|
+
applyLocalIgnore(planLocalIgnore(child, { outputs: ['AGENTS.md'] }));
|
|
155
|
+
for (const path of ['apps/my project/.agent-tests/x', 'apps/my project/nested/.Playwright-MCP/x', 'apps/my project/sub/test-results/x']) {
|
|
156
|
+
assert.equal(git(root, ['check-ignore', '--no-index', path]).trim(), path);
|
|
157
|
+
}
|
|
158
|
+
for (const path of ['apps/other/.agent-tests/x', 'apps/other/tmp/x', '.agent-tests/x']) {
|
|
159
|
+
const result = spawnSync('git', ['-C', root, 'check-ignore', '--no-index', path], { encoding: 'utf8' });
|
|
160
|
+
assert.equal(result.status, 1, path + result.stderr);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('ignore global não mascara a ausência das regras locais de artefatos', t => {
|
|
165
|
+
const root = fixture(t), outside = fixture(t); git(root, ['init', '-q']);
|
|
166
|
+
put(outside, 'global.ignore', '*\n');
|
|
167
|
+
git(root, ['config', 'core.excludesFile', join(outside, 'global.ignore')]);
|
|
168
|
+
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
169
|
+
assert.equal(plan.edits.length, 1);
|
|
170
|
+
const missing = checkLocalIgnore({ ...plan, edits: [] });
|
|
171
|
+
assert.ok(missing.missing.some(path => path.startsWith('.agent-tests/')));
|
|
172
|
+
applyLocalIgnore(plan);
|
|
173
|
+
assert.equal(checkLocalIgnore(planLocalIgnore(root, { outputs: ['AGENTS.md'] })).ok, true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('negação humana de artefatos bloqueia aplicação sem modificar regras ou índice', t => {
|
|
177
|
+
const root = fixture(t); git(root, ['init', '-q']);
|
|
178
|
+
const human = '!/.agent-tests/**\n'; put(root, '.gitignore', human);
|
|
179
|
+
const exclude = readFileSync(join(root, '.git/info/exclude'));
|
|
180
|
+
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
181
|
+
assert.ok(checkLocalIgnore(plan).missing.some(path => path.startsWith('.agent-tests/')));
|
|
182
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
183
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), exclude);
|
|
184
|
+
assert.equal(readFileSync(join(root, '.gitignore'), 'utf8'), human);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('probes reconhecem exceções humanas para nomes reais de Playwright', async t => {
|
|
188
|
+
for (const path of ['.playwright-mcp', 'playwright-report', 'playwright/.cache']) await t.test(path, subtest => {
|
|
189
|
+
const root = fixture(subtest); git(root, ['init', '-q']);
|
|
190
|
+
const prefixes = path.split('/').map((_part, index, parts) => '!/' + parts.slice(0, index + 1).join('/') + '/');
|
|
191
|
+
const human = [...prefixes, `!/${path}/**`, ''].join('\n'); put(root, '.gitignore', human);
|
|
192
|
+
const before = readFileSync(join(root, '.git/info/exclude'));
|
|
193
|
+
const plan = planLocalIgnore(root, { outputs: ['AGENTS.md'] });
|
|
194
|
+
assert.ok(checkLocalIgnore(plan).missing.some(probe => probe.startsWith(path + '/')));
|
|
195
|
+
assert.throws(() => applyLocalIgnore(plan), /Regras humanas/);
|
|
196
|
+
assert.deepEqual(readFileSync(join(root, '.git/info/exclude')), before);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
132
200
|
test('milhares de caminhos usam stdin e não ampliam metadata de área já ignorada', t => {
|
|
133
201
|
const root = fixture(t); git(root, ['init', '-q']); put(root, '.gitignore', '/ignored/\n');
|
|
134
202
|
const before = readFileSync(join(root, '.git/info/exclude'), 'utf8');
|
|
@@ -288,3 +356,145 @@ test('worktree usa a raiz nativa, exclude comum e seu próprio índice', t => {
|
|
|
288
356
|
assert.equal(git(linked, ['show', ':generated.md']), 'gerado\n');
|
|
289
357
|
assert.equal(git(root, ['ls-files', 'generated.md']), '');
|
|
290
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,7 @@ 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';
|
|
13
14
|
import { validateCatalog as validateExternalCatalog } from '../../gerenciar-skills-externas/SCRIPT.mjs';
|
|
14
15
|
|
|
15
16
|
const project = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
@@ -58,7 +59,7 @@ function fixture(t, minimal = false) {
|
|
|
58
59
|
write(root, '.pbai4devs/agents/revisor/kimi-code.yaml', '---\nname: revisor\ntools: [Read, Grep, Glob]\n---\n');
|
|
59
60
|
write(root, '.pbai4devs/agents/revisor/opencode.yaml', '---\nmode: subagent\npermission:\n edit: deny\n---\n');
|
|
60
61
|
write(root, '.pbai4devs/adapters/antigravity/agents/revisor.md', pointer('../../../agents/revisor/AGENT.md', 'Revisor'));
|
|
61
|
-
write(root, '.opencode/agents/revisor/AGENT.md',
|
|
62
|
+
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
63
|
write(root, '.github/workflows/ci.yml', 'workflow-preservado\n');
|
|
63
64
|
write(root, '.vscode/settings.json', '{"fixture":"config-preservada"}\n');
|
|
64
65
|
write(root, '.vscode/extensions.json', '{"recommendations":[]}\n');
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.8 — 2026-11-11
|
|
8
|
+
|
|
9
|
+
- Preservação de arquivos humanos que parecem apontamentos gerados.
|
|
10
|
+
- Conflitos de ignore recusados antes das gravações e rollback revisado.
|
|
11
|
+
- Permissões mantidas em atualização, backup e restauração de arquivos.
|
|
12
|
+
- Caminhos simbólicos de ferramentas bloqueados antes de escrita externa.
|
|
13
|
+
- Argumentos de ferramentas preservados após `--`; ajuda sem projeto válido.
|
|
14
|
+
- Verificação de artefatos com nomes próprios, arquivos e regras aninhadas.
|
|
15
|
+
|
|
16
|
+
## 0.0.7 — 2026-11-11
|
|
17
|
+
|
|
18
|
+
- Artefatos de testes e pastas temporárias protegidos em projetos existentes.
|
|
19
|
+
- Regras locais respeitam o escopo do projeto e as exceções do usuário.
|
|
20
|
+
- Teste opt-in em clones reais, com commits fixos e comparação de arquivos.
|
|
21
|
+
- Relato explícito de bloqueios por links simbólicos preexistentes.
|
|
22
|
+
|
|
7
23
|
## 0.0.6 — 2026-11-11
|
|
8
24
|
|
|
9
25
|
- Metadados npm com autoria PathBit, palavras-chave e licença MIT.
|
package/README.en.md
CHANGED
|
@@ -34,7 +34,7 @@ Then open the project in your coding agent and ask:
|
|
|
34
34
|
|
|
35
35
|
See [installation](docs/guides/installation.md) for Windows, missing Node, previews
|
|
36
36
|
and updates. Install and update through the [public npm package][packages]; the
|
|
37
|
-
[v0.0.
|
|
37
|
+
[v0.0.8 release][release] retains artifacts and checksums for verification.
|
|
38
38
|
No global installation is required.
|
|
39
39
|
The CLI supports `pt`, `en` and `es`; it saves your selection per project.
|
|
40
40
|
|
|
@@ -78,7 +78,7 @@ forms. See [how to participate](docs/maintainers/community.md).
|
|
|
78
78
|
agent to read `AGENTS.md`.
|
|
79
79
|
|
|
80
80
|
[clients]: .pbai4devs/COMPATIBILIDADE.md
|
|
81
|
-
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.
|
|
81
|
+
[release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
|
|
82
82
|
[npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
|
|
83
83
|
[npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
|
|
84
84
|
[license-badge]: https://img.shields.io/badge/license-MIT-blue
|