@pathbit/agentdevkit 0.0.7 → 0.0.9
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/COMPATIBILIDADE.md +4 -2
- package/.pbai4devs/INSTRUCTIONS.md +4 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
- package/.pbai4devs/rules/documentacao/RULE.md +4 -0
- package/.pbai4devs/rules/qualidade/RULE.md +4 -2
- package/.pbai4devs/scripts/README.md +3 -3
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
- package/.pbai4devs/scripts/lib/gitignore.mjs +84 -15
- package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +41 -20
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +81 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +193 -49
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +151 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +145 -1
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +30 -29
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
- package/.pbai4devs/templates/README.md +4 -1
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
- package/CHANGELOG.md +19 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +11 -0
- package/docs/guides/documentation.md +63 -0
- package/docs/guides/installation.md +18 -5
- package/docs/guides/resources.md +6 -5
- package/docs/guides/tools-and-testing.md +2 -0
- package/docs/guides/troubleshooting.md +6 -6
- package/docs/guides/workflow.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/design.md +12 -0
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +4 -0
- package/docs/specs/README.md +2 -0
- package/docs/specs/v0.0.8/README.md +10 -0
- package/docs/specs/v0.0.8/SPEC.md +62 -0
- package/docs/specs/v0.0.9/README.md +9 -0
- package/docs/specs/v0.0.9/SPEC.md +68 -0
- package/package.json +3 -2
- package/src/cli.mjs +13 -4
- package/src/i18n.mjs +3 -0
- package/src/install.mjs +145 -38
package/docs/product/prd.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Requisitos da distribuição portátil AgentDevKit.
|
|
3
3
|
status: aprovado
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.9
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# PRD do AgentDevKit
|
|
@@ -40,6 +40,9 @@ extraído do projeto de origem como produto independente.
|
|
|
40
40
|
| FR-018 | Publicar autoria PathBit, palavras-chave e licença MIT no npm. |
|
|
41
41
|
| FR-019 | Exibir README legível no npm sem alterar o Markdown fonte. |
|
|
42
42
|
| FR-020 | Levar a licença do kit ao hub sem alterar a licença do projeto. |
|
|
43
|
+
| FR-021 | Inventariar documentos existentes antes de propor alterações. |
|
|
44
|
+
| FR-022 | Complementar documentação sem substituir arquivos por templates. |
|
|
45
|
+
| FR-023 | Provar o fluxo documental em projetos reais e em projeto vazio. |
|
|
43
46
|
|
|
44
47
|
Execuções repetidas devem ser idempotentes; a CLI oferece simulação e JSON.
|
|
45
48
|
As instruções permanecem concisas, com detalhes opcionais em referências.
|
package/docs/product/roadmap.md
CHANGED
|
@@ -18,6 +18,8 @@ status: vigente
|
|
|
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
20
|
| Compatibilidade real v0.0.7 | Testes em clones existentes | [Spec][oss]. |
|
|
21
|
+
| Preservação v0.0.8 | Auditoria e regressões | [Spec][robustness]. |
|
|
22
|
+
| Coexistência v0.0.9 | Agentes, documentos e projetos reais | [Spec][coexistence]. |
|
|
21
23
|
|
|
22
24
|
A primeira entrega segue a [spec v0.0.1](../specs/v0.0.1/SPEC.md), disponível
|
|
23
25
|
na [release v0.0.1][release]. Os detalhes de mudanças estão no
|
|
@@ -38,3 +40,5 @@ comportamento existente, sem alterar a versão de uma release já publicada.
|
|
|
38
40
|
[registry]: ../specs/v0.0.5/SPEC.md
|
|
39
41
|
[public]: ../specs/v0.0.6/SPEC.md
|
|
40
42
|
[oss]: ../specs/v0.0.7/SPEC.md
|
|
43
|
+
[robustness]: ../specs/v0.0.8/SPEC.md
|
|
44
|
+
[coexistence]: ../specs/v0.0.9/SPEC.md
|
package/docs/specs/README.md
CHANGED
|
@@ -12,6 +12,8 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
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
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.
|
|
16
|
+
- [v0.0.9](v0.0.9/SPEC.md): coexistência e evolução documental sem perdas.
|
|
15
17
|
|
|
16
18
|
Cada incremento vincula requisitos, critérios, testes e evidências locais.
|
|
17
19
|
|
|
@@ -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.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Coexistência com agentes e evolução da documentação existente.
|
|
3
|
+
status: aprovado
|
|
4
|
+
version: 0.0.9
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Preservar agentes e documentação existentes
|
|
8
|
+
|
|
9
|
+
## Objetivo
|
|
10
|
+
|
|
11
|
+
Instalar e atualizar o AgentDevKit no Zod e em projetos com agentes próprios,
|
|
12
|
+
preservando configurações, arquivos, permissões, links e estado do Git.
|
|
13
|
+
Orientar também a evolução documental a partir do inventário e da leitura
|
|
14
|
+
dos materiais existentes, sem reiniciar o projeto. O pedido atual substitui
|
|
15
|
+
o bloqueio conservador documentado na
|
|
16
|
+
[versão anterior](../v0.0.8/SPEC.md).
|
|
17
|
+
|
|
18
|
+
## Contrato
|
|
19
|
+
|
|
20
|
+
- Tratar links simbólicos humanos nas entradas e nos clientes como caminhos
|
|
21
|
+
preservados: não abrir, atravessar, converter, sobrescrever ou adotar.
|
|
22
|
+
- Preservar agentes e skills humanos, inclusive nomes iguais aos do kit e
|
|
23
|
+
durante execução com `--force`. Informar os caminhos preservados.
|
|
24
|
+
- Gerar os apontamentos disponíveis sem atribuir ao kit arquivos preservados.
|
|
25
|
+
No Zod, manter `.codex/skills` e usar também `.agents/skills` para o Codex.
|
|
26
|
+
- Manter a validação estrita em fontes, estado, ferramentas, Git e caminhos
|
|
27
|
+
antes pertencentes ao kit. Um link não pode esconder alteração gerenciada.
|
|
28
|
+
- Manter os mesmos contratos na CLI e no sincronizador instalado.
|
|
29
|
+
- Antes de editar documentos, inventariar e ler os existentes. Reaproveitar
|
|
30
|
+
caminhos, seções e decisões; templates orientam somente lacunas.
|
|
31
|
+
- Organizar por índices e links, sem apagar, mover ou substituir documentos.
|
|
32
|
+
Em projetos novos, não inventar produto ou aprovação sem respostas.
|
|
33
|
+
|
|
34
|
+
## Critérios de aceite
|
|
35
|
+
|
|
36
|
+
| ID | Resultado observável |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| AC-001 | Links humanos internos, externos e quebrados permanecem intactos. |
|
|
39
|
+
| AC-002 | Skills, agentes e configurações preexistentes são preservados. |
|
|
40
|
+
| AC-003 | Caminhos gerenciados recusam links sem escrita externa. |
|
|
41
|
+
| AC-004 | Init, update, doctor e sync são coerentes e idempotentes. |
|
|
42
|
+
| AC-005 | Zod, Express e Chi passam na matriz do pacote, preservando Git. |
|
|
43
|
+
| AC-006 | Regressões, documentação, CI e pacote público são verificados. |
|
|
44
|
+
| AC-007 | Inventário detecta documentação real sem alterar arquivos. |
|
|
45
|
+
| AC-008 | Skills exigem leitura e plano aditivo antes dos templates. |
|
|
46
|
+
| AC-009 | Exercícios documentais preservam conteúdo e arquivos fora do plano. |
|
|
47
|
+
| AC-010 | Projeto vazio recebe o kit sem documentos de produto inventados. |
|
|
48
|
+
|
|
49
|
+
## Validação
|
|
50
|
+
|
|
51
|
+
Executar testes de regressão para links e colisões, inclusive `--force`,
|
|
52
|
+
substituição de caminhos após o plano e mudança de seleção dos clientes.
|
|
53
|
+
Rodar pipeline completo, maturidade e matriz OSS com o TGZ candidato;
|
|
54
|
+
repetir a matriz com o pacote publicado. Comparar bytes, tipos, modos,
|
|
55
|
+
destinos de links, staging e configuração Git nos clones.
|
|
56
|
+
Execute também o inventário instalado e o fluxo de documentação em clones
|
|
57
|
+
reais: plano por arquivo, complemento baseado em fatos, índice novo e revisão.
|
|
58
|
+
Separe essa prova da simples instalação e de fixtures do harness; não atribua
|
|
59
|
+
ao pacote geração automática de conteúdo ou aprovações de produto.
|
|
60
|
+
|
|
61
|
+
Clones e evidências ficam ignorados em `tmp/` e `.agent-tests/`.
|
|
62
|
+
O escopo é o kit e seu fluxo documental; não afirmar execução das aplicações
|
|
63
|
+
clonadas.
|
|
64
|
+
|
|
65
|
+
## Documentação afetada
|
|
66
|
+
|
|
67
|
+
Atualizar instalação, CLI, recuperação, recursos, sincronização, testes,
|
|
68
|
+
changelog e índices. Preservar as specs antigas como histórico.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pathbit/agentdevkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Shared skills, specifications and workflows for AI coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"ci:full": "node scripts/release/pipeline.mjs full",
|
|
52
52
|
"ci:portable": "node scripts/release/pipeline.mjs portable",
|
|
53
53
|
"ci:smoke": "node scripts/release/pipeline.mjs smoke",
|
|
54
|
-
"test:oss": "node tests/oss-compatibility.mjs"
|
|
54
|
+
"test:oss": "node tests/oss-compatibility.mjs",
|
|
55
|
+
"test:docs": "node tests/documentation-compatibility.mjs"
|
|
55
56
|
},
|
|
56
57
|
"author": {
|
|
57
58
|
"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 (
|
|
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.'); }
|
|
@@ -59,6 +62,7 @@ function print(result, options, output) {
|
|
|
59
62
|
if (result.clients) output.write(`${text(result.language, 'clientsCurrent')}: ${result.clients.join(', ')}\n`);
|
|
60
63
|
if (result.availableClients) output.write(`${text(result.language, 'clientsAvailable')}: ${result.availableClients.map(client => client.id).join(', ')}\n`);
|
|
61
64
|
if (result.toolOutput) output.write(`${result.toolOutput.trimEnd()}\n`);
|
|
65
|
+
if (result.preserved?.length) output.write(`${text(result.language, 'preserved')}: ${result.preserved.length}. ${text(result.language, 'preservedDetails')}\n`);
|
|
62
66
|
if (result.plan?.length) {
|
|
63
67
|
const names = { pt: { create: 'criar', update: 'atualizar', remove: 'remover' }, en: { create: 'create', update: 'update', remove: 'remove' }, es: { create: 'crear', update: 'actualizar', remove: 'eliminar' } };
|
|
64
68
|
const labels = {
|
|
@@ -86,11 +90,14 @@ function print(result, options, output) {
|
|
|
86
90
|
|
|
87
91
|
export async function main(argv = process.argv.slice(2), context = {}) {
|
|
88
92
|
const output = context.stdout || process.stdout, input = context.stdin || process.stdin, errorOutput = context.stderr || process.stderr;
|
|
89
|
-
|
|
93
|
+
const separator = argv.indexOf('--'), globalArgs = separator < 0 ? argv : argv.slice(0, separator);
|
|
94
|
+
let options = { json: globalArgs.includes('--json') }, language = normalizeLanguage(globalArgs[globalArgs.indexOf('--lang') + 1]) || 'pt', target = resolve(context.cwd || process.cwd()), clients = ['all'];
|
|
90
95
|
try {
|
|
91
96
|
options = parseArgs(argv);
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
const informational = ['help', 'version'].includes(options.command);
|
|
98
|
+
target = informational ? resolve(context.cwd || process.cwd(), options.target || '.')
|
|
99
|
+
: resolveTarget(options.target || (options.command === 'init' ? options.positionals[0] : '.'), context.cwd || process.cwd());
|
|
100
|
+
const previous = informational ? null : loadState(target);
|
|
94
101
|
language = options.language || previous?.language || 'pt';
|
|
95
102
|
clients = options.clients ?? previous?.clients ?? ['all'];
|
|
96
103
|
if (options.command === 'init' && !previous && !options.yes && !options.json && !options.dryRun && input.isTTY && output.isTTY && (!options.language || options.clients === undefined)) {
|
|
@@ -117,6 +124,7 @@ export async function main(argv = process.argv.slice(2), context = {}) {
|
|
|
117
124
|
const found = inspect(target);
|
|
118
125
|
if (found.installed && !found.issues.length) {
|
|
119
126
|
const plan = planInstallation({ target, language, mode: 'sync' });
|
|
127
|
+
result.preserved = plan.preserved;
|
|
120
128
|
found.issues.push(...plan.changes.map(change => ({ code: 'outdated', path: change.path })));
|
|
121
129
|
found.issues.push(...plan.localIgnoreCheck.missing.map(path => ({ code: 'unignored', path })));
|
|
122
130
|
if (plan.localIgnore.edits.length || plan.untrack?.paths.length) found.issues.push({ code: 'local-ignore', path: '.gitignore / Git info/exclude' });
|
|
@@ -138,6 +146,7 @@ export async function main(argv = process.argv.slice(2), context = {}) {
|
|
|
138
146
|
result.next = getNextSteps({ command: options.command, language, target, installed: Boolean(previous) });
|
|
139
147
|
}
|
|
140
148
|
const plan = planInstallation({ target, language, mode: options.command, force: options.force, clients: options.clients });
|
|
149
|
+
result.preserved = plan.preserved;
|
|
141
150
|
result.plan = plan.changes.map(({ path, action }) => ({ path, action, conflict: plan.conflicts.includes(path) }));
|
|
142
151
|
result.untrackedPaths = [...(plan.untrack?.paths || [])];
|
|
143
152
|
result.localIgnore = { status: plan.localIgnore.status, edits: plan.localIgnore.edits.map(({ path, before }) => ({ path, action: before === null ? 'create' : 'update' })), missing: plan.localIgnoreCheck.missing };
|
package/src/i18n.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export function normalizeLanguage(value) {
|
|
|
7
7
|
|
|
8
8
|
const messages = {
|
|
9
9
|
pt: {
|
|
10
|
+
preserved: 'Caminhos existentes preservados', preservedDetails: 'Use --json para consultar os caminhos e motivos.',
|
|
10
11
|
help: 'AgentDevKit: init [diretório], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <ação>.\nOpções: --target <diretório>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAliases: --clients e clients.',
|
|
11
12
|
clientsCurrent: 'Agentes selecionados', clientsAvailable: 'Agentes disponíveis', clientsSelect: 'Agentes (IDs separados por vírgula ou all) [all]: ', agents: 'Seleção de agentes atualizada.',
|
|
12
13
|
next: 'Próximo passo', init: 'Estrutura instalada.', update: 'Estrutura atualizada.', sync: 'Apontamentos sincronizados.', lang: 'Idioma configurado.', unchanged: 'Estrutura já atualizada.', plan: 'Plano calculado; nenhum arquivo alterado.', conflict: 'Há arquivos próprios ou alterados em conflito. Revise o plano; --force salva backup antes de substituir.',
|
|
@@ -15,6 +16,7 @@ const messages = {
|
|
|
15
16
|
agentLanguage: 'Responda em português, salvo quando o usuário solicitar outro idioma.', inspect: 'Verifique a instalação', workflow: 'Consulte a próxima etapa', install: 'Inicialize este projeto', repair: 'Confira a sincronização', review: 'Revise os arquivos em conflito', tools: 'Resultado da ferramenta',
|
|
16
17
|
},
|
|
17
18
|
en: {
|
|
19
|
+
preserved: 'Existing paths preserved', preservedDetails: 'Use --json to inspect paths and reasons.',
|
|
18
20
|
help: 'AgentDevKit: init [directory], update, doctor, sync [--check], lang [pt|en|es], agents [list], next, tools <action>.\nOptions: --target <directory>, --lang <language>, --agents <list>, --yes, --dry-run, --force, --json.\nAliases: --clients and clients.',
|
|
19
21
|
clientsCurrent: 'Selected agents', clientsAvailable: 'Available agents', clientsSelect: 'Agents (comma-separated IDs or all) [all]: ', agents: 'Agent selection updated.',
|
|
20
22
|
next: 'Next step', init: 'Structure installed.', update: 'Structure updated.', sync: 'Pointers synchronized.', lang: 'Language configured.', unchanged: 'Structure is already current.', plan: 'Plan calculated; no files changed.', conflict: 'User files or edits conflict with this plan. Review it; --force backs them up before replacement.',
|
|
@@ -23,6 +25,7 @@ const messages = {
|
|
|
23
25
|
agentLanguage: 'Respond in English unless the user requests another language.', inspect: 'Check the installation', workflow: 'Review the next stage', install: 'Initialize this project', repair: 'Check synchronization', review: 'Review conflicting files', tools: 'Tool result',
|
|
24
26
|
},
|
|
25
27
|
es: {
|
|
28
|
+
preserved: 'Rutas existentes preservadas', preservedDetails: 'Use --json para consultar las rutas y los motivos.',
|
|
26
29
|
help: 'AgentDevKit: init [directorio], update, doctor, sync [--check], lang [pt|en|es], agents [lista], next, tools <acción>.\nOpciones: --target <directorio>, --lang <idioma>, --agents <lista>, --yes, --dry-run, --force, --json.\nAlias: --clients y clients.',
|
|
27
30
|
clientsCurrent: 'Agentes seleccionados', clientsAvailable: 'Agentes disponibles', clientsSelect: 'Agentes (IDs separados por coma o all) [all]: ', agents: 'Selección de agentes actualizada.',
|
|
28
31
|
next: 'Próximo paso', init: 'Estructura instalada.', update: 'Estructura actualizada.', sync: 'Referencias sincronizadas.', lang: 'Idioma configurado.', unchanged: 'La estructura ya está actualizada.', plan: 'Plan calculado; ningún archivo modificado.', conflict: 'Hay archivos propios o cambios en conflicto. Revise el plan; --force guarda una copia antes de reemplazarlos.',
|
package/src/install.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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';
|
|
6
6
|
import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, planUntrack, applyUntrack, restoreUntrack } from '../.pbai4devs/scripts/lib/gitignore.mjs';
|
|
7
|
+
import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
|
|
7
8
|
import { writeAdapterManifest, adapterRoots, normalizeClients, resolveAdapters } from './adapters/index.mjs';
|
|
8
9
|
import { packageRoot, packageInfo, statePath, entryNames, begin, end } from './config.mjs';
|
|
9
10
|
import { normalizeLanguage, text } from './i18n.mjs';
|
|
@@ -77,8 +78,24 @@ function walk(root, path = '', filter = () => true) {
|
|
|
77
78
|
return files;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function write(root, path, content) {
|
|
81
|
-
const target = safePath(root, path); mkdirSync(dirname(target), { recursive: true });
|
|
81
|
+
function write(root, path, content, mode) {
|
|
82
|
+
const target = safePath(root, path); mkdirSync(dirname(target), { recursive: true });
|
|
83
|
+
writeFileSync(target, content, mode === undefined ? undefined : { mode });
|
|
84
|
+
if (mode !== undefined) chmodSync(target, mode);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function atomicWrite(root, path, content, mode, onPublished) {
|
|
88
|
+
const file = safePath(root, path);
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
const temporary = `${file}.agentdevkit-${randomUUID()}`;
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(temporary, content, { flag: 'wx', ...(mode === undefined ? {} : { mode }) });
|
|
93
|
+
if (mode !== undefined) chmodSync(temporary, mode);
|
|
94
|
+
const writtenMode = stat(temporary).mode & 0o777;
|
|
95
|
+
renameSync(temporary, file);
|
|
96
|
+
onPublished?.(writtenMode);
|
|
97
|
+
return writtenMode;
|
|
98
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
function read(root, path) {
|
|
@@ -157,6 +174,24 @@ function copyResources(source, destination) {
|
|
|
157
174
|
return files;
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
function readDerivedState(target) {
|
|
178
|
+
let entries = {}, outputs = {};
|
|
179
|
+
const derived = read(target, '.pbai4devs/estado/derived.json');
|
|
180
|
+
if (derived) {
|
|
181
|
+
try {
|
|
182
|
+
const value = JSON.parse(derived);
|
|
183
|
+
if (value.schemaVersion !== 1 || !value.entries || typeof value.entries !== 'object' || Array.isArray(value.entries)
|
|
184
|
+
|| value.outputs !== undefined && (!value.outputs || typeof value.outputs !== 'object' || Array.isArray(value.outputs))) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
185
|
+
entries = value.entries; outputs = value.outputs ?? {};
|
|
186
|
+
for (const [path, digest] of Object.entries(entries)) if (!entryNames.includes(path) || !/^[a-f0-9]{64}$/.test(digest)) fail('E_STATE', 'Invalid derived entrypoint state.');
|
|
187
|
+
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.');
|
|
188
|
+
}
|
|
189
|
+
catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
|
|
190
|
+
for (const path of [...Object.keys(entries), ...Object.keys(outputs)]) safePath(target, path);
|
|
191
|
+
}
|
|
192
|
+
return { entries, outputs };
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
export function planInstallation({ target, language = 'pt', mode = 'init', force = false, clients }) {
|
|
161
196
|
const previous = loadState(target);
|
|
162
197
|
if (mode !== 'init' && !previous) fail('E_MISSING', text(language, 'missing'));
|
|
@@ -164,16 +199,37 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
164
199
|
const active = resolveAdapters(selected);
|
|
165
200
|
const changingClients = clients !== undefined || mode === 'agents';
|
|
166
201
|
const protectedEntries = new Set(), entryConflicts = [], ownedEntrypoints = new Set();
|
|
202
|
+
const { entries: derivedEntries, outputs: derivedOutputs } = readDerivedState(target);
|
|
203
|
+
const ownedPaths = [...Object.keys(previous?.files || {}), ...Object.keys(previous?.blocks || {}), ...Object.keys(derivedEntries), ...Object.keys(derivedOutputs)];
|
|
204
|
+
const preserved = new Map();
|
|
205
|
+
const recordPreserved = (path, reason) => { preserved.set(path, { path, reason }); return true; };
|
|
206
|
+
const native = path => entryNames.includes(path) || path === 'README.md' || adapterRoots.some(root => path === root || path.startsWith(`${root}/`));
|
|
207
|
+
const boundary = path => {
|
|
208
|
+
if (!native(path)) return false;
|
|
209
|
+
const found = nativePathBoundary(target, path, ownedPaths, { preserveRootAliases: path === 'README.md' || entryNames.includes(path) });
|
|
210
|
+
return found ? recordPreserved(found.path, found.reason) : false;
|
|
211
|
+
};
|
|
212
|
+
const owned = path => ownedPaths.includes(path);
|
|
213
|
+
const generatedReference = content => Boolean(content && isReference(content.toString('utf8'))
|
|
214
|
+
&& (content.toString('utf8').split(/\r?\n/).includes('<!-- gerado: sincronizar-agentes -->') || content.toString('utf8').startsWith('# gerado: sincronizar-agentes\n')));
|
|
215
|
+
const preserveNative = path => {
|
|
216
|
+
if (!native(path)) return false;
|
|
217
|
+
if (boundary(path)) return true;
|
|
218
|
+
const item = stat(safePath(target, path));
|
|
219
|
+
if (!item || owned(path)) return false;
|
|
220
|
+
if (!item.isFile() && ownedPaths.some(value => value.startsWith(`${path}/`))) fail('E_PATH', `Managed subtree occupies a file destination: ${path}`);
|
|
221
|
+
if (item.isFile()) {
|
|
222
|
+
const current = read(target, path);
|
|
223
|
+
if (generatedReference(current)) return false;
|
|
224
|
+
if (path.endsWith('/.gitignore') && current.toString('utf8').split(/\r?\n/).some(line => ['# BEGIN AGENT IGNORE', '# END AGENT IGNORE'].includes(line))) return false;
|
|
225
|
+
}
|
|
226
|
+
return recordPreserved(path, item.isFile() ? 'existing-file' : 'existing-directory');
|
|
227
|
+
};
|
|
167
228
|
for (const name of entryNames) {
|
|
229
|
+
if (preserveNative(name)) continue;
|
|
168
230
|
const current = read(target, name);
|
|
169
231
|
if (current && previous?.files[name] && previous.files[name] !== hash(current)) { protectedEntries.add(name); entryConflicts.push(name); }
|
|
170
232
|
}
|
|
171
|
-
let derivedEntries = {};
|
|
172
|
-
const derived = read(target, '.pbai4devs/estado/derived.json');
|
|
173
|
-
if (derived) {
|
|
174
|
-
try { const value = JSON.parse(derived); if (value.schemaVersion === 1 && value.entries && typeof value.entries === 'object') derivedEntries = value.entries; }
|
|
175
|
-
catch { fail('E_STATE', 'Invalid derived entrypoint state.'); }
|
|
176
|
-
}
|
|
177
233
|
const stage = realpathSync(mkdtempSync(join(tmpdir(), 'agentdevkit-')));
|
|
178
234
|
try {
|
|
179
235
|
const existing = copyResources(join(target, '.pbai4devs'), join(stage, '.pbai4devs'));
|
|
@@ -190,6 +246,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
190
246
|
if (path.startsWith('.pbai4devs/') && !supplied.has(path.slice(11)) && !internal.has(path.slice(11))) rmSync(safePath(stage, path), { force: true });
|
|
191
247
|
}
|
|
192
248
|
for (const root of active.roots) {
|
|
249
|
+
if (preserveNative(`${root}/.gitignore`)) continue;
|
|
193
250
|
const content = read(target, `${root}/.gitignore`);
|
|
194
251
|
if (content) write(stage, `${root}/.gitignore`, content);
|
|
195
252
|
}
|
|
@@ -198,7 +255,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
198
255
|
writeAdapterManifest(stage, { clients: selected });
|
|
199
256
|
const synchronized = synchronize({ root: stage, force: true, localIgnores: false, generateEntrypoints: false, output() {} });
|
|
200
257
|
for (const name of active.entrypoints) {
|
|
201
|
-
if (protectedEntries.has(name)) continue;
|
|
258
|
+
if (protectedEntries.has(name) || preserveNative(name)) continue;
|
|
202
259
|
const current = read(target, name), template = readFileSync(join(packageRoot, '.pbai4devs/templates/entrypoints', name), 'utf8');
|
|
203
260
|
const legacy = entryContent(null, template, language, name);
|
|
204
261
|
const content = legacy.replace(`${begin}\n`, '').replace(`\n${end}`, '');
|
|
@@ -206,11 +263,18 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
206
263
|
const knownLegacy = current && previous?.blocks[name] && current.toString('utf8') === entryContent(null, template, previous.language, name);
|
|
207
264
|
if (!current || knownFile || knownLegacy) { write(stage, name, content); ownedEntrypoints.add(name); }
|
|
208
265
|
}
|
|
209
|
-
if (!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`);
|
|
266
|
+
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`);
|
|
210
267
|
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`);
|
|
211
268
|
const unknown = new Set(existing.filter(path => !supplied.has(path) && !previous?.files[`.pbai4devs/${path}`] && !internal.has(path)));
|
|
212
269
|
const paths = walk(stage, '', path => !path.split('/').includes('.agent-tests')).filter(path => !path.startsWith('.pbai4devs/') || !unknown.has(path.slice(11)));
|
|
213
|
-
const desired = new Map(paths.map(path => [path, readFileSync(join(stage, path))]));
|
|
270
|
+
const desired = new Map(paths.filter(path => !preserveNative(path)).map(path => [path, readFileSync(join(stage, path))]));
|
|
271
|
+
// 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}/`));
|
|
273
|
+
for (const [path, content] of desired) if (/^\.agents\/(skills|rules|agents)\.json$/.test(path)) {
|
|
274
|
+
const discovery = JSON.parse(content);
|
|
275
|
+
discovery.entries = discovery.entries.filter(entry => !overlapsPreserved(entry.path));
|
|
276
|
+
desired.set(path, Buffer.from(`${JSON.stringify(discovery, null, 2)}\n`));
|
|
277
|
+
}
|
|
214
278
|
const manifest = { schemaVersion: 1, version: bundled.length ? packageInfo.version : previous.version, language, files: {}, blocks: {} };
|
|
215
279
|
if (mode !== 'sync' || clients !== undefined || previous?.clients !== undefined) manifest.clients = selected;
|
|
216
280
|
for (const [path, content] of desired) {
|
|
@@ -224,7 +288,8 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
224
288
|
for (const [path, content] of desired) {
|
|
225
289
|
const current = read(target, path);
|
|
226
290
|
if (current?.equals(content)) continue;
|
|
227
|
-
const managed = previous?.blocks[path] ? blockDigest(path, current || Buffer.alloc(0)) === previous.blocks[path]
|
|
291
|
+
const managed = previous?.blocks[path] ? blockDigest(path, current || Buffer.alloc(0)) === previous.blocks[path]
|
|
292
|
+
: [previous?.files[path], derivedOutputs[path], derivedEntries[path]].some(digest => digest && hash(current || '') === digest);
|
|
228
293
|
const appendBlock = entryNames.includes(path) || path === '.gitignore' || path.endsWith('/.gitignore');
|
|
229
294
|
const conflict = current && path !== statePath && !managed && !appendBlock;
|
|
230
295
|
const changedBlock = current && previous?.blocks[path] && !managed;
|
|
@@ -254,11 +319,29 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
254
319
|
}
|
|
255
320
|
changes.push({ path, action: content.trim() ? 'update' : 'remove', content: content.trim() ? Buffer.from(content) : null, beforeHash: hash(current) });
|
|
256
321
|
}
|
|
257
|
-
|
|
322
|
+
function nativeFiles(root) {
|
|
323
|
+
if (boundary(root)) return [];
|
|
324
|
+
const directory = stat(safePath(target, root));
|
|
325
|
+
if (!directory?.isDirectory()) return [];
|
|
326
|
+
const files = [];
|
|
327
|
+
const visit = folder => {
|
|
328
|
+
for (const name of readdirSync(safePath(target, folder)).sort()) {
|
|
329
|
+
const path = `${folder}/${name}`;
|
|
330
|
+
if (boundary(path)) continue;
|
|
331
|
+
const info = stat(safePath(target, path));
|
|
332
|
+
if (path.split('/').some(part => ignored.has(part))) continue;
|
|
333
|
+
if (info?.isDirectory()) visit(path);
|
|
334
|
+
else if (info?.isFile()) files.push(path);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
visit(root); return files;
|
|
338
|
+
}
|
|
339
|
+
for (const root of changingClients ? [] : active.roots) for (const full of nativeFiles(root)) {
|
|
340
|
+
const path = full.slice(root.length + 1);
|
|
258
341
|
if (!['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].includes(path.split('/')[0])) continue;
|
|
259
|
-
|
|
342
|
+
if (desired.has(full) || changes.some(change => change.path === full)) continue;
|
|
260
343
|
const current = read(target, full), source = current.toString('utf8');
|
|
261
|
-
if (!(source.includes('<!-- gerado: sincronizar-agentes -->') || source.startsWith('# gerado: sincronizar-agentes\n'))) continue;
|
|
344
|
+
if (!(source.includes('<!-- gerado: sincronizar-agentes -->') || source.startsWith('# gerado: sincronizar-agentes\n'))) { recordPreserved(full, 'existing-file'); continue; }
|
|
262
345
|
if (!isReference(source)) conflicts.push(full);
|
|
263
346
|
changes.push({ path: full, action: 'remove', content: null, beforeHash: hash(current) });
|
|
264
347
|
}
|
|
@@ -269,7 +352,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
269
352
|
changes.push({ path: legacy, action: 'remove', content: null, beforeHash: hash(legacyContent) });
|
|
270
353
|
}
|
|
271
354
|
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])));
|
|
272
|
-
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])];
|
|
355
|
+
const ownedOutputs = [...new Set([...synchronized.ownedOutputs, ...ownedEntrypoints, statePath])].filter(path => !overlapsPreserved(path));
|
|
273
356
|
const hashes = {};
|
|
274
357
|
for (const [path, digest] of Object.entries(previous?.files || {})) if ((ownedOutputs.includes(path) || !path.startsWith('.pbai4devs/')) && !protectedEntries.has(path)) hashes[path] = digest;
|
|
275
358
|
for (const path of ownedEntrypoints) { const current = read(target, path); if (current && (previous?.blocks[path] || derivedEntries[path] === hash(current))) hashes[path] = hash(current); }
|
|
@@ -282,7 +365,7 @@ export function planInstallation({ target, language = 'pt', mode = 'init', force
|
|
|
282
365
|
const localIgnore = planLocalIgnore(target, { outputs: ownedOutputs, templateRoot: stage });
|
|
283
366
|
const localIgnoreCheck = checkLocalIgnore(localIgnore, { replacedBlocks: replacedIgnoreBlocks(target, changes, previous) });
|
|
284
367
|
conflicts.push(...(untrack?.conflicts || []), ...localIgnoreCheck.missing);
|
|
285
|
-
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck };
|
|
368
|
+
return { target, language, mode, changes, conflicts: [...new Set(conflicts)].sort(), canApply: (!conflicts.length || force) && !untrack?.conflicts?.length && !localIgnoreCheck.missing.length, manifest, retiredRoots, ownedOutputs, untrack, localIgnore, localIgnoreCheck, preserved: [...preserved.values()].sort((a, b) => a.path.localeCompare(b.path)) };
|
|
286
369
|
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
287
370
|
}
|
|
288
371
|
|
|
@@ -294,42 +377,63 @@ export function applyPlan(plan) {
|
|
|
294
377
|
safePath(target, backup);
|
|
295
378
|
for (const change of plan.changes) safePath(target, change.path);
|
|
296
379
|
const before = new Map(plan.changes.map(change => [change.path, read(target, change.path)]));
|
|
380
|
+
const beforeModes = new Map(plan.changes.map(change => {
|
|
381
|
+
const info = stat(safePath(target, change.path));
|
|
382
|
+
return [change.path, info ? info.mode & 0o777 : undefined];
|
|
383
|
+
}));
|
|
297
384
|
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 = [];
|
|
385
|
+
for (const [path, content] of before) if (content) write(target, `${backup}/${path}`, content, beforeModes.get(path));
|
|
386
|
+
const applied = [], removedDirectories = [];
|
|
300
387
|
let untracked = false;
|
|
301
388
|
try {
|
|
302
389
|
if (plan.untrack?.paths.length) { applyUntrack(plan.untrack); untracked = true; }
|
|
303
390
|
for (const change of plan.changes) {
|
|
304
391
|
const file = safePath(target, change.path);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
applied.push(change.path);
|
|
392
|
+
const content = change.content;
|
|
393
|
+
const signature = { path: change.path, hash: content === null ? null : hash(content) };
|
|
394
|
+
if (content === null) { rmSync(file, { force: true }); applied.push(signature); }
|
|
395
|
+
else atomicWrite(target, change.path, content, beforeModes.get(change.path), mode => {
|
|
396
|
+
signature.mode = mode; applied.push(signature);
|
|
397
|
+
});
|
|
313
398
|
}
|
|
314
|
-
pruneRemovedDirectories(plan);
|
|
399
|
+
pruneRemovedDirectories(plan, directory => removedDirectories.push(directory));
|
|
315
400
|
const changesFallback = plan.changes.some(change => change.path === '.gitignore');
|
|
316
401
|
// Metadata is committed last; its writer rolls back partial writes itself.
|
|
317
402
|
const ignored = applyLocalIgnore(changesFallback ? planLocalIgnore(target, { outputs: plan.ownedOutputs }) : plan.localIgnore);
|
|
318
403
|
return { changed: Boolean(plan.changes.length || ignored.changed || untracked), backup: [...before.values()].some(Boolean) ? backup : null };
|
|
319
404
|
} catch (error) {
|
|
320
|
-
const rollbackErrors = [];
|
|
321
|
-
for (const
|
|
405
|
+
const rollbackErrors = [], preserved = [];
|
|
406
|
+
for (const removed of [...removedDirectories].sort((a, b) => a.path.length - b.path.length)) {
|
|
407
|
+
let directory, current;
|
|
408
|
+
try { directory = safePath(target, removed.path); current = stat(directory); }
|
|
409
|
+
catch { preserved.push(removed.path); continue; }
|
|
410
|
+
if (current) continue;
|
|
411
|
+
try { mkdirSync(directory, { mode: removed.mode }); chmodSync(directory, removed.mode); }
|
|
412
|
+
catch (failure) { rollbackErrors.push(failure); }
|
|
413
|
+
}
|
|
414
|
+
for (const appliedFile of applied.reverse()) {
|
|
415
|
+
const { path } = appliedFile;
|
|
416
|
+
let unchanged = false;
|
|
417
|
+
try {
|
|
418
|
+
const file = safePath(target, path), info = stat(file);
|
|
419
|
+
unchanged = appliedFile.hash === null ? !info : Boolean(info?.isFile()
|
|
420
|
+
&& (info.mode & 0o777) === appliedFile.mode && hash(readFileSync(file)) === appliedFile.hash);
|
|
421
|
+
} catch { /* Não restaurar quando não é possível comprovar a própria gravação. */ }
|
|
422
|
+
if (!unchanged) { preserved.push(path); continue; }
|
|
322
423
|
const content = before.get(path);
|
|
323
|
-
try { if (content)
|
|
424
|
+
try { if (content) atomicWrite(target, path, content, beforeModes.get(path)); else rmSync(safePath(target, path), { force: true }); }
|
|
324
425
|
catch (failure) { rollbackErrors.push(failure); }
|
|
325
426
|
}
|
|
427
|
+
if (preserved.length) {
|
|
428
|
+
try { error.rollbackPreservedPaths = preserved; } catch { /* Preserve a exceção original. */ }
|
|
429
|
+
}
|
|
326
430
|
if (untracked) try { restoreUntrack(plan.untrack); } catch (failure) { rollbackErrors.push(failure); }
|
|
327
431
|
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
432
|
throw error;
|
|
329
433
|
}
|
|
330
434
|
}
|
|
331
435
|
|
|
332
|
-
function pruneRemovedDirectories(plan) {
|
|
436
|
+
function pruneRemovedDirectories(plan, onRemoved) {
|
|
333
437
|
const { target } = plan;
|
|
334
438
|
const legacy = '.pbai4devs/adapters/README.md';
|
|
335
439
|
const removed = plan.changes.filter(change => change.action === 'remove' && (allowedManaged(change.path) || change.path === legacy));
|
|
@@ -342,8 +446,9 @@ function pruneRemovedDirectories(plan) {
|
|
|
342
446
|
const boundary = parent.startsWith('.pbai4devs/') ? '.pbai4devs' : adapterRoots.find(root => parent.startsWith(`${root}/`));
|
|
343
447
|
while (boundary && parent.startsWith(`${boundary}/`)) {
|
|
344
448
|
const directory = safePath(target, parent);
|
|
345
|
-
|
|
346
|
-
|
|
449
|
+
const info = stat(directory);
|
|
450
|
+
if (!info?.isDirectory() || readdirSync(directory).length) break;
|
|
451
|
+
try { rmdirSync(directory); onRemoved?.({ path: parent, mode: info.mode & 0o777 }); }
|
|
347
452
|
catch (error) { if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) break; throw error; }
|
|
348
453
|
parent = portable(dirname(parent));
|
|
349
454
|
}
|
|
@@ -351,8 +456,9 @@ function pruneRemovedDirectories(plan) {
|
|
|
351
456
|
for (const root of plan.retiredRoots || []) {
|
|
352
457
|
if (!adapterRoots.includes(root)) continue;
|
|
353
458
|
const directory = safePath(target, root);
|
|
354
|
-
|
|
355
|
-
|
|
459
|
+
const info = stat(directory);
|
|
460
|
+
if (!info?.isDirectory()) continue;
|
|
461
|
+
try { if (readdirSync(directory).length === 0) { rmdirSync(directory); onRemoved?.({ path: root, mode: info.mode & 0o777 }); } }
|
|
356
462
|
catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error; }
|
|
357
463
|
}
|
|
358
464
|
}
|
|
@@ -360,10 +466,11 @@ function pruneRemovedDirectories(plan) {
|
|
|
360
466
|
export function inspect(target) {
|
|
361
467
|
const state = loadState(target);
|
|
362
468
|
if (!state) return { installed: false, issues: [{ code: 'missing', path: statePath }] };
|
|
469
|
+
const derived = readDerivedState(target);
|
|
363
470
|
const issues = [];
|
|
364
471
|
for (const [path, digest] of Object.entries(state.files)) {
|
|
365
472
|
const content = read(target, path);
|
|
366
|
-
if (!content || hash(content) !== digest) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
473
|
+
if (!content || hash(content) !== digest && hash(content) !== derived.outputs[path]) issues.push({ code: content ? 'modified' : 'missing', path });
|
|
367
474
|
}
|
|
368
475
|
for (const [path, digest] of Object.entries(state.blocks)) {
|
|
369
476
|
const content = read(target, path);
|