iron-edge 2.0.2 → 2.2.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
@@ -40,7 +40,7 @@ Existem **duas coisas diferentes** aqui, e elas se chamam de jeitos diferentes.
40
40
  (As outras: `anotar`, `faxina`, `conectar-mcp`, `roteiro-video`, `lp-cinematico`, `clone-site`,
41
41
  `clone-inventory`, `clone-distill`, `clone-organize`, `clone-transplant`.)
42
42
 
43
- **Agentes — esses NÃO são comando de barra.** São 44, e você os chama **pedindo em português**:
43
+ **Agentes — esses você chama pedindo.** São 44:
44
44
 
45
45
  ```
46
46
  "usa o iron-ceo" ← comece por aqui, ele direciona
@@ -48,14 +48,27 @@ Existem **duas coisas diferentes** aqui, e elas se chamam de jeitos diferentes.
48
48
  "quero o iron-shopify nessa loja"
49
49
  ```
50
50
 
51
+ **E tem um atalho, que é como se usa no dia a dia.** A instalação cria o `iron`, e você o chama
52
+ escrevendo só a palavra:
53
+
54
+ ```
55
+ iron ← ativa e pergunta o que você precisa
56
+ iron preciso da copy desse anúncio ← já entra com o pedido
57
+ ```
58
+
59
+ **Sem barra e sem traço.** Se você preferir a barra, `/iron` faz exatamente o mesmo — ela é o
60
+ atalho explícito da interface, não uma exigência. As três formas (`iron`, `/iron`,
61
+ `"usa o iron-ceo"`) chegam no mesmo lugar.
62
+
63
+ Os outros 43 agentes continuam sendo chamados pedindo. O pacote instala **um** atalho, não 44.
64
+
51
65
  Digitar `/iron-ceo` **não funciona** — barra é para skill, e agente não é skill. Se quiser a
52
66
  menção explícita, a forma que resolve é **`@agent-iron-ceo`** (com o prefixo `agent-`; o
53
67
  `@iron-ceo` sozinho não é documentado). O Claude Code também escolhe o agente sozinho quando o
54
68
  pedido é claro; nomear é só para ter certeza de qual.
55
69
 
56
- > Se você viu em algum lugar a instrução de digitar `/iron-ceo`, ela está desatualizada — isso
57
- > nunca funcionou, porque o pacote não cria `~/.claude/commands/`. Sua instalação não tem defeito.
58
- > Para conferir que está tudo no lugar: `npx iron-edge verify`.
70
+ > Se você viu em algum lugar a instrução de digitar `/iron-ceo`, ela está desatualizada: o atalho
71
+ > se chama **`/iron`**. Sua instalação não tem defeito. Para conferir: `npx iron-edge verify`.
59
72
 
60
73
  **Squads — o terceiro nível, e você não chama direto.** São 18 equipes que vivem em `~/.iron/`.
61
74
  Cada uma tem um agente de topo, e é por ele que se entra:
@@ -152,11 +165,29 @@ Instalada junto. Depois de `npx iron-edge install`, os dois arquivos abaixo est
152
165
  npx iron-edge verify
153
166
  ```
154
167
 
155
- **Não existe `update` nem `uninstall` automáticos, e isso é decisão, não falta.**
168
+ **Atualizar é um comando. Remover é operação conduzida.**
169
+
170
+ - **Atualizar:**
171
+
172
+ ```bash
173
+ npx iron-edge@latest update
174
+ ```
175
+
176
+ Ele lê o manifesto e confere **arquivo por arquivo, por hash**. Só troca o que estiver
177
+ byte a byte idêntico ao pacote — ou seja, o que ninguém encostou. Numa instalação típica
178
+ isso é mais de 2.100 dos 2.126 arquivos.
179
+
180
+ **O que ele nunca toca:** o seu `config.yaml`, o seu cofre, agentes e skills que você
181
+ criou, e **qualquer arquivo do pacote que você tenha editado** — esses ficam marcados no
182
+ manifesto e continuam preservados nas atualizações seguintes. Ele lista um por um antes de
183
+ agir. Se quiser ver o plano sem escrever nada: `npx iron-edge update --dry-run`.
156
184
 
157
- 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:
185
+ Se algo falhar no meio, nada fica pela metade: ele escreve tudo ao lado primeiro e só
186
+ depois coloca no lugar. Falhou antes disso, sua instalação continua exatamente como estava.
158
187
 
159
- - **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.
188
+ > Instalação anterior a 24/08/2026, feita por zip, não tem manifesto — sem ele não dá para
189
+ > separar o que é seu por lista. Nesse caso rode `npx iron-edge install` e siga o **CASO B**
190
+ > da mensagem que aparece.
160
191
  - **Remover — nesta ordem, porque a lista do que remover mora dentro da pasta que some:**
161
192
 
162
193
  1. **Primeiro** tire uma cópia do manifesto para fora:
@@ -164,7 +195,7 @@ O instalador **nunca sobrescreve** o que já está em `~/.claude/` e em `~/.iron
164
195
  cp ~/.iron/install-manifest.json ./iron-manifest.json
165
196
  ```
166
197
  (Guarda na pasta em que você está. `~/Desktop` nem sempre existe — em servidor e container, não existe.)
167
- 2. Abra essa cópia e olhe o campo `files` — é a lista exata do que o pacote escreveu em `~/.claude/agents/` e `~/.claude/skills/`.
198
+ 2. Abra essa cópia e olhe o campo `files` — é a lista exata do que o pacote escreveu em `~/.claude/agents/`, `~/.claude/skills/` e `~/.claude/commands/`.
168
199
  3. Apague **só** esses arquivos. O que você criou não está na lista e fica.
169
200
  4. **Por último**, apague a pasta `~/.iron/` inteira.
170
201
 
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 };
@@ -58,8 +58,9 @@ const IRON_DIR = path.join(HOME, '.iron');
58
58
  const CLAUDE_DIR = path.join(HOME, '.claude');
59
59
  const AGENTS_DEST = path.join(CLAUDE_DIR, 'agents');
60
60
  const SKILLS_DEST = path.join(CLAUDE_DIR, 'skills');
61
+ const COMMANDS_DEST = path.join(CLAUDE_DIR, 'commands');
61
62
 
62
- const ALLOWED_ROOTS = [IRON_DIR, AGENTS_DEST, SKILLS_DEST];
63
+ const ALLOWED_ROOTS = [IRON_DIR, AGENTS_DEST, SKILLS_DEST, COMMANDS_DEST];
63
64
 
64
65
  const WIN_RESERVED = new Set([
65
66
  'CON','PRN','AUX','NUL',
@@ -289,7 +290,7 @@ Da pra separar o que e seu por lista:
289
290
  O que NAO estiver la foi voce que criou.
290
291
  3. Guarde o que for seu (agente proprio, skill propria, o que voce editou).
291
292
  "npx iron-edge verify" mostra o que voce alterou em relacao ao manifesto.
292
- 4. Apague em ~/.claude/agents e ~/.claude/skills SO os arquivos que estao na
293
+ 4. Apague em ~/.claude/agents, ~/.claude/skills e ~/.claude/commands SO os que estao na
293
294
  lista. Depois, POR ULTIMO, apague a pasta ~/.iron inteira.
294
295
  5. Rode a instalacao de novo.
295
296
 
@@ -446,28 +447,6 @@ log(` ${manifest.length} arquivos copiados.`);
446
447
 
447
448
  for (const d of [path.join(IRON_DIR, 'integrations'), path.join(IRON_DIR, 'secrets')]) {
448
449
  criarPasta(d, 'as pastas de estado');
449
-
450
- // As duas pastas nascem vazias e com nome que gera duvida ("secrets" assusta quem vai por
451
- // dado de negocio). Um LEIAME em cada uma explica pra que servem — pasta vazia sem
452
- // explicacao foi achado de um leitor sem contexto nenhum.
453
- escrever(path.join(IRON_DIR, 'secrets', 'LEIA-ME.md'), [
454
- '# secrets/',
455
- '',
456
- 'Pasta para as SUAS credenciais, se um dia voce conectar alguma integracao.',
457
- '**Ela nasce vazia e o Iron nao escreve nada aqui sozinho.**',
458
- '',
459
- 'Nada nesta pasta e enviado para lugar nenhum: e arquivo no seu disco, como',
460
- 'qualquer outro. Se voce nunca conectar integracao, ela fica vazia para sempre —',
461
- 'e pode apagar sem quebrar nada.',
462
- ''
463
- ].join('\n'), 'o LEIA-ME de secrets');
464
- escrever(path.join(IRON_DIR, 'integrations', 'LEIA-ME.md'), [
465
- '# integrations/',
466
- '',
467
- 'Configuracao das integracoes opcionais (por enquanto, so o Shopify via Iron Office).',
468
- 'O `shopify.yaml` nasce com `enabled: false`: nada e ligado sem voce ligar.',
469
- ''
470
- ].join('\n'), 'o LEIA-ME de integrations');
471
450
  }
472
451
 
473
452
  // memoria por cargo do squad-engineering: pasta vazia, FORA do manifesto de propósito.
@@ -478,31 +457,6 @@ criarPasta(path.join(IRON_DIR, 'memory', 'squad-engineering'), 'a pasta de memor
478
457
  const shopifyYaml = path.join(IRON_DIR, 'integrations', 'shopify.yaml');
479
458
  escrever(shopifyYaml, 'enabled: false\nendpoint: ""\napi_key_file: ""\n', 'a config da integracao');
480
459
 
481
- const configYaml = path.join(IRON_DIR, 'config.yaml');
482
- escrever(configYaml, [
483
- '# Iron Edge - configuracao. Tudo aqui e opcional, mas os dois primeiros blocos mudam',
484
- '# muito a qualidade da resposta. Edite a mao, ou peca ao Iron: "configura o Iron pra mim".',
485
- '',
486
- '# QUEM E VOCE. Sem isto, tudo que o Iron responder sai generico.',
487
- 'owner: "" # seu nome',
488
- 'businesses: [] # ex: ["loja de roupa", "consultoria de trafego"]',
489
- '',
490
- '# O COFRE - a memoria do Iron entre uma conversa e outra.',
491
- '#',
492
- '# SEM cofre, cada conversa comeca do zero: o Iron nao lembra dos seus projetos, do que',
493
- '# ficou pendente, nem do que voce decidiu ontem. COM cofre, ele le tudo isso antes de',
494
- '# trabalhar. E uma pasta de arquivos .md comuns, na SUA maquina - abre no Obsidian, no',
495
- '# VS Code ou em qualquer editor. Nada e enviado pra lugar nenhum.',
496
- '#',
497
- '# Ainda nao tem? Peca ao Iron: "cria meu cofre". Ele monta a estrutura no lugar que',
498
- '# voce escolher e preenche este campo sozinho. Leva um minuto.',
499
- 'vault_path: "" # ex: ~/Documents/Cofre',
500
- '',
501
- '# focus: dev # prioriza o iron-eng (engenharia de software) no roteamento.',
502
- '# # descomente se o seu uso principal for programar.',
503
- ''
504
- ].join('\n'), 'a config inicial');
505
-
506
460
  const installedAt = new Date().toISOString();
507
461
 
508
462
  // PATCH 7: gerados entram no manifesto com generated:true
@@ -518,13 +472,21 @@ const installManifest = {
518
472
  platform: process.platform,
519
473
  node: process.version,
520
474
  home: HOME,
521
- scopes: { iron_root: IRON_DIR, claude_agents: AGENTS_DEST, claude_skills: SKILLS_DEST },
475
+ scopes: { iron_root: IRON_DIR, claude_agents: AGENTS_DEST, claude_skills: SKILLS_DEST, claude_commands: COMMANDS_DEST },
522
476
  files: manifest // sera reescrito abaixo ja com os gerados
523
477
  };
524
478
 
525
- // 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
+ }
526
489
  addGenerated(shopifyYaml);
527
- addGenerated(configYaml);
528
490
 
529
491
  const metadataPath = path.join(IRON_DIR, 'metadata.json');
530
492
  escrever(metadataPath,
@@ -579,11 +541,15 @@ log(' PROXIMO PASSO');
579
541
  log('--------------------------------');
580
542
  log('1. Reabra o Claude Code. Agente novo so aparece em sessao nova.');
581
543
  log('');
582
- log('2. Chame o orquestrador PEDINDO, em portugues:');
544
+ log('2. Chame o orquestrador. Escreva so isto:');
545
+ log('');
546
+ log(' iron');
583
547
  log('');
584
- log(' usa o iron-ceo');
548
+ log(' So a palavra, sem barra e sem traco. Se preferir a barra, /iron faz o mesmo,');
549
+ log(' e da pra ja mandar o pedido junto: iron preciso da copy desse anuncio');
585
550
  log('');
586
- log(' (Agente nao e comando de barra. As 14 SKILLS sim: /gate /salvar /brandkit ...)');
551
+ log(' Os outros 43 agentes se chamam PEDINDO ("usa o iron-copy").');
552
+ log(' As 14 SKILLS sao de barra: /gate /salvar /brandkit /escada ...');
587
553
  log('');
588
554
  log('3. Ligue a memoria. Diga a ele:');
589
555
  log('');
@@ -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('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iron-edge",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Ecossistema de agentes para Claude Code: 44 agentes de topo, 18 squads e 14 skills, com quality gate, memoria e instalador que nunca sobrescreve.",
5
5
  "keywords": [
6
6
  "claude",