@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/README.es.md CHANGED
@@ -35,7 +35,7 @@ Después, abre el proyecto en tu agente y pide:
35
35
  La [guía de instalación](docs/guides/installation.md) incluye Windows, Node ausente,
36
36
  simulación y actualización. Instala y actualiza mediante el
37
37
  [paquete npm público][packages];
38
- la [release v0.0.6][release] conserva artefactos y checksums para verificarlos.
38
+ la [release v0.0.8][release] conserva artefactos y checksums para verificarlos.
39
39
  No requiere instalación global.
40
40
  La CLI guarda el idioma `pt`, `en` o `es` por proyecto.
41
41
 
@@ -80,7 +80,7 @@ formularios. Consulta [cómo participar](docs/maintainers/community.md).
80
80
  al agente que lea `AGENTS.md`.
81
81
 
82
82
  [clients]: .pbai4devs/COMPATIBILIDADE.md
83
- [release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.6
83
+ [release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
84
84
  [npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
85
85
  [npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
86
86
  [license-badge]: https://img.shields.io/badge/license-MIT-blue
package/README.md CHANGED
@@ -32,7 +32,7 @@ Depois, abra o projeto no seu agente e peça:
32
32
 
33
33
  Windows, instalação sem Node, simulação e atualização estão no
34
34
  [guia de instalação](https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/installation.md). A distribuição usa a
35
- versão publicada no [npm público][packages] e na [release v0.0.6][release],
35
+ versão publicada no [npm público][packages] e na [release v0.0.8][release],
36
36
  sem instalação global. Instalação e atualização usam o pacote do registro;
37
37
  os arquivos da release servem para conferir os artefatos publicados.
38
38
  O idioma da CLI pode ser `pt`, `en` ou `es`.
@@ -89,7 +89,7 @@ ao seu agente para ler `AGENTS.md`.
89
89
  [help]: https://github.com/pathbit/AgentDevKit/blob/master/docs/guides/troubleshooting.md
90
90
  [issues]: https://github.com/pathbit/AgentDevKit/issues/new/choose
91
91
  [packages]: https://github.com/pathbit/AgentDevKit/blob/master/docs/maintainers/packages.md
92
- [release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.6
92
+ [release]: https://github.com/pathbit/AgentDevKit/releases/tag/v0.0.8
93
93
  [npm-package]: https://www.npmjs.com/package/@pathbit/agentdevkit
94
94
  [npm-badge]: https://img.shields.io/npm/v/%40pathbit%2Fagentdevkit
95
95
  [license-badge]: https://img.shields.io/badge/license-MIT-blue
@@ -63,6 +63,15 @@ e `unlock`. Consultar ou executar não instala silenciosamente.
63
63
  `setup` prepara as ferramentas compatíveis; outros comandos podem receber
64
64
  um nome ou grupo. Consulte [ferramentas e testes](tools-and-testing.md).
65
65
 
66
+ Em `tools`, use `--` antes dos argumentos que pertencem à ferramenta:
67
+
68
+ ```console
69
+ npx @pathbit/agentdevkit tools run agent-browser -- --json --help
70
+ ```
71
+
72
+ Nesse exemplo, `--json` pertence ao agent-browser. Opções do kit ficam antes
73
+ do separador. `--help` e `--version` do kit não exigem um projeto instalado.
74
+
66
75
  ## JSON e retorno
67
76
 
68
77
  As respostas incluem `ok`, `command`, `version`, `language`, `target` e
@@ -60,6 +60,13 @@ workflows, formulários de issues ou configurações humanas em `.github`.
60
60
  Sem Git, uma proteção no `.gitignore` da raiz evita incluir os derivados
61
61
  quando o repositório for iniciado.
62
62
 
63
+ O bloco local também protege `.agent-tests`, `tmp`, `temp`, `.playwright*`
64
+ e relatórios de testes, inclusive com letras maiúsculas e em subpastas.
65
+ O `.gitignore` existente e seus arquivos rastreados são preservados;
66
+ uma exceção humana que conflite com essa proteção é informada como conflito.
67
+ Nomes próprios de artefatos e arquivos existentes também são conferidos;
68
+ a validação fica restrita ao projeto e aos ignores de seus ancestrais.
69
+
63
70
  Arquivos já rastreados exigem migração do índice: apenas saídas identificadas
64
71
  do kit podem deixar de ser rastreadas, preservando seus bytes no disco.
65
72
  Revise o plano e os conflitos em um projeto existente; use `--dry-run` antes
@@ -77,10 +84,18 @@ gerenciados alterados geram conflito; use `--force` somente após revisar o
77
84
  backup e o conteúdo que será substituído. Preserve os dados locais, as
78
85
  configurações próprias e os commits do projeto.
79
86
 
87
+ Links simbólicos em caminhos usados pela instalação podem bloquear a operação.
88
+ O kit informa o caminho e não converte nem remove o link automaticamente.
89
+ Use a simulação para identificar esse caso antes de aplicar.
90
+
91
+ Arquivos humanos não se tornam gerenciados apenas por conterem um link.
92
+ As alterações de arquivos existentes preservam permissões, inclusive em backup
93
+ e restauração após falha.
94
+
80
95
  O [fluxo de trabalho](workflow.md) orienta a próxima etapa do projeto.
81
96
  Se houver erro, consulte [solução de problemas](troubleshooting.md).
82
97
 
83
- [Wiki](../README.md) · [Spec v0.0.6][spec]
98
+ [Wiki](../README.md) · [Spec v0.0.8][spec]
84
99
 
85
100
  [bootstrap]: ../../.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md
86
- [spec]: ../specs/v0.0.6/SPEC.md
101
+ [spec]: ../specs/v0.0.8/SPEC.md
@@ -32,6 +32,8 @@ node .pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs setup
32
32
  `setup` reutiliza instalações saudáveis e verifica as compatíveis. Um nome
33
33
  ou grupo limita a seleção. `check`, `info` e `run` não trocam uma consulta
34
34
  por instalação. Downloads, caches e recibos permanecem locais e ignorados.
35
+ O gerenciador recusa links simbólicos nos caminhos que escreve ou usa como
36
+ estado local; a atualização não segue esses links para fora da instalação.
35
37
 
36
38
  ## Escolher a prova
37
39
 
@@ -64,7 +64,7 @@ existir; a primeira publicação pode usar o secret `NPMJS_TOKEN`.
64
64
  Para publicar somente o pacote de uma tag que já tem artefato aprovado:
65
65
 
66
66
  ```console
67
- gh workflow run release.yml --ref master -f tag=v0.0.6 -F package_only=true
67
+ gh workflow run release.yml --ref master -f tag=v0.0.8 -F package_only=true
68
68
  ```
69
69
 
70
70
  Se o artefato expirou, reexecute o CI original da tag antes da publicação.
@@ -17,6 +17,8 @@ status: vigente
17
17
  | Estrutura e exemplos v0.0.4 | CLI, distribuição e gerador | [Spec][layout]. |
18
18
  | Instalação pelo registro v0.0.5 | Pacote publicado | [Spec][registry]. |
19
19
  | Apresentação e licença v0.0.6 | Distribuição e atualização | [Spec][public]. |
20
+ | Compatibilidade real v0.0.7 | Testes em clones existentes | [Spec][oss]. |
21
+ | Preservação v0.0.8 | Auditoria e regressões | [Spec][robustness]. |
20
22
 
21
23
  A primeira entrega segue a [spec v0.0.1](../specs/v0.0.1/SPEC.md), disponível
22
24
  na [release v0.0.1][release]. Os detalhes de mudanças estão no
@@ -36,3 +38,5 @@ comportamento existente, sem alterar a versão de uma release já publicada.
36
38
  [layout]: ../specs/v0.0.4/SPEC.md
37
39
  [registry]: ../specs/v0.0.5/SPEC.md
38
40
  [public]: ../specs/v0.0.6/SPEC.md
41
+ [oss]: ../specs/v0.0.7/SPEC.md
42
+ [robustness]: ../specs/v0.0.8/SPEC.md
@@ -11,6 +11,8 @@ description: Incrementos verificáveis do AgentDevKit.
11
11
  - [v0.0.4](v0.0.4/SPEC.md): estrutura padrão e exemplos na raiz.
12
12
  - [v0.0.5](v0.0.5/SPEC.md): instalação pelo registro em todos os fluxos.
13
13
  - [v0.0.6](v0.0.6/SPEC.md): apresentação pública, licença e atualização segura.
14
+ - [v0.0.7](v0.0.7/SPEC.md): projetos reais e proteção local de artefatos.
15
+ - [v0.0.8](v0.0.8/SPEC.md): preservação, transações e validação aprofundada.
14
16
 
15
17
  Cada incremento vincula requisitos, critérios, testes e evidências locais.
16
18
 
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Compatibilidade do pacote com projetos open source existentes.
3
+ ---
4
+
5
+ # Versão 0.0.7
6
+
7
+ A [spec](SPEC.md) define testes em clones reais e a correção das exclusões
8
+ locais de artefatos, preservando os arquivos e as decisões dos projetos.
9
+
10
+ [Specs](../README.md)
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: Testes em projetos reais e proteção de artefatos locais.
3
+ status: aprovado
4
+ version: 0.0.7
5
+ ---
6
+
7
+ # Compatibilidade com projetos existentes
8
+
9
+ ## Objetivo e autorização
10
+
11
+ Atender ao pedido de testar o pacote publicado em projetos open source com
12
+ conteúdo real, usando clones em `tmp/` ignorado. Corrigir a falta de proteção
13
+ de artefatos revelada em projetos que não tinham as regras do AgentDevKit.
14
+
15
+ O trabalho usa o [PRD](../../product/prd.md),
16
+ o [design](../../product/design.md) e a
17
+ [distribuição pública](../v0.0.6/SPEC.md). Não altera as aplicações clonadas.
18
+
19
+ ## Contrato
20
+
21
+ - Clonar commits fixos de Express, Zod e Chi; preservar os projetos originais.
22
+ - Comparar bytes, tipos, permissões, links, HEAD e alterações staged.
23
+ - Verificar instalação, atualização, repetição, skill própria e conflitos.
24
+ - Proteger artefatos no ignore local sem reescrever o `.gitignore` do usuário
25
+ nem desindexar arquivos humanos. Respeitar projetos irmãos e exceções.
26
+ - Manter as recusas de links simbólicos. Um bloqueio seguro não comprova
27
+ instalação bem-sucedida; relatá-lo e verificar ausência de alterações.
28
+ - Versionar o teste; guardar clones, logs e resultados somente em `tmp/`.
29
+ - Publicar correções como nova versão estável; repetir a prova pública.
30
+
31
+ ## Critérios de aceite
32
+
33
+ | ID | Resultado observável |
34
+ | --- | --- |
35
+ | AC-001 | Matriz real registra resultado e preservação por projeto. |
36
+ | AC-002 | Artefatos são ignorados sem alterar fontes ou projetos irmãos. |
37
+ | AC-003 | Regressões e revisão independente aprovam a correção. |
38
+ | AC-004 | Pacote estável publicado repete os resultados e atualiza o router. |
39
+
40
+ ## Verificação
41
+
42
+ - **AC-001:** executar o runner contra o npm, incluindo caso staged e bloqueios;
43
+ registrar commits, versão e hashes. Não confundir com testes das aplicações.
44
+ - **AC-002:** testar Git, instalação sem Git, projeto aninhado, caixa mista,
45
+ exceções humanas e artefatos já rastreados. Não forçar sobrescritas.
46
+ - **AC-003:** executar testes do pacote, infraestrutura, lint e distribuição.
47
+ Comparar testes nativos do Express antes e depois, sem modificar suas fontes.
48
+ - **AC-004:** conferir release, `latest`, checksum e instalação anônima; repetir
49
+ matriz com o pacote publicado e atualizar o router com backup e diagnóstico.
50
+
51
+ Evidências dos clones ficam em `tmp/oss-compatibility/`; o relatório de revisão
52
+ em `.agent-tests/oss-compatibility/` referencia cópias dessas provas.
53
+ Zod e Chi não terão testes nativos sem os runtimes/gerenciadores exigidos.
54
+ Não concluir compatibilidade universal a partir desta matriz.
55
+
56
+ ## Documentação afetada
57
+
58
+ Atualizar instalação, testes, changelog, índices e roadmap. Documentar a
59
+ proteção de artefatos locais e os limites de configurações preexistentes.
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Validação aprofundada e preservação de dados do AgentDevKit.
3
+ ---
4
+
5
+ # Versão 0.0.8
6
+
7
+ A [spec](SPEC.md) reúne correções da auditoria do instalador, sincronizador,
8
+ gerenciador de ferramentas, CLI e regras de ignore.
9
+
10
+ [Specs](../README.md)
@@ -0,0 +1,62 @@
1
+ ---
2
+ description: Correções verificadas de preservação, transações e interface CLI.
3
+ status: aprovado
4
+ version: 0.0.8
5
+ ---
6
+
7
+ # Preservação e validação aprofundada
8
+
9
+ ## Objetivo e autorização
10
+
11
+ Atender ao pedido de revisão profunda, corrigindo falhas comprovadas e
12
+ publicando uma nova versão estável após validação. Esta etapa continua o
13
+ [PRD](../../product/prd.md), o [design](../../product/design.md) e os
14
+ [testes em projetos reais](../v0.0.7/SPEC.md).
15
+
16
+ ## Contrato
17
+
18
+ - Não apagar ou sobrescrever arquivos humanos pela aparência de seu Markdown.
19
+ Exigir propriedade comprovada para limpeza automática de apontamentos.
20
+ - Recusar conflitos conhecidos antes de escrever. Em falhas posteriores,
21
+ restaurar as alterações próprias sem substituir edições humanas concorrentes.
22
+ - Preservar permissões em atualização, backup e rollback, inclusive em
23
+ arquivos somente para leitura quando a operação for permitida pelo sistema.
24
+ - Recusar componentes simbólicos nos caminhos locais de ferramentas, inclusive
25
+ após operações externas, antes de escrever ou confirmar uma instalação.
26
+ - Preservar integralmente argumentos após `tools ... --`; ajuda e versão
27
+ não exigem diretório de projeto válido.
28
+ - Conferir artefatos existentes e exceções literais além dos nomes de amostra,
29
+ sem ler ignores de projetos irmãos ou desindexar arquivos humanos.
30
+ - Manter a recusa de links simbólicos incompatíveis do Zod; não converter
31
+ esse bloqueio documentado em uma instalação bem-sucedida.
32
+
33
+ ## Critérios de aceite
34
+
35
+ | ID | Resultado observável |
36
+ | --- | --- |
37
+ | AC-001 | Markdown humano é preservado; limpeza usa propriedade comprovada. |
38
+ | AC-002 | Falhas não deixam gravações próprias parciais nem mudam permissões. |
39
+ | AC-003 | Gerenciador não escreve fora da instalação por links simbólicos. |
40
+ | AC-004 | CLI repassa argumentos; ajuda funciona sem projeto. |
41
+ | AC-005 | Ignore protege artefatos sem invadir projetos irmãos. |
42
+ | AC-006 | Regressões, pacote público e atualização do router são verificados. |
43
+
44
+ ## Verificação
45
+
46
+ Para AC-001 a AC-005, conservar reproduções anteriores e testes de regressão
47
+ que falham antes da correção, com assertivas de conteúdo, modo, estado e Git.
48
+ Executar os testes relevantes em Node 24/26 e revisar os diffs de forma
49
+ independente. Limitações de plataforma devem ficar explícitas.
50
+
51
+ Para AC-006, executar pipeline completo, cenários de maturidade e matriz OSS
52
+ do pacote gerado. Confirmar no CI Linux, macOS e Windows; publicar o mesmo TGZ,
53
+ comparar checksum e repetir o consumo público. Atualizar o router com preview,
54
+ backup, diagnóstico e comparação da baseline, preservando o índice existente.
55
+
56
+ Testes e fixtures são versionados. Clones e provas ficam em `tmp/` e o registro
57
+ de revisão em `.agent-tests/`. Não prometer ausência universal de bugs.
58
+
59
+ ## Documentação afetada
60
+
61
+ Atualizar referência da CLI, instalação, ferramentas, changelog, README,
62
+ roadmap e índices. Não criar documentos de produto nos projetos consumidores.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pathbit/agentdevkit",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Shared skills, specifications and workflows for AI coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,7 +50,8 @@
50
50
  "pack:release": "node scripts/release/assets.mjs",
51
51
  "ci:full": "node scripts/release/pipeline.mjs full",
52
52
  "ci:portable": "node scripts/release/pipeline.mjs portable",
53
- "ci:smoke": "node scripts/release/pipeline.mjs smoke"
53
+ "ci:smoke": "node scripts/release/pipeline.mjs smoke",
54
+ "test:oss": "node tests/oss-compatibility.mjs"
54
55
  },
55
56
  "author": {
56
57
  "name": "PathBit",
package/src/cli.mjs CHANGED
@@ -18,7 +18,10 @@ export function parseArgs(argv) {
18
18
  const options = { command: null, positionals: [], toolArgs: [], target: null, language: null, clients: undefined, yes: false, json: false, dryRun: false, force: false, check: false };
19
19
  for (let index = 0; index < argv.length; index++) {
20
20
  const arg = argv[index];
21
- if (['--target', '--lang', '--agents', '--clients'].includes(arg)) {
21
+ if (arg === '--' && options.command === 'tools') {
22
+ options.toolArgs.push(...argv.slice(index));
23
+ break;
24
+ } else if (['--target', '--lang', '--agents', '--clients'].includes(arg)) {
22
25
  const value = argv[++index]; if (!value || value.startsWith('-')) usage(`Missing value: ${arg}`);
23
26
  if (arg === '--target') options.target = value;
24
27
  else if (arg === '--lang') { options.language = normalizeLanguage(value); if (!options.language) usage('Use --lang pt, en or es.'); }
@@ -86,11 +89,14 @@ function print(result, options, output) {
86
89
 
87
90
  export async function main(argv = process.argv.slice(2), context = {}) {
88
91
  const output = context.stdout || process.stdout, input = context.stdin || process.stdin, errorOutput = context.stderr || process.stderr;
89
- let options = { json: argv.includes('--json') }, language = normalizeLanguage(argv[argv.indexOf('--lang') + 1]) || 'pt', target = resolve(context.cwd || process.cwd()), clients = ['all'];
92
+ const separator = argv.indexOf('--'), globalArgs = separator < 0 ? argv : argv.slice(0, separator);
93
+ let options = { json: globalArgs.includes('--json') }, language = normalizeLanguage(globalArgs[globalArgs.indexOf('--lang') + 1]) || 'pt', target = resolve(context.cwd || process.cwd()), clients = ['all'];
90
94
  try {
91
95
  options = parseArgs(argv);
92
- target = resolveTarget(options.target || (options.command === 'init' ? options.positionals[0] : '.'), context.cwd || process.cwd());
93
- const previous = ['help', 'version'].includes(options.command) ? null : loadState(target);
96
+ const informational = ['help', 'version'].includes(options.command);
97
+ target = informational ? resolve(context.cwd || process.cwd(), options.target || '.')
98
+ : resolveTarget(options.target || (options.command === 'init' ? options.positionals[0] : '.'), context.cwd || process.cwd());
99
+ const previous = informational ? null : loadState(target);
94
100
  language = options.language || previous?.language || 'pt';
95
101
  clients = options.clients ?? previous?.clients ?? ['all'];
96
102
  if (options.command === 'init' && !previous && !options.yes && !options.json && !options.dryRun && input.isTTY && output.isTTY && (!options.language || options.clients === undefined)) {
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
  }