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 +29 -9
- package/bin/iron-edge.js +15 -3
- package/installer/gerados.js +106 -0
- package/installer/install.js +10 -78
- package/installer/update.js +343 -0
- package/package.json +1 -1
- package/skills/blindar-sistema/SKILL.md +123 -0
- package/skills/blindar-sistema/advanced/frieren-opcional.md +61 -0
- package/skills/blindar-sistema/kb/idor.md +94 -0
- package/skills/blindar-sistema/kb/llm-injection.md +86 -0
- package/skills/blindar-sistema/kb/secrets.md +89 -0
- package/skills/blindar-sistema/kb/sqli.md +77 -0
- package/skills/blindar-sistema/procedures/llm-endpoint-hardening.md +80 -0
- package/skills/blindar-sistema/procedures/sandbox-dado-falso.md +63 -0
- package/skills/blindar-sistema/procedures/secret-hygiene.md +72 -0
- package/skills/gate/SKILL.md +6 -0
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,
|
|
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
|
|
32
|
+
**Skills — essas viram comando de barra.** São 15, e o Claude Code as reconhece por `/nome`:
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
/gate
|
|
36
|
-
/
|
|
37
|
-
/
|
|
38
|
-
/
|
|
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
|
-
**
|
|
170
|
+
**Atualizar é um comando. Remover é operação conduzida.**
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
- **Atualizar:**
|
|
171
173
|
|
|
172
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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 };
|
package/installer/install.js
CHANGED
|
@@ -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
|
-
//
|
|
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('');
|