@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/src/install.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
2
+ import { chmodSync, existsSync, lstatSync, readFileSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
@@ -77,8 +77,24 @@ function walk(root, path = '', filter = () => true) {
77
77
  return files;
78
78
  }
79
79
 
80
- function write(root, path, content) {
81
- const target = safePath(root, path); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, content);
80
+ function write(root, path, content, mode) {
81
+ const target = safePath(root, path); mkdirSync(dirname(target), { recursive: true });
82
+ writeFileSync(target, content, mode === undefined ? undefined : { mode });
83
+ if (mode !== undefined) chmodSync(target, mode);
84
+ }
85
+
86
+ function atomicWrite(root, path, content, mode, onPublished) {
87
+ const file = safePath(root, path);
88
+ mkdirSync(dirname(file), { recursive: true });
89
+ const temporary = `${file}.agentdevkit-${randomUUID()}`;
90
+ try {
91
+ writeFileSync(temporary, content, { flag: 'wx', ...(mode === undefined ? {} : { mode }) });
92
+ if (mode !== undefined) chmodSync(temporary, mode);
93
+ const writtenMode = stat(temporary).mode & 0o777;
94
+ renameSync(temporary, file);
95
+ onPublished?.(writtenMode);
96
+ return writtenMode;
97
+ } finally { rmSync(temporary, { force: true }); }
82
98
  }
83
99
 
84
100
  function read(root, path) {
@@ -294,42 +310,63 @@ export function applyPlan(plan) {
294
310
  safePath(target, backup);
295
311
  for (const change of plan.changes) safePath(target, change.path);
296
312
  const before = new Map(plan.changes.map(change => [change.path, read(target, change.path)]));
313
+ const beforeModes = new Map(plan.changes.map(change => {
314
+ const info = stat(safePath(target, change.path));
315
+ return [change.path, info ? info.mode & 0o777 : undefined];
316
+ }));
297
317
  for (const change of plan.changes) if ((before.get(change.path) ? hash(before.get(change.path)) : null) !== change.beforeHash) fail('E_CHANGED', `File changed after planning: ${change.path}`);
298
- for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content);
299
- const applied = [];
318
+ for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content, beforeModes.get(path));
319
+ const applied = [], removedDirectories = [];
300
320
  let untracked = false;
301
321
  try {
302
322
  if (plan.untrack?.paths.length) { applyUntrack(plan.untrack); untracked = true; }
303
323
  for (const change of plan.changes) {
304
324
  const file = safePath(target, change.path);
305
- if (change.content === null) rmSync(file, { force: true });
306
- else {
307
- mkdirSync(dirname(file), { recursive: true });
308
- const temporary = `${file}.agentdevkit-${randomUUID()}`;
309
- try { writeFileSync(temporary, change.content, { flag: 'wx' }); renameSync(temporary, file); }
310
- finally { rmSync(temporary, { force: true }); }
311
- }
312
- applied.push(change.path);
325
+ const content = change.content;
326
+ const signature = { path: change.path, hash: content === null ? null : hash(content) };
327
+ if (content === null) { rmSync(file, { force: true }); applied.push(signature); }
328
+ else atomicWrite(target, change.path, content, beforeModes.get(change.path), mode => {
329
+ signature.mode = mode; applied.push(signature);
330
+ });
313
331
  }
314
- pruneRemovedDirectories(plan);
332
+ pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
315
333
  const changesFallback = plan.changes.some(change => change.path === '.gitignore');
316
334
  // Metadata is committed last; its writer rolls back partial writes itself.
317
335
  const ignored = applyLocalIgnore(changesFallback ? planLocalIgnore(target, { outputs: plan.ownedOutputs }) : plan.localIgnore);
318
336
  return { changed: Boolean(plan.changes.length || ignored.changed || untracked), backup: [...before.values()].some(Boolean) ? backup : null };
319
337
  } catch (error) {
320
- const rollbackErrors = [];
321
- for (const path of applied.reverse()) {
338
+ const rollbackErrors = [], preserved = [];
339
+ for (const removed of [...removedDirectories].sort((a, b) => a.path.length - b.path.length)) {
340
+ let directory, current;
341
+ try { directory = safePath(target, removed.path); current = stat(directory); }
342
+ catch { preserved.push(removed.path); continue; }
343
+ if (current) continue;
344
+ try { mkdirSync(directory, { mode: removed.mode }); chmodSync(directory, removed.mode); }
345
+ catch (failure) { rollbackErrors.push(failure); }
346
+ }
347
+ for (const appliedFile of applied.reverse()) {
348
+ const { path } = appliedFile;
349
+ let unchanged = false;
350
+ try {
351
+ const file = safePath(target, path), info = stat(file);
352
+ unchanged = appliedFile.hash === null ? !info : Boolean(info?.isFile()
353
+ && (info.mode & 0o777) === appliedFile.mode && hash(readFileSync(file)) === appliedFile.hash);
354
+ } catch { /* Não restaurar quando não é possível comprovar a própria gravação. */ }
355
+ if (!unchanged) { preserved.push(path); continue; }
322
356
  const content = before.get(path);
323
- try { if (content) write(target, path, content); else rmSync(safePath(target, path), { force: true }); }
357
+ try { if (content) atomicWrite(target, path, content, beforeModes.get(path)); else rmSync(safePath(target, path), { force: true }); }
324
358
  catch (failure) { rollbackErrors.push(failure); }
325
359
  }
360
+ if (preserved.length) {
361
+ try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
362
+ }
326
363
  if (untracked) try { restoreUntrack(plan.untrack); } catch (failure) { rollbackErrors.push(failure); }
327
364
  if (rollbackErrors.length) throw Object.assign(new AggregateError([error, ...rollbackErrors], `${error.message}; rollback incompleto: ${rollbackErrors.map(failure => failure.message).join('; ')}`, { cause: error }), { code: error.code || 'E_OPERATION' });
328
365
  throw error;
329
366
  }
330
367
  }
331
368
 
332
- function pruneRemovedDirectories(plan) {
369
+ function pruneRemovedDirectories(plan, onRemoved) {
333
370
  const { target } = plan;
334
371
  const legacy = '.pbai4devs/adapters/README.md';
335
372
  const removed = plan.changes.filter(change => change.action === 'remove' && (allowedManaged(change.path) || change.path === legacy));
@@ -342,8 +379,9 @@ function pruneRemovedDirectories(plan) {
342
379
  const boundary = parent.startsWith('.pbai4devs/') ? '.pbai4devs' : adapterRoots.find(root => parent.startsWith(`${root}/`));
343
380
  while (boundary && parent.startsWith(`${boundary}/`)) {
344
381
  const directory = safePath(target, parent);
345
- if (!stat(directory)?.isDirectory() || readdirSync(directory).length) break;
346
- try { rmdirSync(directory); }
382
+ const info = stat(directory);
383
+ if (!info?.isDirectory() || readdirSync(directory).length) break;
384
+ try { rmdirSync(directory); onRemoved?.({ path: parent, mode: info.mode & 0o777 }); }
347
385
  catch (error) { if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) break; throw error; }
348
386
  parent = portable(dirname(parent));
349
387
  }
@@ -351,8 +389,9 @@ function pruneRemovedDirectories(plan) {
351
389
  for (const root of plan.retiredRoots || []) {
352
390
  if (!adapterRoots.includes(root)) continue;
353
391
  const directory = safePath(target, root);
354
- if (!stat(directory)?.isDirectory()) continue;
355
- try { if (readdirSync(directory).length === 0) rmdirSync(directory); }
392
+ const info = stat(directory);
393
+ if (!info?.isDirectory()) continue;
394
+ try { if (readdirSync(directory).length === 0) { rmdirSync(directory); onRemoved?.({ path: root, mode: info.mode & 0o777 }); } }
356
395
  catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error; }
357
396
  }
358
397
  }