@pathbit/agentdevkit 0.0.11 → 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/rules/documentacao/RULE.md +2 -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/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/CHANGELOG.md +8 -0
- package/README.en.md +2 -2
- package/README.es.md +2 -2
- package/README.md +7 -3
- package/docs/guides/cli.md +2 -1
- package/docs/guides/installation.md +23 -6
- package/docs/guides/troubleshooting.md +8 -5
- package/docs/maintainers/maintenance.md +5 -0
- package/docs/maintainers/packages.md +2 -2
- package/docs/product/design.md +10 -3
- package/docs/product/prd.md +2 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +3 -0
- package/docs/specs/additive-integration/README.md +9 -0
- package/docs/specs/additive-integration/SPEC.md +53 -0
- package/package.json +1 -1
- package/src/install.mjs +139 -63
package/src/install.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
5
5
|
import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
6
6
|
import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, planUntrack, applyUntrack, restoreUntrack } from '../.pbai4devs/scripts/lib/gitignore.mjs';
|
|
7
7
|
import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
|
|
8
|
+
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, managedBlockDigest, mergeManagedContent, stripManagedContent } from '../.pbai4devs/scripts/lib/managed-content.mjs';
|
|
9
|
+
import { extractTomlManagedBlock, tomlManagedBlockDigest, mergeTomlContent, stripTomlContent } from '../.pbai4devs/scripts/lib/managed-toml.mjs';
|
|
8
10
|
import { writeAdapterManifest, adapterRoots, normalizeClients, resolveAdapters } from './adapters/index.mjs';
|
|
9
11
|
import { packageRoot, packageInfo, statePath, entryNames, begin, end } from './config.mjs';
|
|
10
12
|
import { normalizeLanguage, text } from './i18n.mjs';
|
|
@@ -84,7 +86,7 @@ function write(root, path, content, mode) {
|
|
|
84
86
|
if (mode !== undefined) chmodSync(target, mode);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function atomicWrite(root, path, content, mode, onPublished) {
|
|
89
|
+
function atomicWrite(root, path, content, mode, onPublished, beforePublish) {
|
|
88
90
|
const file = safePath(root, path);
|
|
89
91
|
mkdirSync(dirname(file), { recursive: true });
|
|
90
92
|
const temporary = `${file}.agentdevkit-${randomUUID()}`;
|
|
@@ -92,6 +94,7 @@ function atomicWrite(root, path, content, mode, onPublished) {
|
|
|
92
94
|
writeFileSync(temporary, content, { flag: 'wx', ...(mode === undefined ? {} : { mode }) });
|
|
93
95
|
if (mode !== undefined) chmodSync(temporary, mode);
|
|
94
96
|
const writtenMode = stat(temporary).mode & 0o777;
|
|
97
|
+
beforePublish?.();
|
|
95
98
|
renameSync(temporary, file);
|
|
96
99
|
onPublished?.(writtenMode);
|
|
97
100
|
return writtenMode;
|
|
@@ -106,13 +109,26 @@ function read(root, path) {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
function allowedManaged(path) {
|
|
109
|
-
if (
|
|
112
|
+
if (canonicalEntrypoint(path) || path === '.gitignore') return true;
|
|
113
|
+
if (nativeMarkdown(path) || nativeToml(path)) return true;
|
|
110
114
|
if (['catalog.json', 'skills.json', 'agents.json', 'rules.json', 'espelhos.conf'].some(name => path === `.pbai4devs/${name}`)) return true;
|
|
111
115
|
if (['.agents/skills.json', '.agents/agents.json', '.agents/rules.json'].includes(path)) return true;
|
|
112
116
|
if (path.startsWith('.pbai4devs/')) return resourceAllowed(path.slice('.pbai4devs/'.length));
|
|
113
117
|
return adapterRoots.some(root => path === `${root}/.gitignore` || ['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].some(kind => path.startsWith(`${root}/${kind}/`)));
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
function nativeMarkdown(path) {
|
|
121
|
+
return Boolean(canonicalEntrypoint(path)) || /\.(?:md|mdc)$/i.test(path)
|
|
122
|
+
&& adapterRoots.some(root => {
|
|
123
|
+
if (!path.startsWith(`${root}/`)) return false;
|
|
124
|
+
const relative = path.slice(root.length + 1).split('/');
|
|
125
|
+
return !['tools', 'scripts', 'templates', 'external-skills', 'estado', 'tests', 'docs', 'adapters', 'workflows'].includes(relative[0].toLowerCase())
|
|
126
|
+
&& !['readme.md', 'instructions.md'].includes(relative.at(-1).toLowerCase());
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const nativeToml = path => path.startsWith('.gemini/commands/') && path.endsWith('.toml');
|
|
130
|
+
const nativeManagedText = path => nativeMarkdown(path) || nativeToml(path);
|
|
131
|
+
|
|
116
132
|
export function loadState(root) {
|
|
117
133
|
const content = read(root, statePath); if (content === null) return null;
|
|
118
134
|
let state; try { state = JSON.parse(content); } catch { fail('E_STATE', 'Invalid AgentDevKit state JSON.'); }
|
|
@@ -137,7 +153,9 @@ function extractBlock(content, first = begin, last = end) {
|
|
|
137
153
|
|
|
138
154
|
function blockDigest(path, content) {
|
|
139
155
|
const value = content.toString('utf8');
|
|
140
|
-
|
|
156
|
+
if (nativeToml(path)) return tomlManagedBlockDigest(content);
|
|
157
|
+
if (nativeMarkdown(path)) return managedBlockDigest(content);
|
|
158
|
+
const block = path === '.gitignore' ? extractBlock(value, '# BEGIN AGENTDEVKIT', '# END AGENTDEVKIT') : path.endsWith('/.gitignore') ? extractBlock(value, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : null;
|
|
141
159
|
return block === null ? null : hash(block);
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -175,21 +193,25 @@ function copyResources(source, destination) {
|
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
function readDerivedState(target) {
|
|
178
|
-
let entries = {}, outputs = {};
|
|
196
|
+
let entries = {}, outputs = {}, blocks = {}, files = {}, removedBlocks = [];
|
|
179
197
|
const derived = read(target, '.pbai4devs/estado/derived.json');
|
|
180
198
|
if (derived) {
|
|
181
199
|
try {
|
|
182
200
|
const value = JSON.parse(derived);
|
|
183
201
|
if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
|
|
184
|
-
||
|
|
185
|
-
entries = value.entries; outputs = value.outputs ?? {};
|
|
186
|
-
|
|
202
|
+
|| ['outputs', 'blocks', 'files'].some(key => value[key] !== undefined && (!value[key] || typeof value[key] !== 'object' || Array.isArray(value[key])))) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
203
|
+
entries = value.entries; outputs = value.outputs ?? {}; blocks = value.blocks ?? {}; files = value.files ?? {};
|
|
204
|
+
removedBlocks = value.removedBlocks ?? [];
|
|
205
|
+
if (!Array.isArray(removedBlocks) || removedBlocks.some(path => typeof path !== 'string' || !nativeManagedText(path))) fail('E_STATE', 'Invalid removed block state.');
|
|
206
|
+
for (const [path, digest] of Object.entries(entries)) if (!canonicalEntrypoint(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
187
207
|
for (const [path, digest] of Object.entries(outputs)) if (!allowedManaged(path) || !/^\.[^/]+\/(?:skills|agents|rules)\.json$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived output state.');
|
|
208
|
+
for (const [path, digest] of Object.entries(blocks)) if (!nativeManagedText(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived block state.');
|
|
209
|
+
for (const [path, digest] of Object.entries(files)) if (!allowedManaged(path) || !/\.(?:md|mdc|toml)$/.test(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived file state.');
|
|
188
210
|
}
|
|
189
211
|
catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
|
|
190
|
-
for (const path of [...Object.keys(entries), ...Object.keys(outputs)]) safePath(target, path);
|
|
212
|
+
for (const path of [...Object.keys(entries), ...Object.keys(outputs), ...Object.keys(blocks), ...Object.keys(files), ...removedBlocks]) safePath(target, path);
|
|
191
213
|
}
|
|
192
|
-
return { entries, outputs };
|
|
214
|
+
return { entries, outputs, blocks, files, removedBlocks };
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
|
|
@@ -198,15 +220,26 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
198
220
|
const selected = normalizeClients(clients ?? previous?.clients ?? ['all']);
|
|
199
221
|
const active = resolveAdapters(selected);
|
|
200
222
|
const changingClients = clients !== undefined || mode === 'agents';
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
223
|
+
const ownedEntrypoints = new Set(), mixedPaths = new Set(), nativeInputs = new Map();
|
|
224
|
+
const observeNative = (path, content) => {
|
|
225
|
+
if (nativeInputs.has(path)) {
|
|
226
|
+
const original = nativeInputs.get(path);
|
|
227
|
+
if (original === null ? content !== null : !content?.equals(original)) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
228
|
+
} else nativeInputs.set(path, content);
|
|
229
|
+
};
|
|
230
|
+
const { entries: derivedEntries, outputs: derivedOutputs, blocks: derivedBlocks, files: derivedFiles, removedBlocks } = readDerivedState(target);
|
|
231
|
+
const previousBlocks = Object.fromEntries(Object.entries(previous?.blocks || {}).filter(([path]) => !removedBlocks.includes(path)));
|
|
232
|
+
const knownBlocks = { ...previousBlocks, ...derivedBlocks };
|
|
233
|
+
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previousBlocks), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs), ...Object.keys(derivedBlocks), ...Object.keys(derivedFiles)];
|
|
234
|
+
const entryPaths = new Map(entryNames.map(name => [name, resolveEntrypoint(target, name)]));
|
|
235
|
+
const blockHash = path => knownBlocks[path]
|
|
236
|
+
?? (canonicalEntrypoint(path) ? Object.entries(knownBlocks).find(([candidate]) => canonicalEntrypoint(candidate) === canonicalEntrypoint(path))?.[1] : undefined);
|
|
204
237
|
const preserved = new Map();
|
|
205
238
|
const recordPreserved = (path, reason) => { preserved.set(path, { path, reason }); return true; };
|
|
206
|
-
const native = path =>
|
|
239
|
+
const native = path => canonicalEntrypoint(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
|
|
207
240
|
const boundary = path => {
|
|
208
241
|
if (!native(path)) return false;
|
|
209
|
-
const found = nativePathBoundary(target, path, ownedPaths, { preserveRootAliases: path === 'README.md'
|
|
242
|
+
const found = nativePathBoundary(target, path, ownedPaths, { preserveRootAliases: path === 'README.md' });
|
|
210
243
|
return found ? recordPreserved(found.path, found.reason) : false;
|
|
211
244
|
};
|
|
212
245
|
const owned = path => ownedPaths.includes(path);
|
|
@@ -216,20 +249,18 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
216
249
|
if (!native(path)) return false;
|
|
217
250
|
if (boundary(path)) return true;
|
|
218
251
|
const item = stat(safePath(target, path));
|
|
219
|
-
if (!item
|
|
252
|
+
if (!item) { observeNative(path, null); return false; }
|
|
253
|
+
const current = item.isFile() ? read(target, path) : null;
|
|
254
|
+
if (item.isFile()) observeNative(path, current);
|
|
255
|
+
if (owned(path)) return false;
|
|
220
256
|
if (!item.isFile() && ownedPaths.some(value => value.startsWith(`${path}/`))) fail('E_PATH', `Managed subtree occupies a file destination: ${path}`);
|
|
221
257
|
if (item.isFile()) {
|
|
222
|
-
|
|
258
|
+
if (nativeManagedText(path)) return false;
|
|
223
259
|
if (generatedReference(current)) return false;
|
|
224
260
|
if (path.endsWith('/.gitignore') && current.toString('utf8').split(/\r?\n/).some(line => ['# BEGIN AGENT IGNORE', '# END AGENT IGNORE'].includes(line))) return false;
|
|
225
261
|
}
|
|
226
262
|
return recordPreserved(path, item.isFile() ? 'existing-file' : 'existing-directory');
|
|
227
263
|
};
|
|
228
|
-
for (const name of entryNames) {
|
|
229
|
-
if (preserveNative(name)) continue;
|
|
230
|
-
const current = read(target, name);
|
|
231
|
-
if (current && previous?.files[name] && previous.files[name] !== hash(current)) { protectedEntries.add(name); entryConflicts.push(name); }
|
|
232
|
-
}
|
|
233
264
|
const stage = realpathSync(mkdtempSync(join(tmpdir(), 'agentdevkit-')));
|
|
234
265
|
try {
|
|
235
266
|
const existing = copyResources(join(target, '.pbai4devs'), join(stage, '.pbai4devs'));
|
|
@@ -253,23 +284,44 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
253
284
|
const ownIgnore = read(target, '.pbai4devs/.gitignore');
|
|
254
285
|
if (ownIgnore) write(stage, '.pbai4devs/.gitignore', ownIgnore);
|
|
255
286
|
writeAdapterManifest(stage, { clients: selected });
|
|
256
|
-
const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, output() {} });
|
|
257
|
-
for (const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
287
|
+
const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, recordDerivedState: false, output() {} });
|
|
288
|
+
for (const canonical of active.entrypoints) {
|
|
289
|
+
const name = entryPaths.get(canonical);
|
|
290
|
+
if (preserveNative(name)) continue;
|
|
291
|
+
const current = read(target, name), template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', canonical), 'utf8');
|
|
292
|
+
observeNative(name, current);
|
|
293
|
+
const legacy = entryContent(null, template, language, canonical);
|
|
294
|
+
const content = renderEntrypointImports(target, legacy.replace(`${begin}\n`, '').replace(`\n${end}`, ''));
|
|
262
295
|
const knownFile = current && (previous?.files[name] === hash(current) || derivedEntries[name] === hash(current));
|
|
263
|
-
const knownLegacy = current && previous?.blocks[name] && current.toString('utf8') === entryContent(null, template, previous.language,
|
|
296
|
+
const knownLegacy = current && previous?.blocks[name] && current.toString('utf8') === entryContent(null, template, previous.language, canonical);
|
|
264
297
|
if (!current || knownFile || knownLegacy) { write(stage, name, content); ownedEntrypoints.add(name); }
|
|
298
|
+
else {
|
|
299
|
+
const body = entrypointReference(target, canonical);
|
|
300
|
+
const merged = mergeManagedContent(current, body, { expectedHash: blockHash(name) });
|
|
301
|
+
write(stage, name, merged.text); mixedPaths.add(name); recordPreserved(name, 'existing-file-extended');
|
|
302
|
+
}
|
|
265
303
|
}
|
|
266
304
|
if (!preserveNative('README.md') && !read(target, 'README.md')) write(stage, 'README.md', `---\ndescription: ${JSON.stringify(text(language, 'readme'))}\nlanguage: ${language}\n---\n\n# ${text(language, 'project')}\n\n${text(language, 'readme')}\n`);
|
|
267
305
|
if (!existsSync(join(stage, '.pbai4devs/estado/README.md'))) write(stage, '.pbai4devs/estado/README.md', `---\ndescription: AgentDevKit state.\n---\n\n# AgentDevKit\n\n${text(language, 'state')}\n`);
|
|
268
306
|
const unknown = new Set(existing.filter(path => !supplied.has(path) && !previous?.files[`.pbai4devs/${path}`] && !internal.has(path)));
|
|
269
307
|
const paths = walk(stage, '', path => !path.split('/').includes('.agent-tests')).filter(path => !path.startsWith('.pbai4devs/') || !unknown.has(path.slice(11)));
|
|
270
308
|
const desired = new Map(paths.filter(path => !preserveNative(path)).map(path => [path, readFileSync(join(stage, path))]));
|
|
309
|
+
for (const [path, content] of desired) {
|
|
310
|
+
if (!nativeManagedText(path) || canonicalEntrypoint(path)) continue;
|
|
311
|
+
const current = read(target, path);
|
|
312
|
+
observeNative(path, current);
|
|
313
|
+
if (!current) continue;
|
|
314
|
+
const known = [previous?.files[path], derivedFiles[path]].some(digest => digest && digest === hash(current));
|
|
315
|
+
if (known) continue;
|
|
316
|
+
const source = content.toString('utf8'), marker = '<!-- gerado: sincronizar-agentes -->';
|
|
317
|
+
const index = source.indexOf(marker);
|
|
318
|
+
if (!nativeToml(path) && index < 0) fail('E_TEMPLATE', `Missing reference marker: ${path}`);
|
|
319
|
+
const body = nativeToml(path) ? JSON.parse(source.match(/^prompt = (.+)$/m)[1]) : source.slice(index + marker.length).trim();
|
|
320
|
+
const merged = (nativeToml(path) ? mergeTomlContent : mergeManagedContent)(current, body, { expectedHash: blockHash(path) });
|
|
321
|
+
desired.set(path, Buffer.from(merged.text)); mixedPaths.add(path); recordPreserved(path, 'existing-file-extended');
|
|
322
|
+
}
|
|
271
323
|
// Native discovery manifests must not advertise resources omitted at a human boundary.
|
|
272
|
-
const overlapsPreserved = path => [...preserved.keys()].some(kept => path === kept || path.startsWith(`${kept}/`) || kept.startsWith(`${path}/`));
|
|
324
|
+
const overlapsPreserved = path => [...preserved.keys()].some(kept => !mixedPaths.has(kept) && (path === kept || path.startsWith(`${kept}/`) || kept.startsWith(`${path}/`)));
|
|
273
325
|
for (const [path, content] of desired) if (/^\.agents\/(skills|rules|agents)\.json$/.test(path)) {
|
|
274
326
|
const discovery = JSON.parse(content);
|
|
275
327
|
discovery.entries = discovery.entries.filter(entry => !overlapsPreserved(entry.path));
|
|
@@ -284,40 +336,46 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
284
336
|
else manifest.files[path] = hash(content);
|
|
285
337
|
}
|
|
286
338
|
desired.set(statePath, Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`));
|
|
287
|
-
const changes = [], conflicts = [
|
|
339
|
+
const changes = [], conflicts = [];
|
|
340
|
+
const aliasedEntrypoint = path => canonicalEntrypoint(path) && entryPaths.get(canonicalEntrypoint(path)) !== path;
|
|
288
341
|
for (const [path, content] of desired) {
|
|
289
342
|
const current = read(target, path);
|
|
343
|
+
if (nativeInputs.has(path) && (nativeInputs.get(path) === null ? current !== null : !current?.equals(nativeInputs.get(path)))) fail('E_CHANGED', `File changed while planning: ${path}`);
|
|
290
344
|
if (current?.equals(content)) continue;
|
|
291
|
-
const managed = previous?.
|
|
292
|
-
|
|
293
|
-
const appendBlock =
|
|
345
|
+
const managed = [previous?.files[path], derivedOutputs[path], derivedEntries[path], derivedFiles[path]].some(digest => digest && hash(current || '') === digest)
|
|
346
|
+
|| knownBlocks[path] && blockDigest(path, current || Buffer.alloc(0)) === knownBlocks[path];
|
|
347
|
+
const appendBlock = mixedPaths.has(path) || canonicalEntrypoint(path) || path === '.gitignore' || path.endsWith('/.gitignore');
|
|
294
348
|
const conflict = current && path !== statePath && !managed && !appendBlock;
|
|
295
|
-
const changedBlock = current &&
|
|
349
|
+
const changedBlock = current && knownBlocks[path] && !managed;
|
|
296
350
|
if (conflict || changedBlock) conflicts.push(path);
|
|
297
351
|
changes.push({ path, action: current ? 'update' : 'create', content, beforeHash: current ? hash(current) : null });
|
|
298
352
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
for (const [path, digest] of Object.entries(previous?.files || {})) if (!desired.has(path) && !protectedEntries.has(path)) {
|
|
353
|
+
const wholeRecords = [previous?.files || {}, derivedEntries, derivedFiles];
|
|
354
|
+
for (const path of new Set(wholeRecords.flatMap(record => Object.keys(record)))) if (!desired.has(path)) {
|
|
355
|
+
if (aliasedEntrypoint(path)) continue;
|
|
304
356
|
const current = read(target, path); if (!current) continue;
|
|
305
|
-
|
|
357
|
+
const known = wholeRecords.some(record => record[path] === hash(current));
|
|
358
|
+
if (nativeManagedText(path) && !known) { recordPreserved(path, 'existing-file'); continue; }
|
|
359
|
+
if (!known) conflicts.push(path);
|
|
306
360
|
changes.push({ path, action: 'remove', content: null, beforeHash: hash(current) });
|
|
307
361
|
}
|
|
308
|
-
for (const [
|
|
362
|
+
for (const [recordedPath, digest] of Object.entries(knownBlocks)) {
|
|
363
|
+
const path = canonicalEntrypoint(recordedPath) ? entryPaths.get(canonicalEntrypoint(recordedPath)) : recordedPath;
|
|
364
|
+
if (desired.has(path)) continue;
|
|
365
|
+
if (changes.some(change => change.path === path)) continue;
|
|
309
366
|
if (path === '.gitignore') continue;
|
|
310
367
|
const current = read(target, path); if (!current) continue;
|
|
311
368
|
const source = current.toString('utf8');
|
|
312
|
-
const block = path
|
|
369
|
+
const block = path.endsWith('/.gitignore') ? extractBlock(source, '# BEGIN AGENT IGNORE', '# END AGENT IGNORE') : nativeToml(path) ? extractTomlManagedBlock(current) : extractManagedBlock(current);
|
|
313
370
|
if (!block || hash(block) !== digest) conflicts.push(path);
|
|
314
371
|
if (!block) continue;
|
|
315
|
-
let content = source.replace(block, '');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
372
|
+
let content = path.endsWith('/.gitignore') ? source.replace(block, '') : (nativeToml(path) ? stripTomlContent : stripManagedContent)(current, digest);
|
|
373
|
+
let removable = path.endsWith('/.gitignore') && !content.trim();
|
|
374
|
+
if (canonicalEntrypoint(path)) {
|
|
375
|
+
const template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', canonicalEntrypoint(path)), 'utf8');
|
|
376
|
+
if (source === entryContent(null, template, previous?.language || language, canonicalEntrypoint(path))) { content = ''; removable = true; }
|
|
319
377
|
}
|
|
320
|
-
changes.push({ path, action:
|
|
378
|
+
changes.push({ path, action: removable ? 'remove' : 'update', content: removable ? null : Buffer.from(content), beforeHash: hash(current) });
|
|
321
379
|
}
|
|
322
380
|
function nativeFiles(root) {
|
|
323
381
|
if (boundary(root)) return [];
|
|
@@ -340,9 +398,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
340
398
|
const path = full.slice(root.length + 1);
|
|
341
399
|
if (!['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].includes(path.split('/')[0])) continue;
|
|
342
400
|
if (desired.has(full) || changes.some(change => change.path === full)) continue;
|
|
343
|
-
const current = read(target, full)
|
|
344
|
-
|
|
345
|
-
if (!
|
|
401
|
+
const current = read(target, full);
|
|
402
|
+
const known = [previous?.files[full], derivedFiles[full]].some(digest => digest && digest === hash(current));
|
|
403
|
+
if (preserved.has(full) || !known || !generatedReference(current)) { recordPreserved(full, 'existing-file'); continue; }
|
|
346
404
|
changes.push({ path: full, action: 'remove', content: null, beforeHash: hash(current) });
|
|
347
405
|
}
|
|
348
406
|
const legacy = '.pbai4devs/adapters/README.md', legacyContent = read(target, legacy);
|
|
@@ -352,9 +410,9 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
352
410
|
changes.push({ path: legacy, action: 'remove', content: null, beforeHash: hash(legacyContent) });
|
|
353
411
|
}
|
|
354
412
|
const retiredRoots = adapterRoots.filter(root => !active.roots.includes(root) && changes.some(change => change.action === 'remove' && change.path.startsWith(`${root}/`) && (previous?.files[change.path] || previous?.blocks[change.path])));
|
|
355
|
-
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !overlapsPreserved(path));
|
|
413
|
+
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !mixedPaths.has(path) && !overlapsPreserved(path));
|
|
356
414
|
const hashes = {};
|
|
357
|
-
for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !
|
|
415
|
+
for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !mixedPaths.has(path) && !overlapsPreserved(path) && !aliasedEntrypoint(path)) hashes[path] = digest;
|
|
358
416
|
for (const path of ownedEntrypoints) { const current = read(target, path); if (current && (previous?.blocks[path] || derivedEntries[path] === hash(current))) hashes[path] = hash(current); }
|
|
359
417
|
for (const path of Object.keys(previous?.blocks || {})) if (path.endsWith('/.gitignore') && adapterRoots.some(root => path === `${root}/.gitignore`)) {
|
|
360
418
|
const current = read(target, path);
|
|
@@ -385,16 +443,22 @@ export function applyPlan(plan) {
|
|
|
385
443
|
for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content, beforeModes.get(path));
|
|
386
444
|
const applied = [], removedDirectories = [];
|
|
387
445
|
let untracked = false;
|
|
446
|
+
const assertOriginal = path => {
|
|
447
|
+
const file = safePath(target, path), info = stat(file), bytes = before.get(path);
|
|
448
|
+
if (bytes === null ? info !== null : !info?.isFile() || !readFileSync(file).equals(bytes) || (info.mode & 0o777) !== beforeModes.get(path)) fail('E_CHANGED', `File changed before writing: ${path}`);
|
|
449
|
+
};
|
|
388
450
|
try {
|
|
451
|
+
for (const change of plan.changes) assertOriginal(change.path);
|
|
389
452
|
if (plan.untrack?.paths.length) { applyUntrack(plan.untrack); untracked = true; }
|
|
390
453
|
for (const change of plan.changes) {
|
|
454
|
+
assertOriginal(change.path);
|
|
391
455
|
const file = safePath(target, change.path);
|
|
392
456
|
const content = change.content;
|
|
393
457
|
const signature = { path: change.path, hash: content === null ? null : hash(content) };
|
|
394
458
|
if (content === null) { rmSync(file, { force: true }); applied.push(signature); }
|
|
395
459
|
else atomicWrite(target, change.path, content, beforeModes.get(change.path), mode => {
|
|
396
460
|
signature.mode = mode; applied.push(signature);
|
|
397
|
-
});
|
|
461
|
+
}, () => assertOriginal(change.path));
|
|
398
462
|
}
|
|
399
463
|
pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
|
|
400
464
|
const changesFallback = plan.changes.some(change => change.path === '.gitignore');
|
|
@@ -413,16 +477,25 @@ export function applyPlan(plan) {
|
|
|
413
477
|
}
|
|
414
478
|
for (const appliedFile of applied.reverse()) {
|
|
415
479
|
const { path } = appliedFile;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
480
|
+
const stillApplied = () => {
|
|
481
|
+
try {
|
|
482
|
+
const file = safePath(target, path), info = stat(file);
|
|
483
|
+
return appliedFile.hash === null ? !info : Boolean(info?.isFile()
|
|
484
|
+
&& (info.mode & 0o777) === appliedFile.mode && hash(readFileSync(file)) === appliedFile.hash);
|
|
485
|
+
} catch { return false; }
|
|
486
|
+
};
|
|
487
|
+
const unchanged = stillApplied();
|
|
422
488
|
if (!unchanged) { preserved.push(path); continue; }
|
|
423
489
|
const content = before.get(path);
|
|
424
|
-
try {
|
|
425
|
-
|
|
490
|
+
try {
|
|
491
|
+
if (content) atomicWrite(target, path, content, beforeModes.get(path), undefined, () => {
|
|
492
|
+
if (!stillApplied()) fail('E_CHANGED', `File changed during rollback: ${path}`);
|
|
493
|
+
});
|
|
494
|
+
else rmSync(safePath(target, path), { force: true });
|
|
495
|
+
} catch (failure) {
|
|
496
|
+
if (failure.code === 'E_CHANGED') preserved.push(path);
|
|
497
|
+
else rollbackErrors.push(failure);
|
|
498
|
+
}
|
|
426
499
|
}
|
|
427
500
|
if (preserved.length) {
|
|
428
501
|
try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
|
|
@@ -469,11 +542,14 @@ export function inspect(target) {
|
|
|
469
542
|
const derived = readDerivedState(target);
|
|
470
543
|
const issues = [];
|
|
471
544
|
for (const [path, digest] of Object.entries(state.files)) {
|
|
545
|
+
if (derived.blocks[path] || derived.removedBlocks.includes(path)) continue;
|
|
472
546
|
const content = read(target, path);
|
|
473
|
-
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path]) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
547
|
+
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path] && hash(content) !== derived.files[path] && hash(content) !== derived.entries[path]) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
474
548
|
}
|
|
475
|
-
for (const [path, digest] of Object.entries(state.blocks)) {
|
|
549
|
+
for (const [path, digest] of Object.entries({ ...state.blocks, ...derived.blocks })) {
|
|
550
|
+
if (derived.removedBlocks.includes(path) && !Object.hasOwn(derived.blocks, path)) continue;
|
|
476
551
|
const content = read(target, path);
|
|
552
|
+
if (content && [derived.entries[path], derived.files[path]].some(value => value && hash(content) === value)) continue;
|
|
477
553
|
if (!content || blockDigest(path, content) !== digest) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
478
554
|
}
|
|
479
555
|
return { installed: true, version: state.version, language: state.language, issues };
|