@pathbit/agentdevkit 0.0.7 → 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 +84 -15
- 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 +145 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +2 -1
- package/CHANGELOG.md +9 -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 +8 -2
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +1 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/package.json +1 -1
- package/src/cli.mjs +10 -4
- package/src/install.mjs +61 -22
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
@@ -63,6 +63,7 @@ export function renderClientIgnore(template, settings = {}) {
|
|
|
63
63
|
|
|
64
64
|
const localStat = file => { try { return lstatSync(file); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; } };
|
|
65
65
|
const localRead = file => localStat(file) ? readFileSync(file, 'utf8') : null;
|
|
66
|
+
const localMode = file => { const item = localStat(file); return item ? item.mode & 0o777 : null; };
|
|
66
67
|
const portable = path => path.split(sep).join('/');
|
|
67
68
|
|
|
68
69
|
function canonicalExisting(path) {
|
|
@@ -144,6 +145,61 @@ function artifactProbes(root, patterns) {
|
|
|
144
145
|
return [...probes];
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
const matchesComponent = (part, pattern) => posix.matchesGlob(`x${part}`, `x${pattern.replace(/[{}()]/g, '\\$&')}`);
|
|
149
|
+
|
|
150
|
+
function matchesParts(parts, pattern) {
|
|
151
|
+
const cache = new Map();
|
|
152
|
+
function match(part, glob) {
|
|
153
|
+
const key = `${part}:${glob}`; if (cache.has(key)) return cache.get(key);
|
|
154
|
+
const expected = pattern[glob];
|
|
155
|
+
const result = glob === pattern.length ? part === parts.length
|
|
156
|
+
: expected === '**' ? match(part, glob + 1) || part < parts.length && match(part + 1, glob)
|
|
157
|
+
: part < parts.length && matchesComponent(parts[part], expected) && match(part + 1, glob + 1);
|
|
158
|
+
cache.set(key, result); return result;
|
|
159
|
+
}
|
|
160
|
+
return match(0, 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function artifactPath(path, patterns, directory = false) {
|
|
164
|
+
const parts = path.split('/');
|
|
165
|
+
return patterns.some(pattern => {
|
|
166
|
+
const rule = pattern.slice(3).replace(/\/$/, '');
|
|
167
|
+
return parts.some((part, index) => (index < parts.length - 1 || directory || !pattern.endsWith('/'))
|
|
168
|
+
&& (rule.includes('/') ? matchesParts(parts.slice(0, index + 1), ['**', ...rule.split('/')])
|
|
169
|
+
: matchesComponent(part, rule)));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function visibleArtifactProbes(root, location, patterns) {
|
|
174
|
+
const listed = gitCommand(location.top, ['--literal-pathspecs', '-c', 'core.excludesFile=', 'ls-files', '--cached', '--others', '--exclude-standard', '-z', '--', location.prefix ? `${location.prefix}/` : '.']);
|
|
175
|
+
if (listed.error || listed.status !== 0) throw new Error(listed.error?.message || listed.stderr || 'Falha ao conferir artefatos existentes.');
|
|
176
|
+
const paths = listed.stdout.split('\0').filter(Boolean), prefix = location.prefix ? `${location.prefix}/` : '';
|
|
177
|
+
const probes = new Set(paths.filter(path => path.startsWith(prefix)).map(path => path.slice(prefix.length)).filter(path => artifactPath(path, patterns)));
|
|
178
|
+
const ignores = new Set(paths.filter(path => path.startsWith(prefix) && (path === '.gitignore' || path.endsWith('/.gitignore'))));
|
|
179
|
+
let ancestor = root;
|
|
180
|
+
while (true) {
|
|
181
|
+
ignores.add(portable(relative(location.top, join(ancestor, '.gitignore'))));
|
|
182
|
+
if (ancestor === location.top) break;
|
|
183
|
+
ancestor = dirname(ancestor);
|
|
184
|
+
}
|
|
185
|
+
for (const path of ignores) {
|
|
186
|
+
const file = join(location.top, path), item = localStat(file);
|
|
187
|
+
if (!item?.isFile() || item.isSymbolicLink()) continue;
|
|
188
|
+
// Git resolves precedence; these literals only add witnesses for named exceptions.
|
|
189
|
+
for (const line of readFileSync(checkedLocalPath(file), 'utf8').split(/\r?\n/)) {
|
|
190
|
+
if (!line.startsWith('!') || line.length === 1) continue;
|
|
191
|
+
const rule = line.slice(1).replace(/(?<!\\) +$/, ''), directory = rule.endsWith('/') || rule.endsWith('/**');
|
|
192
|
+
const literal = rule.replace(/(?:\/\*\*)+\/?$/, '').replace(/\/$/, '').replace(/^\//, '');
|
|
193
|
+
if (/(^|[^\\])[*?\[]/.test(literal)) continue;
|
|
194
|
+
const ruleBase = !rule.startsWith('/') && !literal.includes('/') && relative(dirname(file), root).split(sep)[0] !== '..' ? root : dirname(file);
|
|
195
|
+
const candidate = portable(relative(root, resolve(ruleBase, literal.replace(/\\(.)/g, '$1'))));
|
|
196
|
+
if (!candidate || candidate === '..' || candidate.startsWith('../') || !artifactPath(candidate, patterns, directory)) continue;
|
|
197
|
+
probes.add(checkedOutput(directory ? `${candidate}/.agentdevkit-artifact-probe` : candidate));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [...probes];
|
|
201
|
+
}
|
|
202
|
+
|
|
147
203
|
function localIgnoreSource(location, source) {
|
|
148
204
|
const path = resolve(location.top, source);
|
|
149
205
|
if (path === location.exclude) return true;
|
|
@@ -204,9 +260,9 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
204
260
|
const body = fixed.split(/\r?\n/).map(line => location?.prefix && line.startsWith('/') ? `/${escapePattern(location.prefix)}${line}` : line).join('\n');
|
|
205
261
|
const target = location?.exclude ?? checkedLocalPath(join(root, '.gitignore'));
|
|
206
262
|
const before = localRead(target), after = localBlock(before, first, last, body);
|
|
207
|
-
const edits = before === after ? [] : [{ path: target, before, after }];
|
|
263
|
+
const edits = before === after ? [] : [{ path: target, before, after, beforeMode: localMode(target) }];
|
|
208
264
|
const probes = [...paths, ...rootProbes(root, roots)];
|
|
209
|
-
const artifactPaths = artifactProbes(root, artifacts);
|
|
265
|
+
const artifactPaths = [...new Set([...artifactProbes(root, artifacts), ...(location ? visibleArtifactProbes(root, location, artifacts) : [])])];
|
|
210
266
|
const wanted = new Set(probes.map(path => location?.prefix ? `${location.prefix}/${path}` : path));
|
|
211
267
|
if (location && !before?.includes(first) && !localRead(join(root, '.gitignore'))?.includes(first)) {
|
|
212
268
|
const ignored = ignoreMatches(location, [...probes, ...artifactPaths]);
|
|
@@ -215,10 +271,10 @@ export function planLocalIgnore(root, { outputs = [], templateRoot } = {}) {
|
|
|
215
271
|
if (location) {
|
|
216
272
|
const fallback = checkedLocalPath(join(root, '.gitignore'));
|
|
217
273
|
const old = localRead(fallback), next = localBlock(old, first, last, null);
|
|
218
|
-
if (old !== null && old !== next) edits.push({ path: fallback, before: old, after: next });
|
|
274
|
+
if (old !== null && old !== next) edits.push({ path: fallback, before: old, after: next, beforeMode: localMode(fallback) });
|
|
219
275
|
}
|
|
220
276
|
const tracked = location ? gitCommand(location.top, ['ls-files', '-z']).stdout.split('\0').filter(path => wanted.has(path)).map(path => location.prefix ? path.slice(location.prefix.length + 1) : path) : [];
|
|
221
|
-
return { root, status: location ? 'git' : 'fallback', location, paths, roots, artifactProbes: artifactPaths, edits, trackedOutputs: tracked, first, last };
|
|
277
|
+
return { root, status: location ? 'git' : 'fallback', location, paths, roots, artifactPatterns: artifacts, artifactProbes: artifactPaths, edits, trackedOutputs: tracked, first, last };
|
|
222
278
|
}
|
|
223
279
|
|
|
224
280
|
export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
@@ -235,7 +291,8 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
235
291
|
return { ...item, path };
|
|
236
292
|
});
|
|
237
293
|
if (plan.location && (plan.paths.length || plan.artifactProbes?.length)) {
|
|
238
|
-
const probes = [...plan.paths, ...rootProbes(plan.root, plan.roots || []), ...(plan.artifactProbes || [])
|
|
294
|
+
const probes = [...new Set([...plan.paths, ...rootProbes(plan.root, plan.roots || []), ...(plan.artifactProbes || []),
|
|
295
|
+
...(plan.artifactPatterns ? visibleArtifactProbes(plan.root, plan.location, plan.artifactPatterns) : [])])];
|
|
239
296
|
const { paths: relativePaths, matches } = ignoreMatches(plan.location, probes);
|
|
240
297
|
for (let index = 0; index < relativePaths.length; index++) {
|
|
241
298
|
const match = matches.get(relativePaths[index]);
|
|
@@ -252,10 +309,21 @@ export function checkLocalIgnore(plan, { replacedBlocks = [] } = {}) {
|
|
|
252
309
|
export function assertLocalIgnoreUnchanged(plan) {
|
|
253
310
|
for (const edit of plan.edits) {
|
|
254
311
|
checkedLocalPath(edit.path);
|
|
255
|
-
if (localRead(edit.path) !== edit.before) throw new Error(`Exclusao alterada apos planejar: ${edit.path}`);
|
|
312
|
+
if (localRead(edit.path) !== edit.before || Object.hasOwn(edit, 'beforeMode') && localMode(edit.path) !== edit.beforeMode) throw new Error(`Exclusao alterada apos planejar: ${edit.path}`);
|
|
256
313
|
}
|
|
257
314
|
}
|
|
258
315
|
|
|
316
|
+
function atomicLocalWrite(path, content, mode, published) {
|
|
317
|
+
const temporary = `${path}.agentdevkit-${randomUUID()}`;
|
|
318
|
+
try {
|
|
319
|
+
writeFileSync(temporary, content, { flag: 'wx' });
|
|
320
|
+
if (mode !== null && mode !== undefined) chmodSync(temporary, mode);
|
|
321
|
+
const writtenMode = localMode(temporary);
|
|
322
|
+
renameSync(temporary, path);
|
|
323
|
+
published?.(writtenMode);
|
|
324
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
325
|
+
}
|
|
326
|
+
|
|
259
327
|
export function applyLocalIgnore(plan) {
|
|
260
328
|
assertLocalIgnoreUnchanged(plan);
|
|
261
329
|
const before = checkLocalIgnore(plan);
|
|
@@ -264,18 +332,19 @@ export function applyLocalIgnore(plan) {
|
|
|
264
332
|
try {
|
|
265
333
|
for (const edit of plan.edits) {
|
|
266
334
|
mkdirSync(dirname(edit.path), { recursive: true });
|
|
267
|
-
|
|
268
|
-
try { writeFileSync(temporary, edit.after, { flag: 'wx' }); renameSync(temporary, edit.path); }
|
|
269
|
-
finally { rmSync(temporary, { force: true }); }
|
|
270
|
-
written.push(edit);
|
|
335
|
+
atomicLocalWrite(edit.path, edit.after, edit.beforeMode, mode => written.push({ edit, mode }));
|
|
271
336
|
}
|
|
272
337
|
const verified = checkLocalIgnore({ ...plan, edits: [], trackedOutputs: [] });
|
|
273
338
|
if (verified.missing.length) throw new Error(`Regras humanas reabrem derivados locais: ${verified.missing.join(', ')}`);
|
|
274
339
|
} catch (error) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
340
|
+
const rollbackErrors = [];
|
|
341
|
+
for (const { edit, mode } of written.reverse()) {
|
|
342
|
+
try {
|
|
343
|
+
if (localRead(edit.path) !== edit.after || localMode(edit.path) !== mode) continue;
|
|
344
|
+
if (edit.before === null) rmSync(edit.path, { force: true }); else atomicLocalWrite(edit.path, edit.before, edit.beforeMode);
|
|
345
|
+
} catch (failure) { rollbackErrors.push(failure); }
|
|
278
346
|
}
|
|
347
|
+
if (rollbackErrors.length) throw new AggregateError([error, ...rollbackErrors], `${error.message}; rollback incompleto: ${rollbackErrors.map(failure => failure.message).join('; ')}`, { cause: error });
|
|
279
348
|
throw error;
|
|
280
349
|
}
|
|
281
350
|
return { status: plan.status, changed: plan.edits.length, trackedOutputs: plan.trackedOutputs };
|
|
@@ -101,6 +101,14 @@ function sourcePath(root, name) {
|
|
|
101
101
|
return file;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function storagePath(root, name, kind) {
|
|
105
|
+
const file = sourcePath(root, name);
|
|
106
|
+
let item;
|
|
107
|
+
try { item = lstatSync(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
108
|
+
if (item && !(kind === 'directory' ? item.isDirectory() : item.isFile())) fail(`Caminho local exige ${kind}: ${name}`);
|
|
109
|
+
return file;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
export function loadCatalog(root = projectRoot) {
|
|
105
113
|
root = realpathSync(root);
|
|
106
114
|
const home = sourcePath(root, '.pbai4devs/tools'); const catalog = Object.create(null);
|
|
@@ -272,15 +280,21 @@ export class ToolManager {
|
|
|
272
280
|
return { mode: 'setup', selection: selected, platform: this.target, node: process.version, nodePath: process.execPath, npm, results: entries, summary, status: summary.failed ? 'failed' : summary.passed ? 'passed' : 'not-applicable', exitCode: summary.failed ? 1 : 0 };
|
|
273
281
|
}
|
|
274
282
|
local(name) { this.entry(name); return join(this.home, name, '.local'); }
|
|
283
|
+
storage(name) {
|
|
284
|
+
const local = this.local(name), tool = join(this.home, name);
|
|
285
|
+
for (const path of [this.home, tool, local, join(local, 'versions'), join(tool, 'versions'), join(this.home, '.cache'), join(this.home, '.cache/npm'), join(this.home, 'npm-cache')]) storagePath(this.root, path, 'directory');
|
|
286
|
+
for (const path of [join(local, 'CURRENT.json'), join(tool, 'CURRENT.json')]) storagePath(this.root, path, 'file');
|
|
287
|
+
return { local, tool };
|
|
288
|
+
}
|
|
275
289
|
current(name) {
|
|
276
|
-
this.
|
|
277
|
-
const local = this.local(name); const legacy = join(this.home, name);
|
|
290
|
+
const { local, tool: legacy } = this.storage(name);
|
|
278
291
|
const base = existsSync(join(local, 'CURRENT.json')) ? local : legacy;
|
|
279
292
|
const file = join(base, 'CURRENT.json');
|
|
280
293
|
if (!existsSync(file)) fail(`Ferramenta ausente: ${name}; execute install ${name}.`);
|
|
281
294
|
const info = json(file);
|
|
282
295
|
if (info.name !== name || info.platform !== this.target || !versionPattern.test(info.version || '')) fail(`Instalacao incompativel: ${name}`);
|
|
283
|
-
|
|
296
|
+
const prefix = storagePath(this.root, join(base, 'versions', `${info.version}-${this.target}`), 'directory');
|
|
297
|
+
return { ...info, layout: base === local ? 'local' : 'legacy', prefix };
|
|
284
298
|
}
|
|
285
299
|
check(name) {
|
|
286
300
|
const entry = this.entry(name); const runtime = this.runtime(name); const info = this.current(name);
|
|
@@ -306,7 +320,7 @@ export class ToolManager {
|
|
|
306
320
|
for (const legacy of [name, `${name}.cmd`, `${name}.ps1`, `${name}.sh`]) rmSync(join(bin, legacy), { force: true });
|
|
307
321
|
}
|
|
308
322
|
migrate(name) {
|
|
309
|
-
const
|
|
323
|
+
const { local, tool } = this.storage(name);
|
|
310
324
|
const legacyState = join(tool, 'CURRENT.json'); const legacyVersions = join(tool, 'versions');
|
|
311
325
|
if (existsSync(legacyState)) {
|
|
312
326
|
if (existsSync(join(local, 'CURRENT.json')) || (existsSync(join(local, 'versions')) && readdirSync(join(local, 'versions')).length)) fail(`Layouts local e antigo coexistem: ${name}; revise antes de migrar.`);
|
|
@@ -318,15 +332,15 @@ export class ToolManager {
|
|
|
318
332
|
}
|
|
319
333
|
const oldCache = join(this.home, 'npm-cache');
|
|
320
334
|
if (existsSync(oldCache)) {
|
|
321
|
-
const cache = join(this.home, '.cache'); mkdirSync(cache, { recursive: true });
|
|
322
|
-
|
|
335
|
+
const cache = storagePath(this.root, join(this.home, '.cache'), 'directory'); mkdirSync(cache, { recursive: true });
|
|
336
|
+
const destination = storagePath(this.root, join(cache, existsSync(join(cache, 'npm')) ? `npm-legacy-${randomUUID()}` : 'npm'), 'directory');
|
|
337
|
+
try { renameSync(oldCache, destination); }
|
|
323
338
|
catch (error) { if (error.code !== 'ENOENT' || existsSync(oldCache)) throw error; }
|
|
324
339
|
}
|
|
325
340
|
}
|
|
326
341
|
async withLock(name, action) {
|
|
327
|
-
const local = this.
|
|
328
|
-
|
|
329
|
-
mkdirSync(local, { recursive: true }); const lock = join(local, '.update-lock');
|
|
342
|
+
const { local } = this.storage(name);
|
|
343
|
+
mkdirSync(local, { recursive: true }); const lock = storagePath(this.root, join(local, '.update-lock'), 'directory');
|
|
330
344
|
try { mkdirSync(lock); } catch (error) {
|
|
331
345
|
if (error.code === 'EEXIST') {
|
|
332
346
|
const status = this.lockStatus(name);
|
|
@@ -334,8 +348,8 @@ export class ToolManager {
|
|
|
334
348
|
}
|
|
335
349
|
throw error;
|
|
336
350
|
}
|
|
337
|
-
try { writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() })); return await action(); }
|
|
338
|
-
finally { rmSync(lock, { recursive: true, force: true }); }
|
|
351
|
+
try { writeFileSync(storagePath(this.root, join(lock, 'owner.json'), 'file'), JSON.stringify({ pid: process.pid, hostname: hostname(), startedAt: new Date().toISOString() }), { flag: 'wx' }); return await action(); }
|
|
352
|
+
finally { rmSync(storagePath(this.root, lock, 'directory'), { recursive: true, force: true }); }
|
|
339
353
|
}
|
|
340
354
|
lockStatus(name) {
|
|
341
355
|
const lock = sourcePath(this.root, join(this.local(name), '.update-lock'));
|
|
@@ -380,29 +394,36 @@ export class ToolManager {
|
|
|
380
394
|
return this.updateLocked(name);
|
|
381
395
|
}); }
|
|
382
396
|
async update(name) { this.preflight([name]); return this.withLock(name, async () => { this.migrate(name); return this.updateLocked(name); }); }
|
|
383
|
-
commitState(name, temporary) {
|
|
397
|
+
commitState(name, temporary) {
|
|
398
|
+
const { local } = this.storage(name);
|
|
399
|
+
renameSync(storagePath(this.root, temporary, 'file'), storagePath(this.root, join(local, 'CURRENT.json'), 'file'));
|
|
400
|
+
}
|
|
384
401
|
async updateLocked(name) {
|
|
385
402
|
const entry = this.entry(name); const runtime = this.runtime(name); const release = validateRelease(name, entry, await this.fetchRelease(name, entry));
|
|
403
|
+
const { local } = this.storage(name);
|
|
404
|
+
const target = storagePath(this.root, join(local, 'versions', `${release.version}-${this.target}`), 'directory');
|
|
386
405
|
let installed;
|
|
387
406
|
try { installed = this.check(name); } catch { /* Ausente ou quebrado: reparar em staging. */ }
|
|
388
407
|
if (installed?.version === release.version) { this.launchers(name); return { ...installed, changed: false }; }
|
|
389
|
-
|
|
390
|
-
const stage = mkdtempSync(join(local, '.stage-'));
|
|
391
|
-
const temporary = join(local, `CURRENT.${
|
|
408
|
+
mkdirSync(storagePath(this.root, join(local, 'versions'), 'directory'), { recursive: true });
|
|
409
|
+
const stage = mkdtempSync(join(local, '.stage-'));
|
|
410
|
+
const temporary = storagePath(this.root, join(local, `CURRENT.${randomUUID()}.json`), 'file'); let previous; let moved = false;
|
|
392
411
|
try {
|
|
393
412
|
this.installPackage(stage, entry.download.package, release.version, entry, runtime);
|
|
413
|
+
this.storage(name); storagePath(this.root, stage, 'directory');
|
|
394
414
|
const pkg = json(join(stage, 'node_modules', entry.download.package, 'package.json')); const lock = json(join(stage, 'package-lock.json'));
|
|
395
415
|
if (pkg.name !== entry.download.package || pkg.version !== release.version || lock.packages?.[`node_modules/${entry.download.package}`]?.integrity !== release.dist.integrity) fail('Pacote instalado nao corresponde ao registro.');
|
|
396
416
|
prepareExecutables(stage, entry, this.target);
|
|
397
417
|
execute(stage, entry, entry.probe, true, entry.probeInput, toolEnvironment(entry, local, runtime), runtime);
|
|
418
|
+
this.storage(name); storagePath(this.root, target, 'directory');
|
|
398
419
|
const info = { name, package: entry.download.package, version: release.version, platform: this.target, node: runtime.version, nodePath: runtime.file, source: entry.source, tarball: release.dist.tarball, integrity: release.dist.integrity, checkedAt: new Date().toISOString() };
|
|
399
|
-
writeFileSync(temporary, `${JSON.stringify(info, null, 2)}\n
|
|
420
|
+
writeFileSync(storagePath(this.root, temporary, 'file'), `${JSON.stringify(info, null, 2)}\n`, { flag: 'wx' });
|
|
400
421
|
this.launchers(name);
|
|
401
|
-
if (existsSync(target)) { previous = `${target}.previous-${
|
|
402
|
-
try { renameSync(stage, target); moved = true; this.commitState(name, temporary); }
|
|
403
|
-
catch (error) { if (moved) rmSync(target, { recursive: true, force: true }); if (previous) renameSync(previous, target); throw error; }
|
|
422
|
+
if (existsSync(target)) { previous = storagePath(this.root, `${target}.previous-${randomUUID()}`, 'directory'); renameSync(storagePath(this.root, target, 'directory'), previous); }
|
|
423
|
+
try { renameSync(storagePath(this.root, stage, 'directory'), storagePath(this.root, target, 'directory')); moved = true; this.commitState(name, temporary); }
|
|
424
|
+
catch (error) { if (moved) rmSync(storagePath(this.root, target, 'directory'), { recursive: true, force: true }); if (previous) renameSync(storagePath(this.root, previous, 'directory'), storagePath(this.root, target, 'directory')); throw error; }
|
|
404
425
|
return { ...info, layout: 'local', prefix: target, changed: true };
|
|
405
|
-
} finally { rmSync(stage, { recursive: true, force: true }); rmSync(temporary, { force: true }); }
|
|
426
|
+
} finally { rmSync(storagePath(this.root, stage, 'directory'), { recursive: true, force: true }); rmSync(storagePath(this.root, temporary, 'file'), { force: true }); }
|
|
406
427
|
}
|
|
407
428
|
run(name, args) { const entry = this.entry(name); const runtime = this.runtime(name); return execute(this.current(name).prefix, entry, args, false, undefined, toolEnvironment(entry, this.local(name), runtime), runtime); }
|
|
408
429
|
}
|
|
@@ -232,6 +232,87 @@ test('falha ao gravar CURRENT restaura o target anterior e o pacote em uso', asy
|
|
|
232
232
|
assert.equal(manager.check('test-tool').version, '1.0.0'); assert.ok(!existsSync(join(local, '.update-lock')));
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
test('versions simbolico existente ou quebrado e recusado antes de baixar ou escrever', async t => {
|
|
236
|
+
for (const broken of [false, true]) await t.test(broken ? 'broken' : 'existing', async subtest => {
|
|
237
|
+
const { root, manager, state, local } = fixture(subtest);
|
|
238
|
+
const outside = join(root, 'outside'); mkdirSync(outside);
|
|
239
|
+
writeFileSync(join(outside, 'keep.txt'), 'preservado'); mkdirSync(local);
|
|
240
|
+
if (!createSymlink(subtest, broken ? join(outside, 'missing') : outside, join(local, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
241
|
+
for (const action of ['install', 'update']) await assert.rejects(manager[action]('test-tool'), /nao pode ser um link/);
|
|
242
|
+
assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
|
|
243
|
+
assert.deepEqual(readdirSync(outside), ['keep.txt']);
|
|
244
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
245
|
+
assert.equal(existsSync(join(local, 'CURRENT.json')), false);
|
|
246
|
+
assert.equal(existsSync(join(local, '.update-lock')), false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('recibo CURRENT e destino de versao simbolicos nao sao seguidos nem substituidos', async t => {
|
|
251
|
+
for (const kind of ['current', 'target', 'broken-target']) await t.test(kind, async subtest => {
|
|
252
|
+
const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
|
|
253
|
+
const receipt = readFileSync(join(local, 'CURRENT.json'));
|
|
254
|
+
const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
|
|
255
|
+
if (kind === 'current') {
|
|
256
|
+
writeFileSync(join(outside, 'CURRENT.json'), receipt); rmSync(join(local, 'CURRENT.json'));
|
|
257
|
+
if (!createSymlink(subtest, join(outside, 'CURRENT.json'), join(local, 'CURRENT.json'))) return;
|
|
258
|
+
assert.throws(() => manager.check('test-tool'), /nao pode ser um link/);
|
|
259
|
+
} else {
|
|
260
|
+
state.version = '2.0.0';
|
|
261
|
+
if (!createSymlink(subtest, kind === 'target' ? outside : join(outside, 'missing'), join(local, 'versions', `2.0.0-${platformKey()}`), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
262
|
+
}
|
|
263
|
+
const before = readdirSync(outside), installs = state.installs;
|
|
264
|
+
await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
|
|
265
|
+
assert.equal(state.installs, installs); assert.deepEqual(readdirSync(outside), before);
|
|
266
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
267
|
+
assert.deepEqual(readFileSync(kind === 'current' ? join(outside, 'CURRENT.json') : join(local, 'CURRENT.json')), receipt);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('troca de versions durante fetch ou install nao escapa da arvore local', async t => {
|
|
272
|
+
for (const phase of ['fetch', 'install']) await t.test(phase, async subtest => {
|
|
273
|
+
const { root, manager, state, local } = fixture(subtest); await manager.install('test-tool');
|
|
274
|
+
const receipt = readFileSync(join(local, 'CURRENT.json')); state.version = '2.0.0';
|
|
275
|
+
const outside = join(root, 'outside'); mkdirSync(outside);
|
|
276
|
+
const probe = join(local, 'symlink-probe');
|
|
277
|
+
if (!createSymlink(subtest, outside, probe, process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
278
|
+
unlinkSync(probe);
|
|
279
|
+
const versions = join(local, 'versions'), kept = join(local, 'versions-kept'); let linked = false;
|
|
280
|
+
const replace = () => {
|
|
281
|
+
renameSync(versions, kept);
|
|
282
|
+
symlinkSync(outside, versions, process.platform === 'win32' ? 'junction' : 'dir'); linked = true;
|
|
283
|
+
};
|
|
284
|
+
if (phase === 'fetch') manager.fetchRelease = async () => { replace(); return release('2.0.0'); };
|
|
285
|
+
else manager.installPackage = (prefix, name, version) => { packageFiles(prefix, name, version); replace(); };
|
|
286
|
+
try {
|
|
287
|
+
await assert.rejects(manager.update('test-tool'), /nao pode ser um link/);
|
|
288
|
+
assert.deepEqual(readdirSync(outside), []); assert.deepEqual(readFileSync(join(local, 'CURRENT.json')), receipt);
|
|
289
|
+
} finally { if (linked) { unlinkSync(versions); renameSync(kept, versions); } }
|
|
290
|
+
assert.equal(manager.check('test-tool').version, '1.0.0');
|
|
291
|
+
assert.equal(existsSync(join(local, '.update-lock')), false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('migracao recusa links de versions antigo e cache compartilhado sem mover dados', async t => {
|
|
296
|
+
for (const kind of ['legacy-versions', 'cache']) await t.test(kind, async subtest => {
|
|
297
|
+
const { root, manager, state } = fixture(subtest);
|
|
298
|
+
const home = join(root, '.pbai4devs/tools'), legacy = join(home, 'test-tool');
|
|
299
|
+
const outside = join(root, 'outside'); mkdirSync(outside); writeFileSync(join(outside, 'keep.txt'), 'preservado');
|
|
300
|
+
if (kind === 'legacy-versions') {
|
|
301
|
+
writeFileSync(join(legacy, 'CURRENT.json'), JSON.stringify({ name: 'test-tool', version: '1.0.0', platform: platformKey() }));
|
|
302
|
+
if (!createSymlink(subtest, outside, join(legacy, 'versions'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
303
|
+
} else {
|
|
304
|
+
mkdirSync(join(home, 'npm-cache')); writeFileSync(join(home, 'npm-cache/keep.txt'), 'cache antigo');
|
|
305
|
+
if (!createSymlink(subtest, outside, join(home, '.cache'), process.platform === 'win32' ? 'junction' : 'dir')) return;
|
|
306
|
+
}
|
|
307
|
+
await assert.rejects(manager.install('test-tool'), /nao pode ser um link/);
|
|
308
|
+
assert.equal(state.fetches, 0); assert.equal(state.installs, 0);
|
|
309
|
+
assert.deepEqual(readdirSync(outside), ['keep.txt']);
|
|
310
|
+
assert.equal(readFileSync(join(outside, 'keep.txt'), 'utf8'), 'preservado');
|
|
311
|
+
if (kind === 'legacy-versions') assert.ok(existsSync(join(legacy, 'CURRENT.json')));
|
|
312
|
+
else assert.equal(readFileSync(join(home, 'npm-cache/keep.txt'), 'utf8'), 'cache antigo');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
235
316
|
test('duas atualizacoes nao disputam a mesma ferramenta nem apagam lock alheio', async t => {
|
|
236
317
|
const { manager, state, local } = fixture(t); let enter; let proceed;
|
|
237
318
|
const entered = new Promise(resolve => { enter = resolve; });
|
|
@@ -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
|
|