iron-edge 2.1.0 → 2.3.0

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Iron Edge — Ecossistema de Agentes para Claude Code
2
2
 
3
- **44 agentes de topo, 18 squads, 14 skills e memória entre sessões.**
3
+ **44 agentes de topo, 18 squads, 15 skills e memória entre sessões.**
4
4
 
5
5
  Uma equipe completa de IA para marketing, vendas, conteúdo, Shopify, vídeo e crescimento — tudo dentro do Claude Code.
6
6
 
@@ -29,13 +29,14 @@ São arquivos `.md` comuns — abrem no Obsidian, no VS Code ou em qualquer edit
29
29
  Existem **duas coisas diferentes** aqui, e elas se chamam de jeitos diferentes. Confundir as duas
30
30
  é o erro mais comum de quem acabou de instalar.
31
31
 
32
- **Skills — essas viram comando de barra.** São 14, e o Claude Code as reconhece por `/nome`:
32
+ **Skills — essas viram comando de barra.** São 15, e o Claude Code as reconhece por `/nome`:
33
33
 
34
34
  ```
35
- /gate → auditoria de qualidade antes de entregar
36
- /salvar → registra o estado do projeto
37
- /brandkit → identidade visual
38
- /escada → qual modelo usar em cada fase
35
+ /gate → auditoria de qualidade antes de entregar
36
+ /blindar-sistema → audita a segurança do seu sistema lendo o código
37
+ /salvar → registra o estado do projeto
38
+ /brandkit → identidade visual
39
+ /escada → qual modelo usar em cada fase
39
40
  ```
40
41
  (As outras: `anotar`, `faxina`, `conectar-mcp`, `roteiro-video`, `lp-cinematico`, `clone-site`,
41
42
  `clone-inventory`, `clone-distill`, `clone-organize`, `clone-transplant`.)
@@ -130,6 +131,7 @@ são os 13 conselhos `xq-*` (squads de referência, ex.: `xq-copy`, `xq-hormozi`
130
131
  | Skill | O que faz |
131
132
  |-------|-----------|
132
133
  | `gate` | Quality gate de aprovação duplicada: um painel de auditores independentes audita o que você produziu — arte, site, código, pipeline, PDF, vídeo — **antes** de virar entrega, commit ou publicação. Nada sai sem parecer com zero bloqueador |
134
+ | `blindar-sistema` | Auditoria de segurança do seu sistema **lendo o código** (white-box): acha as portas abertas mais comuns de quem programa com IA — cliente vendo dado de outro, chave exposta, injeção de SQL, chatbot de IA sem proteção — sem proxy, sem instalar nada, sem mandar dado pra fora, no Mac e no Windows |
133
135
  | `salvar` | Fecha a sessão registrando o que foi feito e o que mudou de estado |
134
136
  | `anotar` | Guarda o aprendizado reaproveitável, separado do registro de projeto |
135
137
  | `escada` | Qual modelo usar em cada fase, para não gastar caro na fase longa |
@@ -165,11 +167,29 @@ Instalada junto. Depois de `npx iron-edge install`, os dois arquivos abaixo est
165
167
  npx iron-edge verify
166
168
  ```
167
169
 
168
- **Não existe `update` nem `uninstall` automáticos, e isso é decisão, não falta.**
170
+ **Atualizar é um comando. Remover é operação conduzida.**
169
171
 
170
- O instalador **nunca sobrescreve** o que já está em `~/.claude/` e em `~/.iron/` — se ele passasse por cima, apagaria o agente que você escreveu e a memória que a sua máquina acumulou. Então atualizar e remover são operações conduzidas, com você no circuito:
172
+ - **Atualizar:**
171
173
 
172
- - **Atualizar:** rode `npx iron-edge verify` para ver o que **você** alterou e guardar esses arquivos. Depois remova a instalação antiga como descrito abaixo e instale de novo. O instalador nunca passa por cima — se encontrar a instalação antiga, ele para e não toca em nada.
174
+ ```bash
175
+ npx iron-edge@latest update
176
+ ```
177
+
178
+ Ele lê o manifesto e confere **arquivo por arquivo, por hash**. Só troca o que estiver
179
+ byte a byte idêntico ao pacote — ou seja, o que ninguém encostou. Numa instalação típica
180
+ isso é mais de 2.100 dos 2.126 arquivos.
181
+
182
+ **O que ele nunca toca:** o seu `config.yaml`, o seu cofre, agentes e skills que você
183
+ criou, e **qualquer arquivo do pacote que você tenha editado** — esses ficam marcados no
184
+ manifesto e continuam preservados nas atualizações seguintes. Ele lista um por um antes de
185
+ agir. Se quiser ver o plano sem escrever nada: `npx iron-edge update --dry-run`.
186
+
187
+ Se algo falhar no meio, nada fica pela metade: ele escreve tudo ao lado primeiro e só
188
+ depois coloca no lugar. Falhou antes disso, sua instalação continua exatamente como estava.
189
+
190
+ > Instalação anterior a 24/08/2026, feita por zip, não tem manifesto — sem ele não dá para
191
+ > separar o que é seu por lista. Nesse caso rode `npx iron-edge install` e siga o **CASO B**
192
+ > da mensagem que aparece.
173
193
  - **Remover — nesta ordem, porque a lista do que remover mora dentro da pasta que some:**
174
194
 
175
195
  1. **Primeiro** tire uma cópia do manifesto para fora:
package/bin/iron-edge.js CHANGED
@@ -10,8 +10,12 @@
10
10
  * imprime versao, edicao, commit e os
11
11
  * arquivos que o usuario alterou
12
12
  *
13
- * Nao existe 'update' nem 'uninstall' automaticos: atualizar e remover sao
14
- * operacoes conduzidas (runbook), com humano no circuito.
13
+ * update [--dry-run] [--source <dir>] atualiza uma instalacao existente: troca SO os
14
+ * arquivos cujo hash bate com o manifesto — os que
15
+ * ninguem alterou — e preserva o resto
16
+ *
17
+ * Nao existe 'uninstall' automatico: remover continua sendo operacao conduzida, com o
18
+ * manifesto na mao (o README descreve o procedimento).
15
19
  */
16
20
  const fs = require('fs');
17
21
  const path = require('path');
@@ -35,6 +39,14 @@ if (cmd === 'install') {
35
39
  const r = spawnSync(process.execPath, args, { stdio: 'inherit' });
36
40
  process.exit(r.status ?? 1);
37
41
 
42
+ } else if (cmd === 'update') {
43
+ // atualiza uma instalacao existente: troca so' o que esta identico ao manifesto.
44
+ const pkgRoot = path.join(__dirname, '..');
45
+ const args = [path.join(pkgRoot, 'installer', 'update.js'), ...rest];
46
+ if (!rest.includes('--source')) args.push('--source', pkgRoot);
47
+ const r = spawnSync(process.execPath, args, { stdio: 'inherit' });
48
+ process.exit(r.status ?? 1);
49
+
38
50
  } else if (cmd === 'verify') {
39
51
  const manPath = path.join(HOME, '.iron', 'install-manifest.json');
40
52
  if (!fs.existsSync(manPath)) {
@@ -69,6 +81,6 @@ if (cmd === 'install') {
69
81
  process.exit(alterados.length + sumidos.length === 0 ? 0 : 1);
70
82
 
71
83
  } else {
72
- console.log('uso: iron-edge <install|verify> [--dry-run] [--source <dir>]');
84
+ console.log('uso: iron-edge <install|update|verify> [--dry-run] [--source <dir>]');
73
85
  process.exit(cmd ? 1 : 0);
74
86
  }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Os arquivos que o Iron GERA na maquina (nao vem do payload): config, atalho, integracoes.
3
+ *
4
+ * Por que este arquivo existe: o `install` criava esses arquivos e o `update` nao sabia deles.
5
+ * Resultado medido: quem atualizasse do 2.0.2 pro 2.1.0 NAO ganhava o atalho `/iron`, porque
6
+ * ele e' gerado, nao copiado. A correcao obvia — repetir a geracao dentro do update — seria o
7
+ * defeito de propagacao que este pacote passou um ciclo inteiro cacando: duas copias que
8
+ * divergem no dia em que alguem editar so' uma.
9
+ *
10
+ * Entao a lista mora aqui, e os dois scripts leem daqui.
11
+ *
12
+ * `sobrescrever`:
13
+ * false → e' do usuario depois de criado. Nunca se toca (config.yaml, shopify.yaml).
14
+ * true → e' do pacote. O update troca, MAS so' se o hash bater com o manifesto; se a
15
+ * pessoa editou, ela ganha aviso e o arquivo dela fica.
16
+ */
17
+ 'use strict';
18
+ const path = require('path');
19
+
20
+ function arquivosGerados({ IRON_DIR, CLAUDE_DIR }) {
21
+ const COMMANDS = path.join(CLAUDE_DIR, 'commands');
22
+ return [
23
+ {
24
+ dest: path.join(IRON_DIR, 'config.yaml'),
25
+ oQueE: 'a config inicial',
26
+ sobrescrever: false,
27
+ conteudo: [
28
+ '# Iron Edge - configuracao. Tudo aqui e opcional, mas os dois primeiros blocos mudam',
29
+ '# muito a qualidade da resposta. Edite a mao, ou peca ao Iron: "configura o Iron pra mim".',
30
+ '',
31
+ '# QUEM E VOCE. Sem isto, tudo que o Iron responder sai generico.',
32
+ 'owner: "" # seu nome',
33
+ 'businesses: [] # ex: ["loja de roupa", "consultoria de trafego"]',
34
+ '',
35
+ '# O COFRE - a memoria do Iron entre uma conversa e outra.',
36
+ '#',
37
+ '# SEM cofre, cada conversa comeca do zero: o Iron nao lembra dos seus projetos, do que',
38
+ '# ficou pendente, nem do que voce decidiu ontem. COM cofre, ele le tudo isso antes de',
39
+ '# trabalhar. E uma pasta de arquivos .md comuns, na SUA maquina - abre no Obsidian, no',
40
+ '# VS Code ou em qualquer editor. Nada e enviado pra lugar nenhum.',
41
+ '#',
42
+ '# Ainda nao tem? Peca ao Iron: "cria meu cofre". Ele monta a estrutura no lugar que',
43
+ '# voce escolher e preenche este campo sozinho. Leva um minuto.',
44
+ 'vault_path: "" # ex: ~/Documents/Cofre',
45
+ '',
46
+ '# focus: dev # prioriza o iron-eng (engenharia de software) no roteamento.',
47
+ '# # descomente se o seu uso principal for programar.',
48
+ '',
49
+ ].join('\n'),
50
+ },
51
+ {
52
+ dest: path.join(COMMANDS, 'iron.md'),
53
+ oQueE: 'o atalho iron',
54
+ sobrescrever: true,
55
+ conteudo: [
56
+ '---',
57
+ 'description: Iron Edge - chama o orquestrador master. Ele entende o pedido, decide qual equipe resolve e aciona os especialistas certos.',
58
+ '---',
59
+ '',
60
+ 'Leia o arquivo `~/.claude/agents/iron-ceo.md` por inteiro e assuma aquele papel agora,',
61
+ 'seguindo a sequencia de ativacao descrita la (ler o config, ler o cofre se houver, ler o',
62
+ 'mapa de roteamento, e so entao saudar).',
63
+ '',
64
+ 'Nao resuma o arquivo nem trabalhe de memoria: leia o conteudo real, porque ele muda quando',
65
+ 'o pacote e atualizado.',
66
+ '',
67
+ 'Pedido do usuario: $ARGUMENTS',
68
+ '',
69
+ '(Sem pedido depois do comando, apenas ative e pergunte qual e a missao.)',
70
+ '',
71
+ 'Este atalho atende tanto por `/iron` quanto pela palavra `iron` sozinha.',
72
+ '',
73
+ ].join('\n'),
74
+ },
75
+ {
76
+ dest: path.join(IRON_DIR, 'secrets', 'LEIA-ME.md'),
77
+ oQueE: 'o LEIA-ME de secrets',
78
+ sobrescrever: true,
79
+ conteudo: [
80
+ '# secrets/',
81
+ '',
82
+ 'Pasta para as SUAS credenciais, se um dia voce conectar alguma integracao.',
83
+ '**Ela nasce vazia e o Iron nao escreve nada aqui sozinho.**',
84
+ '',
85
+ 'Nada nesta pasta e enviado para lugar nenhum: e arquivo no seu disco, como',
86
+ 'qualquer outro. Se voce nunca conectar integracao, ela fica vazia para sempre —',
87
+ 'e pode apagar sem quebrar nada.',
88
+ '',
89
+ ].join('\n'),
90
+ },
91
+ {
92
+ dest: path.join(IRON_DIR, 'integrations', 'LEIA-ME.md'),
93
+ oQueE: 'o LEIA-ME de integrations',
94
+ sobrescrever: true,
95
+ conteudo: [
96
+ '# integrations/',
97
+ '',
98
+ 'Configuracao das integracoes opcionais (por enquanto, so o Shopify via Iron Office).',
99
+ 'O `shopify.yaml` nasce com `enabled: false`: nada e ligado sem voce ligar.',
100
+ '',
101
+ ].join('\n'),
102
+ },
103
+ ];
104
+ }
105
+
106
+ module.exports = { arquivosGerados };
@@ -447,28 +447,6 @@ log(` ${manifest.length} arquivos copiados.`);
447
447
 
448
448
  for (const d of [path.join(IRON_DIR, 'integrations'), path.join(IRON_DIR, 'secrets')]) {
449
449
  criarPasta(d, 'as pastas de estado');
450
-
451
- // As duas pastas nascem vazias e com nome que gera duvida ("secrets" assusta quem vai por
452
- // dado de negocio). Um LEIAME em cada uma explica pra que servem — pasta vazia sem
453
- // explicacao foi achado de um leitor sem contexto nenhum.
454
- escrever(path.join(IRON_DIR, 'secrets', 'LEIA-ME.md'), [
455
- '# secrets/',
456
- '',
457
- 'Pasta para as SUAS credenciais, se um dia voce conectar alguma integracao.',
458
- '**Ela nasce vazia e o Iron nao escreve nada aqui sozinho.**',
459
- '',
460
- 'Nada nesta pasta e enviado para lugar nenhum: e arquivo no seu disco, como',
461
- 'qualquer outro. Se voce nunca conectar integracao, ela fica vazia para sempre —',
462
- 'e pode apagar sem quebrar nada.',
463
- ''
464
- ].join('\n'), 'o LEIA-ME de secrets');
465
- escrever(path.join(IRON_DIR, 'integrations', 'LEIA-ME.md'), [
466
- '# integrations/',
467
- '',
468
- 'Configuracao das integracoes opcionais (por enquanto, so o Shopify via Iron Office).',
469
- 'O `shopify.yaml` nasce com `enabled: false`: nada e ligado sem voce ligar.',
470
- ''
471
- ].join('\n'), 'o LEIA-ME de integrations');
472
450
  }
473
451
 
474
452
  // memoria por cargo do squad-engineering: pasta vazia, FORA do manifesto de propósito.
@@ -479,31 +457,6 @@ criarPasta(path.join(IRON_DIR, 'memory', 'squad-engineering'), 'a pasta de memor
479
457
  const shopifyYaml = path.join(IRON_DIR, 'integrations', 'shopify.yaml');
480
458
  escrever(shopifyYaml, 'enabled: false\nendpoint: ""\napi_key_file: ""\n', 'a config da integracao');
481
459
 
482
- const configYaml = path.join(IRON_DIR, 'config.yaml');
483
- escrever(configYaml, [
484
- '# Iron Edge - configuracao. Tudo aqui e opcional, mas os dois primeiros blocos mudam',
485
- '# muito a qualidade da resposta. Edite a mao, ou peca ao Iron: "configura o Iron pra mim".',
486
- '',
487
- '# QUEM E VOCE. Sem isto, tudo que o Iron responder sai generico.',
488
- 'owner: "" # seu nome',
489
- 'businesses: [] # ex: ["loja de roupa", "consultoria de trafego"]',
490
- '',
491
- '# O COFRE - a memoria do Iron entre uma conversa e outra.',
492
- '#',
493
- '# SEM cofre, cada conversa comeca do zero: o Iron nao lembra dos seus projetos, do que',
494
- '# ficou pendente, nem do que voce decidiu ontem. COM cofre, ele le tudo isso antes de',
495
- '# trabalhar. E uma pasta de arquivos .md comuns, na SUA maquina - abre no Obsidian, no',
496
- '# VS Code ou em qualquer editor. Nada e enviado pra lugar nenhum.',
497
- '#',
498
- '# Ainda nao tem? Peca ao Iron: "cria meu cofre". Ele monta a estrutura no lugar que',
499
- '# voce escolher e preenche este campo sozinho. Leva um minuto.',
500
- 'vault_path: "" # ex: ~/Documents/Cofre',
501
- '',
502
- '# focus: dev # prioriza o iron-eng (engenharia de software) no roteamento.',
503
- '# # descomente se o seu uso principal for programar.',
504
- ''
505
- ].join('\n'), 'a config inicial');
506
-
507
460
  const installedAt = new Date().toISOString();
508
461
 
509
462
  // PATCH 7: gerados entram no manifesto com generated:true
@@ -523,38 +476,17 @@ const installManifest = {
523
476
  files: manifest // sera reescrito abaixo ja com os gerados
524
477
  };
525
478
 
526
- // adiciona shopify + config no manifesto antes de gravar o manifesto em si
479
+ // Os arquivos GERADOS (config, atalho, LEIA-ME) vivem em installer/gerados.js — um lugar so',
480
+ // lido tambem pelo `update`. Antes ficavam escritos aqui dentro, e o update nao sabia deles:
481
+ // quem atualizasse nao ganhava o atalho /iron. Duas copias divergem; uma lista nao.
482
+ const { arquivosGerados } = require('./gerados');
483
+ let configYaml = null;
484
+ for (const g of arquivosGerados({ IRON_DIR, CLAUDE_DIR })) {
485
+ escrever(g.dest, g.conteudo, g.oQueE);
486
+ addGenerated(g.dest);
487
+ if (g.dest.endsWith('config.yaml')) configYaml = g.dest;
488
+ }
527
489
  addGenerated(shopifyYaml);
528
- addGenerated(configYaml);
529
-
530
- // O ATALHO /iron — o pedido mais repetido de quem usa o Iron todo dia.
531
- //
532
- // Ele NAO e' copia do agente. A maquina de quem desenvolve tinha um commands/iron.md com o
533
- // texto INTEIRO do iron-ceo dentro: 158 linhas duplicadas, que hoje batem por sorte e amanha
534
- // divergem em silencio. Copia e' o defeito de propagacao que este pacote passou um ciclo
535
- // inteiro caçando. Aqui o comando so' APONTA pro agente — uma fonte de verdade, e o /iron
536
- // acompanha qualquer mudanca no iron-ceo.md sem ninguem lembrar de sincronizar.
537
- const ironCmd = path.join(COMMANDS_DEST, 'iron.md');
538
- escrever(ironCmd, [
539
- '---',
540
- 'description: Iron Edge - chama o orquestrador master. Ele entende o pedido, decide qual equipe resolve e aciona os especialistas certos.',
541
- '---',
542
- '',
543
- 'Leia o arquivo `~/.claude/agents/iron-ceo.md` por inteiro e assuma aquele papel agora,',
544
- 'seguindo a sequencia de ativacao descrita la (ler o config, ler o cofre se houver, ler o',
545
- 'mapa de roteamento, e so entao saudar).',
546
- '',
547
- 'Nao resuma o arquivo nem trabalhe de memoria: leia o conteudo real, porque ele muda quando',
548
- 'o pacote e atualizado.',
549
- '',
550
- 'Pedido do usuario: $ARGUMENTS',
551
- '',
552
- '(Sem pedido depois do comando, apenas ative e pergunte qual e a missao.)',
553
- '',
554
- 'Este atalho atende tanto por `/iron` quanto pela palavra `iron` sozinha.',
555
- ''
556
- ].join('\n'), 'o atalho /iron');
557
- addGenerated(ironCmd);
558
490
 
559
491
  const metadataPath = path.join(IRON_DIR, 'metadata.json');
560
492
  escrever(metadataPath,
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * iron-edge update — atualiza uma instalacao existente SEM refazer tudo.
4
+ *
5
+ * Por que isto existe: ate a versao 2.1.0 nao havia caminho de atualizacao. O instalador
6
+ * abortava em qualquer conflito e mandava a pessoa mover tres pastas, reinstalar e preencher
7
+ * o config de novo. O argumento era "nunca sobrescrever, pra nao destruir o que e' seu" — e
8
+ * era bom, ENQUANTO nao havia manifesto. Com manifesto e hash, da' pra separar exatamente o
9
+ * que veio do pacote do que e' da pessoa: numa instalacao tipica, 2120 dos 2123 arquivos estao
10
+ * byte a byte identicos ao pacote. Trocar aqueles e' matematicamente seguro.
11
+ *
12
+ * A REGRA DESTE SCRIPT, em uma frase: so' se troca arquivo cujo hash bate com o manifesto.
13
+ * Hash diferente = a pessoa mexeu = nao se toca, e avisa.
14
+ *
15
+ * Seguranca em duas fases, pra nunca deixar a instalacao pela metade:
16
+ * Fase 1 — escreve tudo como `<destino>.iron-new`. Nada que esta' em uso muda.
17
+ * Fase 2 — renomeia para o lugar. Se falhar no meio, desfaz os que ja' foram.
18
+ */
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+ const crypto = require('crypto');
23
+
24
+ const HOME = os.homedir();
25
+ const IRON_DIR = path.join(HOME, '.iron');
26
+ const CLAUDE_DIR = path.join(HOME, '.claude');
27
+ const MANIFEST = path.join(IRON_DIR, 'install-manifest.json');
28
+
29
+ const args = process.argv.slice(2);
30
+ const DRY = args.includes('--dry-run');
31
+ const srcIdx = args.indexOf('--source');
32
+ const SOURCE = srcIdx >= 0 && args[srcIdx + 1] ? path.resolve(args[srcIdx + 1]) : path.join(__dirname, '..');
33
+
34
+ const log = (m) => console.log(m);
35
+ function abort(msg) {
36
+ console.error('\n[ABORTADO] ' + msg);
37
+ process.exit(1);
38
+ }
39
+
40
+ function sha256(file) {
41
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
42
+ }
43
+
44
+ // Compara caminho por caminho REAL, nao por string. `path.resolve` nao desfaz symlink, e o
45
+ // manifesto grava o caminho ja resolvido: numa maquina onde o HOME esta atras de um link
46
+ // (macOS com /tmp, perfil corporativo redirecionado) NADA casaria, e o update concluiria que
47
+ // precisa adicionar os 2120 arquivos e remover os 2119 antigos. O --dry-run pegou isso antes
48
+ // de qualquer escrita — que e' exatamente pra isso que ele existe.
49
+ function norm(p) {
50
+ let alvo = path.resolve(p);
51
+ let resto = '';
52
+ for (;;) {
53
+ try { return path.join(fs.realpathSync(alvo), resto); }
54
+ catch (e) {
55
+ const pai = path.dirname(alvo);
56
+ if (pai === alvo) return path.resolve(p);
57
+ resto = path.join(path.basename(alvo), resto);
58
+ alvo = pai;
59
+ }
60
+ }
61
+ }
62
+
63
+ // ---------------------------------------------------------------- payload novo
64
+ // Mesmo mapeamento do install.js: o que vai pra ~/.iron e o que vai pra ~/.claude.
65
+ function destinoDe(rel) {
66
+ const norm = rel.split(path.sep).join('/');
67
+ if (norm.startsWith('agents/')) return path.join(CLAUDE_DIR, 'agents', norm.slice('agents/'.length));
68
+ if (norm.startsWith('skills/')) return path.join(CLAUDE_DIR, 'skills', norm.slice('skills/'.length));
69
+ if (norm === 'iron-routing-map.md') return path.join(IRON_DIR, 'iron-routing-map.md');
70
+ if (norm.startsWith('squads/')) return path.join(IRON_DIR, norm.slice('squads/'.length));
71
+ if (norm.startsWith('docs/')) return path.join(IRON_DIR, 'docs', norm.slice('docs/'.length));
72
+ return null;
73
+ }
74
+
75
+ function varrerPayload(dir, base = '') {
76
+ const out = [];
77
+ for (const e of fs.readdirSync(path.join(SOURCE, dir), { withFileTypes: true })) {
78
+ const rel = base ? `${base}/${e.name}` : `${dir}/${e.name}`;
79
+ if (e.isSymbolicLink()) continue;
80
+ if (e.isDirectory()) out.push(...varrerPayload(path.join(dir, e.name), rel));
81
+ else out.push(rel);
82
+ }
83
+ return out;
84
+ }
85
+
86
+ // ---------------------------------------------------------------- pre-voo
87
+ log('================================');
88
+ log(' Iron Edge - Atualizador');
89
+ log('================================');
90
+ log('HOME : ' + HOME);
91
+ log('Origem : ' + SOURCE);
92
+ if (DRY) log('MODO : --dry-run (nada sera escrito)');
93
+ log('');
94
+
95
+ if (!fs.existsSync(MANIFEST)) {
96
+ console.error(`Nao ha manifesto em ${MANIFEST}.`);
97
+ console.error('');
98
+ console.error('Isto significa que esta instalacao e ANTERIOR ao manifesto (veio de zip), e nao');
99
+ console.error('da pra separar por lista o que e do pacote do que e seu. O caminho, nesse caso, e');
100
+ console.error('o manual: rode "npx iron-edge install" e siga o CASO B da mensagem que aparece.');
101
+ process.exit(1);
102
+ }
103
+
104
+ let man;
105
+ try { man = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); }
106
+ catch (e) { abort(`o manifesto existe mas nao e JSON valido: ${e.message}`); }
107
+
108
+ const antigos = new Map(); // dest -> entrada do manifesto
109
+ for (const f of man.files || []) {
110
+ const dest = f.dest || f.path;
111
+ if (dest) antigos.set(norm(dest), f);
112
+ }
113
+ log(`Instalacao encontrada: ${antigos.size} arquivos no manifesto`);
114
+ if (man.label) log(` edicao: ${man.label}`);
115
+ if (man.installed_at) log(` instalada em: ${man.installed_at}`);
116
+
117
+ for (const must of ['agents', 'iron-routing-map.md']) {
118
+ if (!fs.existsSync(path.join(SOURCE, must))) abort(`origem invalida: faltou "${must}" em ${SOURCE}`);
119
+ }
120
+
121
+ // payload novo, mapeado por destino
122
+ const novos = new Map(); // dest -> caminho de origem
123
+ for (const dir of ['agents', 'skills', 'squads', 'docs']) {
124
+ if (!fs.existsSync(path.join(SOURCE, dir))) continue;
125
+ for (const rel of varrerPayload(dir)) {
126
+ const d = destinoDe(rel);
127
+ if (d) novos.set(norm(d), path.join(SOURCE, rel));
128
+ }
129
+ }
130
+ {
131
+ const d = destinoDe('iron-routing-map.md');
132
+ novos.set(norm(d), path.join(SOURCE, 'iron-routing-map.md'));
133
+ }
134
+
135
+ // ---------------------------------------------------------------- classificar
136
+ const plano = { trocar: [], adicionar: [], restaurar: [], preservar: [], remover: [], manter: [], gerar: [] };
137
+
138
+ for (const [dest, origem] of novos) {
139
+ const ent = antigos.get(dest);
140
+ if (!ent) { plano.adicionar.push({ dest, origem }); continue; }
141
+ if (ent.generated) { plano.manter.push({ dest, motivo: 'gerado na instalacao (config, manifesto)' }); continue; }
142
+ if (!fs.existsSync(dest)) { plano.restaurar.push({ dest, origem }); continue; }
143
+ const atual = sha256(dest);
144
+ // Arquivo que a pessoa editou fica marcado no manifesto. Sem essa marca, o update anterior
145
+ // gravava o hash NOVO (o dela) e o update SEGUINTE via hash batendo, concluia "intacto do
146
+ // pacote" e sobrescrevia: a edicao sobrevivia a uma atualizacao e morria calada na proxima.
147
+ // Medido em teste, nao deduzido. A marca so' sai quando o arquivo volta a ser identico ao
148
+ // pacote — ou seja, quando a propria pessoa desfez a edicao.
149
+ if (ent.user_modified) {
150
+ if (sha256(origem) === atual) { plano.manter.push({ dest, motivo: 'voltou a ser igual ao pacote' }); continue; }
151
+ plano.preservar.push({ dest, marcado: true });
152
+ continue;
153
+ }
154
+ if (atual !== ent.sha256) { plano.preservar.push({ dest }); continue; }
155
+ if (sha256(origem) === atual) { plano.manter.push({ dest, motivo: 'ja esta na versao nova' }); continue; }
156
+ plano.trocar.push({ dest, origem });
157
+ }
158
+
159
+ for (const [dest, ent] of antigos) {
160
+ if (novos.has(dest)) continue;
161
+ if (ent.generated) continue;
162
+ if (!fs.existsSync(dest)) continue;
163
+ if (sha256(dest) === ent.sha256) plano.remover.push({ dest });
164
+ else plano.preservar.push({ dest, obsoleto: true });
165
+ }
166
+
167
+ // Os arquivos GERADOS (config, atalho /iron, LEIA-ME) nao vem do payload — o instalador os
168
+ // escreve. Sem este bloco, quem ATUALIZAVA nao ganhava capacidade nova gerada: foi medido no
169
+ // 2.0.2 -> 2.1.0, em que o /iron simplesmente nao aparecia. A lista vem de gerados.js, a
170
+ // mesma que o install le, pra nao existirem duas versoes do mesmo conteudo.
171
+ const { arquivosGerados } = require('./gerados');
172
+ const GERADOS = arquivosGerados({ IRON_DIR, CLAUDE_DIR });
173
+ for (const g of GERADOS) {
174
+ const dest = norm(g.dest);
175
+ const ent = antigos.get(dest);
176
+ const existe = fs.existsSync(g.dest);
177
+ if (!existe) { plano.gerar.push({ ...g, motivo: 'novo nesta versao' }); continue; }
178
+ if (!g.sobrescrever) { plano.manter.push({ dest: g.dest, motivo: 'seu (config/integracao)' }); continue; }
179
+ const atual = sha256(g.dest);
180
+ if (ent && atual !== ent.sha256) { plano.preservar.push({ dest: g.dest }); continue; }
181
+ if (atual === crypto.createHash('sha256').update(g.conteudo).digest('hex')) {
182
+ plano.manter.push({ dest: g.dest, motivo: 'ja esta na versao nova' });
183
+ } else {
184
+ plano.gerar.push({ ...g, motivo: 'mudou nesta versao' });
185
+ }
186
+ }
187
+
188
+ // ---------------------------------------------------------------- mostrar
189
+ log('');
190
+ log('--- PLANO ---');
191
+ log(` trocar : ${plano.trocar.length} (identicos ao pacote antigo, mudaram na versao nova)`);
192
+ log(` adicionar : ${plano.adicionar.length} (nao existiam)`);
193
+ log(` restaurar : ${plano.restaurar.length} (estavam no manifesto e sumiram do disco)`);
194
+ log(` remover : ${plano.remover.length} (a versao nova nao traz mais, e estavam intactos)`);
195
+ log(` preservar : ${plano.preservar.length} (VOCE alterou — nao sao tocados)`);
196
+ log(` manter : ${plano.manter.length} (ja atualizados, ou gerados na sua instalacao)`);
197
+ log(` gerar : ${plano.gerar.length} (config, atalho /iron, LEIA-ME — criados pelo instalador)`);
198
+ for (const g of plano.gerar) log(` + ${g.dest} (${g.motivo})`);
199
+
200
+ if (plano.preservar.length) {
201
+ log('');
202
+ log(' Estes arquivos NAO serao tocados, porque o hash mostra que voce mexeu neles:');
203
+ for (const p of plano.preservar.slice(0, 30))
204
+ log(` ~ ${p.dest}${p.obsoleto ? ' (a versao nova nem traz mais este arquivo)' : ''}${p.marcado ? ' (ja marcado como seu)' : ''}`);
205
+ if (plano.preservar.length > 30) log(` ... e mais ${plano.preservar.length - 30}`);
206
+ log('');
207
+ log(' Se algum deles for edicao sua que voce quer manter, esta tudo certo: fica como esta.');
208
+ log(' Se for edicao que voce ja nao quer, apague o arquivo e rode o update de novo — ele');
209
+ log(' entra em "restaurar" e volta na versao nova.');
210
+ }
211
+
212
+ log('');
213
+ log(' Seu config.yaml, seus agentes e skills proprios, e seu cofre NAO entram neste plano.');
214
+ log(' Config e manifesto sao regravados no fim; o resto nem e olhado.');
215
+
216
+ if (DRY) { log(''); log('--dry-run: nada foi escrito.'); process.exit(0); }
217
+ if (!plano.trocar.length && !plano.adicionar.length && !plano.restaurar.length && !plano.remover.length && !plano.gerar.length) {
218
+ log(''); log('Nada a fazer: sua instalacao ja esta na versao deste pacote.');
219
+ process.exit(0);
220
+ }
221
+
222
+ // ---------------------------------------------------------------- FASE 1: escrever ao lado
223
+ log('');
224
+ log('--- FASE 1: escrevendo ao lado (nada em uso muda ainda) ---');
225
+ const temporarios = [];
226
+ try {
227
+ for (const it of [...plano.trocar, ...plano.adicionar, ...plano.restaurar]) {
228
+ const tmp = it.dest + '.iron-new';
229
+ fs.mkdirSync(path.dirname(it.dest), { recursive: true });
230
+ fs.copyFileSync(it.origem, tmp);
231
+ if (sha256(tmp) !== sha256(it.origem)) throw new Error(`copia divergiu do original: ${it.dest}`);
232
+ temporarios.push({ tmp, dest: it.dest, novo: !fs.existsSync(it.dest) });
233
+ }
234
+ } catch (e) {
235
+ for (const t of temporarios) { try { fs.unlinkSync(t.tmp); } catch (_) {} }
236
+ abort(`falhou ao preparar os arquivos (${e.message}). NADA foi alterado na sua instalacao.`);
237
+ }
238
+ log(` ${temporarios.length} arquivos preparados e conferidos por hash.`);
239
+
240
+ // ---------------------------------------------------------------- FASE 2: trocar de lugar
241
+ log('');
242
+ log('--- FASE 2: colocando no lugar ---');
243
+ const feitos = [];
244
+ try {
245
+ for (const t of temporarios) {
246
+ if (!t.novo) {
247
+ const bak = t.dest + '.iron-old';
248
+ fs.renameSync(t.dest, bak);
249
+ t.bak = bak;
250
+ }
251
+ fs.renameSync(t.tmp, t.dest);
252
+ feitos.push(t);
253
+ }
254
+ } catch (e) {
255
+ log(' ERRO no meio da troca. Desfazendo...');
256
+ for (const f of feitos.reverse()) {
257
+ try {
258
+ fs.unlinkSync(f.dest);
259
+ if (f.bak) fs.renameSync(f.bak, f.dest);
260
+ } catch (_) {}
261
+ }
262
+ for (const t of temporarios) { try { fs.unlinkSync(t.tmp); } catch (_) {} }
263
+ abort(`falhou ao trocar os arquivos (${e.message}). A instalacao foi devolvida ao estado anterior.`);
264
+ }
265
+ for (const f of feitos) { if (f.bak) { try { fs.unlinkSync(f.bak); } catch (_) {} } }
266
+ log(` ${feitos.length} arquivos atualizados.`);
267
+
268
+ let removidos = 0;
269
+ for (const r of plano.remover) { try { fs.unlinkSync(r.dest); removidos++; } catch (_) {} }
270
+ if (removidos) log(` ${removidos} arquivos que a versao nova nao traz mais foram removidos.`);
271
+
272
+ // gerados: escritos direto (sao pequenos e nao ha versao "em uso" a proteger)
273
+ let geradosOk = 0;
274
+ for (const g of plano.gerar) {
275
+ try {
276
+ fs.mkdirSync(path.dirname(g.dest), { recursive: true });
277
+ fs.writeFileSync(g.dest, g.conteudo);
278
+ geradosOk++;
279
+ } catch (e) { log(` aviso: nao consegui gravar ${g.dest} (${e.code || e.message})`); }
280
+ }
281
+ if (geradosOk) log(` ${geradosOk} arquivo(s) gerado(s) desta versao criados (ex: o atalho /iron).`);
282
+
283
+ // ---------------------------------------------------------------- FASE 3: manifesto e config
284
+ log('');
285
+ log('--- FASE 3: manifesto e config ---');
286
+
287
+ const arquivos = [];
288
+ const editados = new Set(plano.preservar.map((x) => norm(x.dest)));
289
+ for (const [dest, origem] of novos) {
290
+ if (!fs.existsSync(dest)) continue;
291
+ const e = {
292
+ rel: path.relative(SOURCE, origem).split(path.sep).join('/'),
293
+ dest,
294
+ sha256: sha256(dest),
295
+ };
296
+ // a marca que impede a proxima atualizacao de comer a edicao da pessoa
297
+ if (editados.has(dest)) e.user_modified = true;
298
+ arquivos.push(e);
299
+ }
300
+ for (const [dest, ent] of antigos) {
301
+ if (novos.has(dest)) continue;
302
+ if (!fs.existsSync(dest)) continue;
303
+ if (GERADOS.some((g) => norm(g.dest) === dest)) continue; // entram abaixo, com hash fresco
304
+ arquivos.push({ ...ent, sha256: sha256(dest), generated: ent.generated || undefined });
305
+ }
306
+ for (const g of GERADOS) {
307
+ if (!fs.existsSync(g.dest)) continue;
308
+ arquivos.push({ rel: path.relative(IRON_DIR, g.dest).split(path.sep).join('/'), dest: g.dest, sha256: sha256(g.dest), generated: true });
309
+ }
310
+
311
+ const novoMan = {
312
+ ...man,
313
+ updated_at: new Date().toISOString(),
314
+ previous_installed_at: man.installed_at,
315
+ files: arquivos,
316
+ };
317
+ fs.writeFileSync(MANIFEST, JSON.stringify(novoMan, null, 2));
318
+ log(` manifesto regravado: ${arquivos.length} arquivos`);
319
+
320
+ // config: NUNCA sobrescrito. Mas se a versao nova tem campo que o seu nao tem, avisa.
321
+ const cfg = path.join(IRON_DIR, 'config.yaml');
322
+ if (fs.existsSync(cfg)) {
323
+ const seu = fs.readFileSync(cfg, 'utf8');
324
+ const campos = ['owner', 'businesses', 'vault_path'];
325
+ const faltando = campos.filter((c) => !new RegExp(`^\\s*${c}\\s*:`, 'm').test(seu));
326
+ log(' config.yaml: preservado como estava (nao foi tocado).');
327
+ if (faltando.length) {
328
+ log('');
329
+ log(' ATENCAO: a versao nova usa campos que o seu config nao tem:');
330
+ for (const f of faltando) log(` - ${f}`);
331
+ log(' Acrescente a mao, ou peca ao Iron: "atualiza meu config".');
332
+ if (faltando.includes('vault_path') && /obsidian_vault_path/.test(seu))
333
+ log(' (Voce tem "obsidian_vault_path": e o mesmo cofre. O Iron entende os dois nomes.)');
334
+ }
335
+ }
336
+
337
+ log('');
338
+ log('================================');
339
+ log(' Atualizado com sucesso');
340
+ log('================================');
341
+ log('Confira quando quiser: npx iron-edge verify');
342
+ log('Reabra o Claude Code para carregar o que mudou.');
343
+ log('');