iron-edge 2.4.3 → 2.4.5
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 +15 -2
- package/agents/iron-auditor.md +8 -1
- package/agents/iron-shopify.md +31 -47
- package/agents/iron-spy.md +3 -1
- package/bin/iron-edge.js +13 -1
- package/installer/atalho.js +87 -0
- package/installer/cofre/CLAUDE.md +33 -0
- package/installer/cofre/INDICE.md +11 -0
- package/installer/cofre/Projetos/.gitkeep +0 -0
- package/installer/cofre/templates/nota.md +18 -0
- package/installer/cofre.js +94 -0
- package/installer/doctor.js +192 -0
- package/installer/install.js +23 -11
- package/installer/office.js +5 -5
- package/installer/update.js +5 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -167,6 +167,19 @@ Instalada junto. Depois de `npx iron-edge install`, os dois arquivos abaixo est
|
|
|
167
167
|
```bash
|
|
168
168
|
# Conferir a instalação: o que é seu, o que está diferente sem registro, o que sumiu
|
|
169
169
|
npx iron-edge verify
|
|
170
|
+
|
|
171
|
+
# Medir a máquina (Node, git, Claude Code, Iron Edge, chave, plugin, cofre, atalho). Com a chave do
|
|
172
|
+
# Iron Office aceita, o resultado acende o checklist da página de Setup do site.
|
|
173
|
+
npx iron-edge doctor
|
|
174
|
+
|
|
175
|
+
# Instalar já criando um cofre vazio em ~/Iron Cofre (pronto pro Obsidian, nunca sobrescreve)
|
|
176
|
+
npx iron-edge install --cofre
|
|
177
|
+
|
|
178
|
+
# Ligar o Iron Office: depois do install (ou update), no Mac/Linux, o comando curto é
|
|
179
|
+
ironoffice # = npx iron-edge office (abra um terminal novo na primeira vez)
|
|
180
|
+
|
|
181
|
+
# Instalar sem criar o atalho ironoffice no ~/.zshrc (ou ~/.bashrc)
|
|
182
|
+
npx iron-edge install --sem-atalho
|
|
170
183
|
```
|
|
171
184
|
|
|
172
185
|
**Atualizar é um comando. Remover é operação conduzida.**
|
|
@@ -257,13 +270,13 @@ O Iron Edge instala uma equipe de agentes especializados diretamente no Claude C
|
|
|
257
270
|
Seus dados ficam locais em `~/.iron/`. Sobre o que sai da sua máquina, com precisão:
|
|
258
271
|
|
|
259
272
|
- **Para o Iron Office** (`iron-office.vercel.app`, servidor deste projeto), e só se você criar a chave em `~/.claude/iron-office-api-key`:
|
|
260
|
-
-
|
|
273
|
+
- **0 agentes** leem a chave. Só o plugin `iron-office` fala com o site, e a loja Shopify é consultada pelas ferramentas `shop_status`/`shop_query` dele (desde a 2.4.4; o `iron-auditor` devolve o relatório pra quem chamou desde a 2.4.2). Confira: `grep -rln "iron-office-api-key" ~/.claude/agents/`;
|
|
261
274
|
- **se você rodou `npx iron-edge office`**, o plugin `iron-office` fica instalado e manda pro seu painel (feed e mapa 3D) a atividade **só da sessão aberta pelo `npx iron-edge office`** (desde o plugin 0.3.1; um `claude` aberto em outra pasta não manda nada), e as entregas que o Iron salvar ali pela ferramenta `deliver`. Da atividade vai: nome da ferramenta, caminho do arquivo (nunca o conteúdo), os primeiros 200 caracteres de cada comando de terminal e a descrição dele, buscas, o domínio dos endereços visitados, nome e descrição dos agentes, a pasta e o identificador da sessão. A resposta das ferramentas nunca sai. Padrões conhecidos de segredo nesses campos (chaves de Stripe, Shopify, Meta, GitHub, OpenAI, Supabase e AWS, `Bearer`, `ALGO_KEY=`, `--password`, `-u usuario:senha`…) são apagados antes do envio; é uma lista de padrões, não garantia. Lista completa no README do plugin (github.com/filipecoff21-code/iron-office-channel). Desligar: `claude plugin disable iron-office@iron-office-channel`. Conferir se está ligado: `claude plugin list`.
|
|
262
275
|
- **Para a internet aberta**, através das ferramentas do próprio Claude Code (`WebSearch` / `WebFetch`), vão as buscas de **41 dos 44 agentes** — é como o `iron-research` pesquisa mercado e o `iron-spy` lê a biblioteca de anúncios. Isso passa pelo Claude Code, com a visibilidade e a aprovação que ele te dá; não é canal deste pacote. Confira: `grep -rln "WebSearch\|WebFetch" ~/.claude/agents/`.
|
|
263
276
|
- **A skill `clone-site`** baixa a URL que você mandar clonar. É o trabalho dela.
|
|
264
277
|
|
|
265
278
|
|
|
266
|
-
Sendo exato sobre a força dessa garantia, porque ela não é um sandbox:
|
|
279
|
+
Sendo exato sobre a força dessa garantia, porque ela não é um sandbox: nenhum agente nem skill do pacote lê a chave (a regra R25 do lint barra quem voltar a ler), e se algum bloco novo chamar o Iron Office a regra R20 exige a guarda (`[ -z "$IRON_KEY" ] && exit 0`) antes. Mas esses arquivos são **instruções lidas por um modelo**, não um binário fechado: o que segura de verdade é o modelo seguir o texto. Se o que você quer é garantia dura, a que existe é não criar o arquivo de chave — sem ele, não há para onde enviar, e todo bloco para na guarda (o plugin `iron-office` também: sem a chave, ele não envia nada).
|
|
267
280
|
|
|
268
281
|
---
|
|
269
282
|
|
package/agents/iron-auditor.md
CHANGED
|
@@ -22,6 +22,8 @@ Quando ativado com uma URL:
|
|
|
22
22
|
|
|
23
23
|
### STEP 1 — Captura de screenshots
|
|
24
24
|
|
|
25
|
+
O pedido pode trazer `dono=sim` ou `dono=nao` (o site é do aluno ou de terceiro). Sem o campo, considere `dono=sim`. Guarde: ele decide a ação de cada item do cartão (STEP 4).
|
|
26
|
+
|
|
25
27
|
Execute este script Playwright via Bash:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
@@ -114,6 +116,7 @@ O relatório SEMPRE termina com o cartão de ação: o título `## Cartão` segu
|
|
|
114
116
|
```json
|
|
115
117
|
{
|
|
116
118
|
"kind": "auditoria",
|
|
119
|
+
"owner": true,
|
|
117
120
|
"score": 6.5,
|
|
118
121
|
"summary": "a frase do diagnóstico, até 160 caracteres",
|
|
119
122
|
"items": [
|
|
@@ -131,7 +134,11 @@ Regras do cartão:
|
|
|
131
134
|
- `score` = o Score Geral; `summary` = a frase do diagnóstico.
|
|
132
135
|
- `items` = os 3 issues mais altos, no máximo 3 (🔴 antes de 🟠).
|
|
133
136
|
- `why` sempre em dinheiro ou cliente perdido, nunca "melhora a experiência".
|
|
134
|
-
- `
|
|
137
|
+
- `owner` = `true` com `dono=sim`, `false` com `dono=nao`.
|
|
138
|
+
- `action` é o pedido que o aluno manda com um clique, até 300 caracteres. Em site de terceiro ele não pode corrigir, então a ação é a mensagem:
|
|
139
|
+
- `dono=sim` → começa com "Iron, corrige isso em <domínio>:" seguido do problema e do que fazer.
|
|
140
|
+
- `dono=nao` → começa com "Iron, escreve a mensagem pro dono de <domínio> sobre:" seguido do problema e de por que importa.
|
|
141
|
+
- O Iron Office descarta item cuja `action` não começa com um desses dois prefixos.
|
|
135
142
|
|
|
136
143
|
### STEP 5 — Devolver
|
|
137
144
|
|
package/agents/iron-shopify.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: iron-shopify
|
|
3
3
|
description: "Iron Edge — controle total de lojas Shopify. Otimiza produtos, SEO, GEO, imagens, banners, tema, páginas de venda, branding e identidade visual. Gerencia múltiplas lojas com memória persistente. Use para qualquer tarefa relacionada a uma loja Shopify. Puxa em: produto da loja, e-commerce, dropshipping."
|
|
4
4
|
model: sonnet
|
|
5
|
-
tools: [Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, mcp__shopify-dev-mcp__learn_shopify_api, mcp__shopify-dev-mcp__search_docs_chunks, mcp__shopify-dev-mcp__validate_theme, mcp__shopify-dev-mcp__validate_graphql_codeblocks, mcp__shopify-dev-mcp__validate_component_codeblocks]
|
|
5
|
+
tools: [Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, mcp__shopify-dev-mcp__learn_shopify_api, mcp__shopify-dev-mcp__search_docs_chunks, mcp__shopify-dev-mcp__validate_theme, mcp__shopify-dev-mcp__validate_graphql_codeblocks, mcp__shopify-dev-mcp__validate_component_codeblocks, mcp__plugin_iron-office_iron-office__shop_status, mcp__plugin_iron-office_iron-office__shop_query]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# iron-shopify — Controle Total de Lojas Shopify
|
|
@@ -15,33 +15,22 @@ Você é um especialista em Shopify com acesso direto às lojas via Shopify CLI
|
|
|
15
15
|
|
|
16
16
|
### Passo 1 — Verificar conexão via Iron Office
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Chame a ferramenta `shop_status` (plugin `iron-office`, sem argumentos). Você **nunca** lê a chave do Iron Office nem faz requisição HTTP pro site: quem fala com o Iron Office é só o plugin, que já tem a chave.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
cat ~/.claude/iron-office-api-key 2>/dev/null
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Se existir a key** → use o Iron Office como fonte de credenciais (preferencial):
|
|
20
|
+
Se ela devolver `{"connected": true, "shopDomain": "..."}` → loja conectada via Iron Office, use a ferramenta `shop_query` para todas as operações GraphQL (ver seção **USANDO O PROXY IRON OFFICE**).
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
IRON_KEY=$(cat ~/.claude/iron-office-api-key 2>/dev/null | tr -d '[:space:]')
|
|
28
|
-
# GUARDA: sem chave, nenhuma requisicao sai. Sustenta a promessa do README.
|
|
29
|
-
[ -z "$IRON_KEY" ] && { echo "Sem iron-office-api-key — nada foi enviado."; exit 0; }
|
|
30
|
-
curl -s -H "x-api-key: $IRON_KEY" https://iron-office.vercel.app/api/shopify/graphql
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Se retornar `{"connected": true, "shopDomain": "..."}` → loja conectada via Iron Office, use o proxy para todas as operações GraphQL (ver seção **USANDO O PROXY IRON OFFICE**).
|
|
34
|
-
|
|
35
|
-
Se retornar `{"connected": false}` → informe:
|
|
22
|
+
Se devolver "Loja não conectada" → informe:
|
|
36
23
|
```
|
|
37
24
|
🛍️ iron-shopify ativo
|
|
38
25
|
|
|
39
26
|
Sua loja ainda não está conectada.
|
|
40
|
-
Acesse https://iron-office.vercel.app/
|
|
27
|
+
Acesse https://iron-office.vercel.app/integrations e conecte sua loja Shopify.
|
|
41
28
|
Depois me chame novamente.
|
|
42
29
|
```
|
|
43
30
|
|
|
44
|
-
**Se
|
|
31
|
+
**Se a ferramenta `shop_status` não existir nesta sessão** e o usuário usa o Iron Office → peça pra ele rodar `npx iron-edge office` uma vez (instala o plugin), abrir uma sessão nova e te chamar de novo. Pare aqui.
|
|
32
|
+
|
|
33
|
+
**Se o usuário não usa o Iron Office** → fallback para arquivo local:
|
|
45
34
|
|
|
46
35
|
Verifique se existe `~/.iron/shopify-stores.yaml`:
|
|
47
36
|
|
|
@@ -170,47 +159,42 @@ Qual é a missão na [apelido]?
|
|
|
170
159
|
|
|
171
160
|
## USANDO O PROXY IRON OFFICE
|
|
172
161
|
|
|
173
|
-
Quando conectado via Iron Office,
|
|
174
|
-
As credenciais ficam no servidor
|
|
162
|
+
Quando conectado via Iron Office, toda chamada GraphQL passa pela ferramenta `shop_query` do plugin `iron-office`.
|
|
163
|
+
As credenciais ficam no servidor e a chave do Iron Office fica com o plugin: você nunca manuseia nenhuma das duas.
|
|
175
164
|
|
|
176
165
|
**Formato de chamada:**
|
|
177
166
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
167
|
+
```
|
|
168
|
+
shop_query({ query: "{ shop { name email } }" })
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Com variáveis:
|
|
182
172
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}'
|
|
173
|
+
```
|
|
174
|
+
shop_query({
|
|
175
|
+
query: "query($n: Int!) { products(first: $n) { edges { node { id title } } } }",
|
|
176
|
+
variables: { n: 10 }
|
|
177
|
+
})
|
|
189
178
|
```
|
|
190
179
|
|
|
191
180
|
**Exemplos práticos:**
|
|
192
181
|
|
|
193
|
-
```
|
|
194
|
-
# Cada cerca ```bash e' um bloco INDEPENDENTE: o Claude Code nao guarda estado de shell entre
|
|
195
|
-
# execucoes, entao $IRON_KEY definido num bloco anterior chega VAZIO aqui. A guarda se repete
|
|
196
|
-
# em todo bloco que fala com a rede — sem ela, o curl sairia com a chave em branco.
|
|
197
|
-
IRON_KEY=$(cat ~/.claude/iron-office-api-key 2>/dev/null | tr -d '[:space:]')
|
|
198
|
-
[ -z "$IRON_KEY" ] && { echo "Sem iron-office-api-key — nada foi enviado."; exit 0; }
|
|
199
|
-
|
|
182
|
+
```
|
|
200
183
|
# Listar produtos com SEO
|
|
201
|
-
|
|
202
|
-
-H "Content-Type: application/json" \
|
|
203
|
-
-H "x-api-key: $IRON_KEY" \
|
|
204
|
-
-d '{"query":"{ products(first: 50) { edges { node { id title handle seo { title description } } } } }"}'
|
|
184
|
+
shop_query({ query: "{ products(first: 50) { edges { node { id title handle seo { title description } } } } }" })
|
|
205
185
|
|
|
206
186
|
# Informações da loja
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
187
|
+
shop_query({ query: "{ shop { name email myshopifyDomain plan { displayName } } }" })
|
|
188
|
+
|
|
189
|
+
# Atualizar o SEO de um produto
|
|
190
|
+
shop_query({
|
|
191
|
+
query: "mutation($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { field message } } }",
|
|
192
|
+
variables: { input: { id: "gid://shopify/Product/123", seo: { title: "...", description: "..." } } }
|
|
193
|
+
})
|
|
211
194
|
```
|
|
212
195
|
|
|
213
|
-
|
|
196
|
+
A ferramenta devolve o JSON da Shopify como texto; resposta muito grande vem cortada com aviso (peça menos itens por vez, com `first:` menor).
|
|
197
|
+
|
|
214
198
|
|
|
215
199
|
---
|
|
216
200
|
|
package/agents/iron-spy.md
CHANGED
|
@@ -259,6 +259,8 @@ Após a execução:
|
|
|
259
259
|
- Perguntar se o usuário quer ajustar as palavras-chave e rodar novamente
|
|
260
260
|
- Oferecer análise mais profunda de algum anúncio específico
|
|
261
261
|
|
|
262
|
+
Quando quem te chamou é o Iron do Iron Office, não cite o caminho `file://` do dashboard: ele não abre no site. A resposta final leva a tabela dos 10 anúncios mais validados (anunciante · dias no ar · ângulo · gancho) e a análise dos ângulos em markdown, e termina com o `## Cartão`.
|
|
263
|
+
|
|
262
264
|
Além do dashboard, a resposta final termina com o cartão de ação: o título `## Cartão` seguido de um bloco ```json (é o que o Iron Office mostra no chat):
|
|
263
265
|
|
|
264
266
|
```json
|
|
@@ -277,7 +279,7 @@ Além do dashboard, a resposta final termina com o cartão de ação: o título
|
|
|
277
279
|
}
|
|
278
280
|
```
|
|
279
281
|
|
|
280
|
-
Regras do cartão: `items` = os 3 ângulos mais validados, no máximo 3; `action` cabe em 300 caracteres
|
|
282
|
+
Regras do cartão: `items` = os 3 ângulos mais validados, no máximo 3; `action` cabe em 300 caracteres, é o pedido que o aluno manda com um clique e sempre começa com "Iron, escreve uma copy nesse ângulo" (o Iron Office descarta item com outro começo).
|
|
281
283
|
|
|
282
284
|
## Palavras-chave por nicho
|
|
283
285
|
|
package/bin/iron-edge.js
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* office [--safe] liga o Iron Office: instala o plugin de canal e abre o
|
|
18
18
|
* Claude Code conversando com o chat do dashboard
|
|
19
19
|
*
|
|
20
|
+
* doctor [--json] mede a maquina (Node, git, Claude Code, Iron Edge, chave,
|
|
21
|
+
* plugin, cofre, atalho) e, com a chave aceita, manda o resultado
|
|
22
|
+
* pra pagina de Setup do Iron Office
|
|
23
|
+
*
|
|
24
|
+
* install --cofre alem do install, cria ~/Iron Cofre/ (cofre vazio pro Obsidian)
|
|
25
|
+
* install/update --sem-atalho nao cria o alias ironoffice (= office) no ~/.zshrc ou ~/.bashrc
|
|
26
|
+
*
|
|
20
27
|
* Nao existe 'uninstall' automatico: remover continua sendo operacao conduzida, com o
|
|
21
28
|
* manifesto na mao (o README descreve o procedimento).
|
|
22
29
|
*/
|
|
@@ -155,7 +162,12 @@ if (cmd === 'install') {
|
|
|
155
162
|
const r = spawnSync(process.execPath, [path.join(__dirname, '..', 'installer', 'office.js'), ...rest], { stdio: 'inherit' });
|
|
156
163
|
process.exit(r.status ?? 1);
|
|
157
164
|
|
|
165
|
+
} else if (cmd === 'doctor') {
|
|
166
|
+
// mede a maquina e acende o checklist do Setup no site (installer/doctor.js)
|
|
167
|
+
const r = spawnSync(process.execPath, [path.join(__dirname, '..', 'installer', 'doctor.js'), ...rest], { stdio: 'inherit' });
|
|
168
|
+
process.exit(r.status ?? 1);
|
|
169
|
+
|
|
158
170
|
} else {
|
|
159
|
-
console.log('uso: iron-edge <install|update|verify|office> [--dry-run] [--source <dir>] [--safe]');
|
|
171
|
+
console.log('uso: iron-edge <install|update|verify|office|doctor> [--dry-run] [--source <dir>] [--cofre] [--safe] [--json]');
|
|
160
172
|
process.exit(cmd && cmd !== '--help' && cmd !== '-h' ? 1 : 0);
|
|
161
173
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* atalho.js — cria o comando curto `ironoffice` (= `npx iron-edge office`) no shell do aluno.
|
|
4
|
+
* Roda no fim do install e do update com sucesso; `--sem-atalho` pula.
|
|
5
|
+
*
|
|
6
|
+
* Regras:
|
|
7
|
+
* - Windows: nao faz nada (o PowerShell nao le .zshrc/.bashrc). O aluno usa `npx iron-edge office`.
|
|
8
|
+
* - escolhe ~/.zshrc se existir, senao ~/.bashrc se existir, senao cria ~/.zshrc (padrao do Mac).
|
|
9
|
+
* - se o arquivo ja tem `alias ironoffice=`, nao mexe: nunca duplica.
|
|
10
|
+
* - so' ACRESCENTA no fim. Nunca remove nem edita linha existente: o arquivo e' do aluno.
|
|
11
|
+
* - escrita atomica: grava ao lado e renomeia por cima. Se o .zshrc e' um link (dotfiles),
|
|
12
|
+
* escreve no arquivo de verdade e o link continua link. Link quebrado: nao mexe.
|
|
13
|
+
* - os bytes do aluno ficam intactos: le e grava em Buffer (um .zshrc em Latin-1 nao e' recodificado).
|
|
14
|
+
* - arquivo que existe e nao da pra ler (permissao, pasta no lugar): nao escreve nada. So' "nao existe"
|
|
15
|
+
* (ENOENT) vale como arquivo novo. Antes, qualquer erro de leitura virava "vazio" e o rename
|
|
16
|
+
* apagava o .zshrc do aluno (gate da onda 9).
|
|
17
|
+
* - dryRun so' diz o que faria.
|
|
18
|
+
*
|
|
19
|
+
* Saida so' em ASCII, como o resto do instalador.
|
|
20
|
+
*/
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
const LINHA = 'alias ironoffice="npx iron-edge office"';
|
|
26
|
+
const BLOCO = '\n# Iron Edge: atalho do canal do Iron Office\n' + LINHA + '\n';
|
|
27
|
+
const JA_TEM = /^\s*alias\s+ironoffice=/m;
|
|
28
|
+
|
|
29
|
+
function arquivoDoShell(home) {
|
|
30
|
+
for (const nome of ['.zshrc', '.bashrc']) {
|
|
31
|
+
const p = path.join(home, nome);
|
|
32
|
+
if (fs.existsSync(p)) return p;
|
|
33
|
+
}
|
|
34
|
+
return path.join(home, '.zshrc');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Devolve { criado: caminho } | { criaria: caminho }
|
|
38
|
+
// | { pulado: 'windows' | 'ja existe' | 'sem-atalho' | 'sem leitura' | 'link quebrado', arquivo? }
|
|
39
|
+
function criarAtalho(opts) {
|
|
40
|
+
const o = opts || {};
|
|
41
|
+
const platform = o.platform || process.platform;
|
|
42
|
+
if (o.semAtalho) return { pulado: 'sem-atalho' };
|
|
43
|
+
if (platform === 'win32') return { pulado: 'windows' };
|
|
44
|
+
const home = o.HOME || os.homedir();
|
|
45
|
+
const rc = arquivoDoShell(home);
|
|
46
|
+
let link = false;
|
|
47
|
+
try { link = fs.lstatSync(rc).isSymbolicLink(); } catch (_) { /* nao existe */ }
|
|
48
|
+
let real = rc;
|
|
49
|
+
if (link) {
|
|
50
|
+
try { real = fs.realpathSync.native(rc); } catch (_) { return { pulado: 'link quebrado', arquivo: rc }; }
|
|
51
|
+
}
|
|
52
|
+
let atual = Buffer.alloc(0);
|
|
53
|
+
try { atual = fs.readFileSync(real); } catch (e) {
|
|
54
|
+
if (!e || e.code !== 'ENOENT') return { pulado: 'sem leitura', arquivo: rc };
|
|
55
|
+
}
|
|
56
|
+
// latin1 mapeia byte a byte: acha o alias (ASCII) em qualquer codificacao sem estourar
|
|
57
|
+
if (JA_TEM.test(atual.toString('latin1'))) return { pulado: 'ja existe', arquivo: rc };
|
|
58
|
+
if (o.dryRun) return { criaria: rc };
|
|
59
|
+
let modo = 0o644;
|
|
60
|
+
try { modo = fs.statSync(real).mode & 0o777; } catch (_) {}
|
|
61
|
+
const tmp = real + '.iron-tmp-' + process.pid;
|
|
62
|
+
fs.writeFileSync(tmp, Buffer.concat([atual, Buffer.from(BLOCO)]), { mode: modo });
|
|
63
|
+
try { fs.renameSync(tmp, real); } catch (e) { try { fs.unlinkSync(tmp); } catch (_) {} throw e; }
|
|
64
|
+
return { criado: rc };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Linha do relatorio do install/update
|
|
68
|
+
function relatarAtalho(r) {
|
|
69
|
+
if (r.criado) return 'Atalho criado: ironoffice em ' + r.criado + ' (abra um terminal novo)';
|
|
70
|
+
if (r.criaria) return 'criaria o atalho ironoffice em ' + r.criaria;
|
|
71
|
+
if (r.pulado === 'ja existe') return 'Atalho ja existia: ironoffice';
|
|
72
|
+
if (r.pulado === 'windows') return 'Windows: use npx iron-edge office';
|
|
73
|
+
if (r.pulado === 'sem leitura') return 'Atalho nao criado: nao consegui ler ' + r.arquivo + ' (nada foi mexido). Use npx iron-edge office';
|
|
74
|
+
if (r.pulado === 'link quebrado') return 'Atalho nao criado: ' + r.arquivo + ' aponta pra um arquivo que nao existe (nada foi mexido). Use npx iron-edge office';
|
|
75
|
+
return 'Atalho pulado (--sem-atalho): use npx iron-edge office';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Rodado no fim do install/update: nunca derruba a instalacao que ja deu certo.
|
|
79
|
+
function atalhoNoFim(opts) {
|
|
80
|
+
try {
|
|
81
|
+
return relatarAtalho(criarAtalho(opts));
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return 'Atalho nao criado (' + (e.code || e.message) + '): use npx iron-edge office';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { criarAtalho, relatarAtalho, atalhoNoFim, LINHA };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CLAUDE.md — regras do cofre
|
|
2
|
+
|
|
3
|
+
Este cofre é a memória do Iron. São arquivos `.md` no seu computador: nada sai daqui.
|
|
4
|
+
Você pode abrir a pasta no Obsidian ou em qualquer editor de texto.
|
|
5
|
+
|
|
6
|
+
## Como o cofre é organizado
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Iron Cofre/
|
|
10
|
+
CLAUDE.md ← estas regras
|
|
11
|
+
INDICE.md ← o mapa: um link pra cada projeto
|
|
12
|
+
memoria/ ← o diário das sessões (o Iron cria no primeiro "salvar")
|
|
13
|
+
Projetos/ ← uma pasta por projeto
|
|
14
|
+
templates/nota.md ← o modelo de nota
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Cada projeto** vira uma pasta: `Projetos/<Nome>/PROJETO.md` é a nota principal (o hub), com o estado, as decisões e os próximos passos.
|
|
18
|
+
- **Projeto com várias partes** ganha uma nota por parte (as folhas) na mesma pasta. As folhas apontam só pro hub; o hub aponta pras folhas e pro `INDICE.md`.
|
|
19
|
+
- Toda nota começa com o cabeçalho do `templates/nota.md`: `projeto`, `tipo`, `status` (ativo, pausado ou concluído) e `atualizado` (a data).
|
|
20
|
+
|
|
21
|
+
## As regras
|
|
22
|
+
|
|
23
|
+
1. Antes de trabalhar num projeto, leia a nota dele em `Projetos/` pra saber em que pé ele está. Não assuma o estado pela memória da conversa.
|
|
24
|
+
2. Quando o usuário disser "salvar" ou "encerrar", registre a sessão sem perguntar nada.
|
|
25
|
+
3. Diário e nota de projeto são coisas diferentes. O diário responde "o que fizemos hoje"; a nota responde "como o projeto está hoje". Conversa, ideia sem decisão e teste que não concluiu ficam só no diário.
|
|
26
|
+
4. Data sempre absoluta (AAAA-MM-DD). Nunca "ontem", "semana passada" nem "amanhã": daqui a dois meses isso não significa mais nada.
|
|
27
|
+
5. Nunca invente número, nome, versão ou caminho. O que faltar entra como "(a preencher)".
|
|
28
|
+
6. Projeto de cliente pode ficar aqui (o que foi feito, o que foi decidido, o que falta). O que não fica: material confidencial de cliente ou empregador (contrato, planilha interna, dado de cliente), senha, chave nem dado pessoal de quem não autorizou. Se aparecer na conversa, não registre e avise que não registrou.
|
|
29
|
+
|
|
30
|
+
## O que salvar e o que não salvar
|
|
31
|
+
|
|
32
|
+
- **Salvar:** o que foi construído e onde está, decisões importantes, status, próximos passos, o que está esperando outra pessoa.
|
|
33
|
+
- **Não salvar:** detalhe técnico que já está no código, conversa genérica, teste que não foi pra frente.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Índice do cofre
|
|
2
|
+
|
|
3
|
+
O mapa da sua memória. Cada projeto novo ganha uma linha aqui.
|
|
4
|
+
|
|
5
|
+
## Projetos
|
|
6
|
+
|
|
7
|
+
| Projeto | Status | Nota |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| (nenhum ainda) | | |
|
|
10
|
+
|
|
11
|
+
Quando começar um projeto, conte ao Iron o que é: ele oferece criar a pasta do projeto e a linha aqui. No fim da conversa, diga "salvar".
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cofre.js — `npx iron-edge install --cofre`: cria um cofre vazio em ~/Iron Cofre/, pronto pro
|
|
4
|
+
* Obsidian. Roda no fim do install com sucesso (ou sozinho, pra teste).
|
|
5
|
+
*
|
|
6
|
+
* Regras:
|
|
7
|
+
* - copia os 4 itens de installer/cofre/ SO' se nao existirem. Nunca sobrescreve: o cofre e' do
|
|
8
|
+
* aluno, e um segundo `install --cofre` nao pode apagar o que ele ja escreveu.
|
|
9
|
+
* - fica FORA do manifesto do pacote (o verify e o update nao encostam nele).
|
|
10
|
+
* - grava o caminho no `vault_path` do ~/.iron/config.yaml so' quando nenhum cofre esta apontado. Sem isso
|
|
11
|
+
* o Iron nao enxerga o cofre e oferece criar outro. Caminho com barra normal (regra da skill
|
|
12
|
+
* salvar, secao 0) e lido de volta antes de declarar.
|
|
13
|
+
* - --dry-run so' lista.
|
|
14
|
+
*
|
|
15
|
+
* Saida so' em ASCII, como o resto do instalador.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
const MODELO = path.join(__dirname, 'cofre');
|
|
22
|
+
const ITENS = ['CLAUDE.md', 'INDICE.md', path.join('templates', 'nota.md'), path.join('Projetos', '.gitkeep')];
|
|
23
|
+
const NOME = 'Iron Cofre';
|
|
24
|
+
|
|
25
|
+
function log(msg) { try { fs.writeSync(1, msg + '\n'); } catch (_) { process.stdout.write(msg + '\n'); } }
|
|
26
|
+
|
|
27
|
+
function homeReal(home) {
|
|
28
|
+
let h = home || os.homedir();
|
|
29
|
+
try { h = fs.realpathSync.native(h); } catch (_) {}
|
|
30
|
+
return h;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function barraNormal(p) { return p.split(path.sep).join('/'); }
|
|
34
|
+
|
|
35
|
+
// Grava vault_path no config SO' se ninguem apontou um cofre ainda. Devolve
|
|
36
|
+
// 'gravado' | 'ja-tinha' | 'sem-config' | 'falhou'.
|
|
37
|
+
// "Ninguem apontou" = vault_path ausente, vazio, null ou ~ E sem obsidian_vault_path preenchido
|
|
38
|
+
// (o nome antigo do campo, que o Iron tambem le). Ausente: acrescenta a linha no fim.
|
|
39
|
+
function gravarVaultPath(home, cofre) {
|
|
40
|
+
const cfg = path.join(home, '.iron', 'config.yaml');
|
|
41
|
+
let txt;
|
|
42
|
+
try { txt = fs.readFileSync(cfg, 'utf8'); } catch (_) { return 'sem-config'; }
|
|
43
|
+
const VAZIO = /^\s*(""|''|null|~)?\s*(#.*)?$/;
|
|
44
|
+
const campo = (nome) => {
|
|
45
|
+
const m = txt.match(new RegExp('^' + nome + ':(.*)$', 'm'));
|
|
46
|
+
return m ? { linha: m[0], valor: m[1] } : null;
|
|
47
|
+
};
|
|
48
|
+
const antigo = campo('obsidian_vault_path');
|
|
49
|
+
if (antigo && !VAZIO.test(antigo.valor)) return 'ja-tinha';
|
|
50
|
+
const atual = campo('vault_path');
|
|
51
|
+
if (atual && !VAZIO.test(atual.valor)) return 'ja-tinha';
|
|
52
|
+
const linha = 'vault_path: "' + barraNormal(cofre) + '"';
|
|
53
|
+
const novo = atual
|
|
54
|
+
? txt.replace(atual.linha, linha)
|
|
55
|
+
: txt + (txt === '' || txt.endsWith('\n') ? '' : '\n') + linha + '\n';
|
|
56
|
+
fs.writeFileSync(cfg, novo);
|
|
57
|
+
const relido = fs.readFileSync(cfg, 'utf8').split(/\r?\n/).find(l => l.startsWith('vault_path:'));
|
|
58
|
+
return relido === linha ? 'gravado' : 'falhou';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function criarCofre(opts) {
|
|
62
|
+
const o = opts || {};
|
|
63
|
+
const home = homeReal(o.home);
|
|
64
|
+
const destino = path.join(home, NOME);
|
|
65
|
+
const criados = [], pulados = [];
|
|
66
|
+
for (const rel of ITENS) {
|
|
67
|
+
const dest = path.join(destino, rel);
|
|
68
|
+
if (fs.existsSync(dest)) { pulados.push(dest); continue; }
|
|
69
|
+
criados.push(dest);
|
|
70
|
+
if (o.dryRun) continue;
|
|
71
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
72
|
+
fs.copyFileSync(path.join(MODELO, rel), dest);
|
|
73
|
+
}
|
|
74
|
+
log('');
|
|
75
|
+
log('--- cofre (' + destino + ') ---');
|
|
76
|
+
for (const c of criados) log((o.dryRun ? ' criaria: ' : ' criado: ') + c);
|
|
77
|
+
for (const p of pulados) log(' ja existe, nao mexi: ' + p);
|
|
78
|
+
let vault = 'dry-run';
|
|
79
|
+
if (!o.dryRun) {
|
|
80
|
+
vault = gravarVaultPath(home, destino);
|
|
81
|
+
if (vault === 'gravado') log(' vault_path gravado no ~/.iron/config.yaml: o Iron ja sabe onde o cofre esta');
|
|
82
|
+
else if (vault === 'ja-tinha') log(' o ~/.iron/config.yaml ja aponta pra um cofre: nao troquei. Pra usar este, diga ao Iron: "usa o Iron Cofre como meu cofre"');
|
|
83
|
+
else if (vault === 'sem-config') log(' sem ~/.iron/config.yaml: diga ao Iron "usa o Iron Cofre como meu cofre"');
|
|
84
|
+
else log(' ATENCAO: o vault_path nao ficou gravado certo. Diga ao Iron: "conserta meu vault_path"');
|
|
85
|
+
log(' Abra a pasta no Obsidian: "Open folder as vault" > ' + destino);
|
|
86
|
+
}
|
|
87
|
+
return { destino, criados, pulados, vault };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { criarCofre, ITENS, NOME };
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
criarCofre({ dryRun: process.argv.includes('--dry-run') });
|
|
94
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* doctor.js — `npx iron-edge doctor`: mede a maquina do aluno e manda o resultado pro Iron Office,
|
|
4
|
+
* pra a pagina de Setup acender cada passo sozinha (o site nao enxerga a maquina).
|
|
5
|
+
*
|
|
6
|
+
* Mede: node (>= 20), git, Claude Code, Iron Edge instalado pelo npm (manifesto), chave do Iron
|
|
7
|
+
* Office (formato + confere com o site), plugin iron-office, cofre (~/Iron Cofre, informativo),
|
|
8
|
+
* atalho ironoffice no .zshrc/.bashrc (informativo).
|
|
9
|
+
*
|
|
10
|
+
* Envio: SO' com a chave aceita pelo site (ping = ok) faz POST /api/chat/setup-status. Sem chave,
|
|
11
|
+
* com chave recusada ou sem rede, o resultado fica so' no terminal. Falhar o envio e' aviso, nao erro.
|
|
12
|
+
* Nao grava nada em disco.
|
|
13
|
+
*
|
|
14
|
+
* Saida: uma linha por item, sem acento (console do Windows antigo). --json imprime o JSON cru
|
|
15
|
+
* (o envio acontece igual).
|
|
16
|
+
* Exit 0 so' se node, git, claude e iron_edge estao ok (chave e plugin sao passos do site).
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
const { readKey, keyFileExists, ping, resolveClaude, PLUGIN } = require('./office.js');
|
|
23
|
+
|
|
24
|
+
const OFFICE_URL = (process.env.IRON_OFFICE_URL || 'https://iron-office.vercel.app').replace(/\/+$/, '');
|
|
25
|
+
const IS_WIN = process.platform === 'win32';
|
|
26
|
+
const POST_TIMEOUT_MS = 15000;
|
|
27
|
+
let VERSION = '';
|
|
28
|
+
try { VERSION = require('../package.json').version || ''; } catch (_) {}
|
|
29
|
+
|
|
30
|
+
function cut(s, n) { s = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '~' : s; }
|
|
31
|
+
function item(ok, detail) { return { ok: !!ok, detail: cut(detail, 120) }; }
|
|
32
|
+
// O detalhe vai COM acento pro site (o aluno le' la'); no terminal sai sem acento (console do
|
|
33
|
+
// Windows antigo mostra lixo em acento). Uma fonte so', duas saidas.
|
|
34
|
+
function ascii(s) { return String(s).normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }
|
|
35
|
+
|
|
36
|
+
function run(bin, args) {
|
|
37
|
+
return spawnSync(bin, args, { encoding: 'utf8', shell: IS_WIN, timeout: 20000 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function homeDir() {
|
|
41
|
+
let h = os.homedir();
|
|
42
|
+
try { h = fs.realpathSync.native(h); } catch (_) {}
|
|
43
|
+
return h;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkNode() {
|
|
47
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
48
|
+
return item(major >= 20, major >= 20 ? process.version : process.version + ': precisa da 20 ou maior (nodejs.org)');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkGit() {
|
|
52
|
+
const r = run('git', ['--version']);
|
|
53
|
+
if (r.error || r.status !== 0) return item(false, 'git não encontrado: instale em git-scm.com');
|
|
54
|
+
return item(true, (r.stdout || '').trim());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkClaude() {
|
|
58
|
+
const bin = resolveClaude();
|
|
59
|
+
if (!bin) return { bin: null, check: item(false, 'Claude Code não encontrado: instale em claude.com/claude-code') };
|
|
60
|
+
const r = run(bin, ['--version']);
|
|
61
|
+
return { bin, check: item(true, ((r.stdout || '').trim().split('\n')[0]) || 'instalado') };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkIronEdge(home) {
|
|
65
|
+
const man = path.join(home, '.iron', 'install-manifest.json');
|
|
66
|
+
if (!fs.existsSync(man)) return item(false, 'não instalado pelo npm: rode npx iron-edge install');
|
|
67
|
+
try {
|
|
68
|
+
const m = JSON.parse(fs.readFileSync(man, 'utf8'));
|
|
69
|
+
return item(true, m.version || m.label || 'instalado');
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return item(false, 'manifesto ilegível: rode npx iron-edge install');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function checkKey(home, baseUrl) {
|
|
76
|
+
const key = readKey(home);
|
|
77
|
+
if (!key) {
|
|
78
|
+
return { key: '', check: item(false, keyFileExists(home) ? 'formato errado: refaça o passo 2 no site' : 'sem chave ainda: faça o passo 2 no site') };
|
|
79
|
+
}
|
|
80
|
+
const v = await ping(key, baseUrl);
|
|
81
|
+
if (v === 'ok') return { key, check: item(true, 'aceita pelo Iron Office') };
|
|
82
|
+
if (v === 'rejected') return { key: '', check: item(false, 'recusada: a chave foi trocada, refaça o passo 2') };
|
|
83
|
+
return { key: '', check: item(false, 'sem rede: não deu pra conferir com o site') };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkPlugin(claudeBin) {
|
|
87
|
+
if (!claudeBin) return item(false, 'sem Claude Code');
|
|
88
|
+
const r = run(claudeBin, ['plugin', 'list']);
|
|
89
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
90
|
+
if (r.error || r.status !== 0) return item(false, 'não deu pra listar os plugins');
|
|
91
|
+
return out.includes(PLUGIN) ? item(true, PLUGIN) : item(false, 'falta o plugin: rode npx iron-edge office');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// alias ironoffice (installer/atalho.js): no Windows nao existe, entao conta como ok
|
|
95
|
+
function checkAtalho(home) {
|
|
96
|
+
if (IS_WIN) return item(true, 'não se aplica (Windows: npx iron-edge office)');
|
|
97
|
+
for (const nome of ['.zshrc', '.bashrc']) {
|
|
98
|
+
let txt = '';
|
|
99
|
+
try { txt = fs.readFileSync(path.join(home, nome), 'utf8'); } catch (_) { continue; }
|
|
100
|
+
if (/^\s*alias\s+ironoffice=/m.test(txt)) return item(true, 'ironoffice em ~/' + nome);
|
|
101
|
+
}
|
|
102
|
+
return item(false, 'ausente: rode npx iron-edge update');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkCofre(home) {
|
|
106
|
+
const ok = fs.existsSync(path.join(home, 'Iron Cofre', 'CLAUDE.md'));
|
|
107
|
+
return item(ok, ok ? '~/Iron Cofre' : 'sem cofre pronto (opcional: npx iron-edge install --cofre)');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function medir(opts) {
|
|
111
|
+
const o = opts || {};
|
|
112
|
+
const home = o.home || homeDir();
|
|
113
|
+
const claude = checkClaude();
|
|
114
|
+
const key = await checkKey(home, o.baseUrl);
|
|
115
|
+
const result = {
|
|
116
|
+
os: cut(process.platform + '-' + process.arch, 20),
|
|
117
|
+
doctor: cut(VERSION, 20),
|
|
118
|
+
checks: {
|
|
119
|
+
node: checkNode(),
|
|
120
|
+
git: checkGit(),
|
|
121
|
+
claude: claude.check,
|
|
122
|
+
iron_edge: checkIronEdge(home),
|
|
123
|
+
key: key.check,
|
|
124
|
+
plugin: checkPlugin(claude.bin),
|
|
125
|
+
cofre: checkCofre(home),
|
|
126
|
+
atalho: checkAtalho(home),
|
|
127
|
+
},
|
|
128
|
+
ran_at: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
return { result, key: key.key };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Devolve 'enviado' | 'recusado' (o site respondeu e nao gravou) | 'falhou' (rede).
|
|
134
|
+
async function enviar(result, key, baseUrl) {
|
|
135
|
+
const url = (baseUrl || OFFICE_URL).replace(/\/+$/, '') + '/api/chat/setup-status';
|
|
136
|
+
if (typeof fetch !== 'function') return 'falhou';
|
|
137
|
+
const ctl = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => ctl.abort(), POST_TIMEOUT_MS);
|
|
139
|
+
try {
|
|
140
|
+
const r = await fetch(url, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'content-type': 'application/json', 'x-api-key': key },
|
|
143
|
+
body: JSON.stringify(result),
|
|
144
|
+
signal: ctl.signal,
|
|
145
|
+
});
|
|
146
|
+
return r.ok ? 'enviado' : 'recusado';
|
|
147
|
+
} catch (_) {
|
|
148
|
+
return 'falhou';
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ROTULO = { node: 'Node', git: 'git', claude: 'Claude Code', iron_edge: 'Iron Edge', key: 'chave', plugin: 'plugin', cofre: 'cofre', atalho: 'atalho' };
|
|
155
|
+
const OBRIGATORIOS = ['node', 'git', 'claude', 'iron_edge'];
|
|
156
|
+
|
|
157
|
+
async function main(argv, opts) {
|
|
158
|
+
const o = opts || {};
|
|
159
|
+
const { result, key } = await medir(o);
|
|
160
|
+
const exit = OBRIGATORIOS.every(k => result.checks[k].ok) ? 0 : 1;
|
|
161
|
+
if (argv.includes('--json')) {
|
|
162
|
+
// mesmo envio, saida so' o JSON (pra teste e pra script)
|
|
163
|
+
if (key) await enviar(result, key, o.baseUrl);
|
|
164
|
+
console.log(JSON.stringify(result, null, 2));
|
|
165
|
+
return exit;
|
|
166
|
+
}
|
|
167
|
+
console.log('Iron Edge doctor ' + result.doctor);
|
|
168
|
+
for (const [k, c] of Object.entries(result.checks)) {
|
|
169
|
+
console.log(' ' + (c.ok ? '[ok] ' : (k === 'cofre' || k === 'atalho' ? '[info] ' : '[falta] ')) + ROTULO[k] + ': ' + ascii(c.detail));
|
|
170
|
+
}
|
|
171
|
+
console.log('');
|
|
172
|
+
if (!key && !readKey(o.home || homeDir())) {
|
|
173
|
+
console.log("sem chave ainda: o resultado fica so' aqui no terminal; faca o passo 2 no site.");
|
|
174
|
+
} else if (!key) {
|
|
175
|
+
console.log("o resultado fica so' aqui no terminal (chave: " + ascii(result.checks.key.detail) + ').');
|
|
176
|
+
} else {
|
|
177
|
+
const env = await enviar(result, key, o.baseUrl);
|
|
178
|
+
if (env === 'enviado') console.log('resultado enviado: confira em ' + OFFICE_URL + '/onboarding');
|
|
179
|
+
else if (env === 'recusado') console.log('[aviso] o site recusou o resultado (chave trocada?). O que esta acima vale; refaca o passo 2 e rode de novo.');
|
|
180
|
+
else console.log('[aviso] nao deu pra mandar o resultado pro site (sem rede?). O que esta acima vale.');
|
|
181
|
+
}
|
|
182
|
+
return exit;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { medir, enviar, main };
|
|
186
|
+
|
|
187
|
+
if (require.main === module) {
|
|
188
|
+
main(process.argv.slice(2)).then(code => process.exit(code), err => {
|
|
189
|
+
console.error('Erro inesperado: ' + (err && err.message ? err.message : err));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
|
192
|
+
}
|
package/installer/install.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - Manifesto com SHA-256 (gerados marcados generated:true).
|
|
17
17
|
* - shopify.yaml nasce desativado. config em branco (sem PII).
|
|
18
18
|
*
|
|
19
|
-
* Uso: node install.js [--dry-run] [--source <dir>]
|
|
19
|
+
* Uso: node install.js [--dry-run] [--source <dir>] [--cofre] [--sem-atalho]
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
'use strict';
|
|
@@ -41,6 +41,8 @@ function abort(msg) {
|
|
|
41
41
|
|
|
42
42
|
const args = process.argv.slice(2);
|
|
43
43
|
const DRY_RUN = args.includes('--dry-run');
|
|
44
|
+
const COFRE = args.includes('--cofre'); // cria ~/Iron Cofre/ no fim (installer/cofre.js)
|
|
45
|
+
const SEM_ATALHO = args.includes('--sem-atalho'); // nao cria o alias ironoffice (installer/atalho.js)
|
|
44
46
|
const srcIdx = args.indexOf('--source');
|
|
45
47
|
let SOURCE = srcIdx >= 0 && args[srcIdx + 1] ? path.resolve(args[srcIdx + 1]) : __dirname;
|
|
46
48
|
// Opera sobre o caminho FISICO: ancestral symlink herdado da maquina (perfil
|
|
@@ -335,6 +337,9 @@ if (DRY_RUN) {
|
|
|
335
337
|
if (payloadPlan.length > 10) log(` ... e mais ${payloadPlan.length - 10} arquivos`);
|
|
336
338
|
log('\n--- arquivos que SERAO gerados ---');
|
|
337
339
|
for (const g of GENERATED) log(' ' + g.dest);
|
|
340
|
+
if (COFRE) require('./cofre').criarCofre({ home: HOME, dryRun: true });
|
|
341
|
+
log('\n--- atalho ---');
|
|
342
|
+
log(' ' + require('./atalho').atalhoNoFim({ HOME, dryRun: true, semAtalho: SEM_ATALHO }));
|
|
338
343
|
log('\nDRY-RUN concluido. Nada foi copiado.');
|
|
339
344
|
process.exit(0);
|
|
340
345
|
}
|
|
@@ -550,6 +555,7 @@ log(`Agentes : ${AGENTS_DEST}`);
|
|
|
550
555
|
log(`Skills : ${SKILLS_DEST}`);
|
|
551
556
|
log(`Nucleo : ${IRON_DIR}`);
|
|
552
557
|
log(`Manifesto: ${manifestPath}`);
|
|
558
|
+
log(require('./atalho').atalhoNoFim({ HOME, semAtalho: SEM_ATALHO }));
|
|
553
559
|
log('\n--------------------------------');
|
|
554
560
|
log(' PROXIMO PASSO');
|
|
555
561
|
log('--------------------------------');
|
|
@@ -569,13 +575,19 @@ log(' Os outros 43 agentes se chamam PEDINDO ("usa o iron-copy").');
|
|
|
569
575
|
const nSkills = (() => { try { return fs.readdirSync(path.join(SOURCE, 'skills')).filter((d) => !d.startsWith('.')).length; } catch (_) { return null; } })();
|
|
570
576
|
log(` As ${nSkills || ''} SKILLS sao de barra: /gate /salvar /brandkit /escada ...`.replace(' ', ' '));
|
|
571
577
|
log('');
|
|
572
|
-
|
|
573
|
-
log('');
|
|
574
|
-
|
|
575
|
-
log('');
|
|
576
|
-
|
|
577
|
-
log('
|
|
578
|
-
log('
|
|
579
|
-
log('
|
|
580
|
-
log('
|
|
581
|
-
log('');
|
|
578
|
+
if (COFRE) {
|
|
579
|
+
log('3. Memoria: o cofre (detalhes abaixo).');
|
|
580
|
+
require('./cofre').criarCofre({ home: HOME });
|
|
581
|
+
log('');
|
|
582
|
+
} else {
|
|
583
|
+
log('3. Ligue a memoria. Diga a ele:');
|
|
584
|
+
log('');
|
|
585
|
+
log(' cria meu cofre');
|
|
586
|
+
log('');
|
|
587
|
+
log(' O cofre e uma pasta de arquivos .md na SUA maquina, onde o Iron guarda quem');
|
|
588
|
+
log(' voce e, seus projetos e o que ficou pendente. SEM ele, cada conversa comeca');
|
|
589
|
+
log(' do zero. Com ele, o Iron abre a sessao ja sabendo em que pe as coisas estao.');
|
|
590
|
+
log(' Nada e enviado pra lugar nenhum: e arquivo seu, no seu disco.');
|
|
591
|
+
log(' Se preferir fazer depois, e so pedir a qualquer momento.');
|
|
592
|
+
log('');
|
|
593
|
+
}
|
package/installer/office.js
CHANGED
|
@@ -37,7 +37,7 @@ function isValidKey(key) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Le o arquivo da chave e devolve so' uma chave no formato certo; qualquer outra coisa vira ''.
|
|
40
|
-
// O `echo` do passo
|
|
40
|
+
// O `echo` do passo 2 no Windows pode gravar UTF-16 com BOM: normaliza antes de decidir.
|
|
41
41
|
function readKey(home) {
|
|
42
42
|
try {
|
|
43
43
|
const buf = fs.readFileSync(keyPath(home));
|
|
@@ -108,9 +108,9 @@ async function main(argv) {
|
|
|
108
108
|
if (!key) {
|
|
109
109
|
if (keyFileExists()) {
|
|
110
110
|
console.error('A chave em ' + keyPath() + ' esta invalida (nao tem o formato iok_...).');
|
|
111
|
-
console.error('Refaca o passo
|
|
111
|
+
console.error('Refaca o passo 2 (Ligar o Iron) em ' + ONBOARDING);
|
|
112
112
|
} else {
|
|
113
|
-
console.error('Falta a chave do Iron Office. Faca o passo
|
|
113
|
+
console.error('Falta a chave do Iron Office. Faca o passo 2 (Ligar o Iron) em ' + ONBOARDING);
|
|
114
114
|
}
|
|
115
115
|
return 1;
|
|
116
116
|
}
|
|
@@ -124,7 +124,7 @@ async function main(argv) {
|
|
|
124
124
|
console.log('Iron Office - preparando o canal');
|
|
125
125
|
const verdict = await ping(key);
|
|
126
126
|
if (verdict === 'rejected') {
|
|
127
|
-
console.error(' [falhou] chave recusada pelo Iron Office: ela foi trocada. Refaca o passo
|
|
127
|
+
console.error(' [falhou] chave recusada pelo Iron Office: ela foi trocada. Refaca o passo 2 (Ligar o Iron) em ' + ONBOARDING);
|
|
128
128
|
return 1;
|
|
129
129
|
}
|
|
130
130
|
if (verdict === 'ok') console.log(' [ok] chave aceita pelo Iron Office');
|
|
@@ -163,7 +163,7 @@ async function main(argv) {
|
|
|
163
163
|
return r.status ?? 1;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
module.exports = { buildClaudeArgs, keyPath, readKey, isValidKey, ping, CHANNEL, PLUGIN, MARKETPLACE_REPO, KEY_RE };
|
|
166
|
+
module.exports = { buildClaudeArgs, keyPath, readKey, keyFileExists, isValidKey, ping, resolveClaude, CHANNEL, PLUGIN, MARKETPLACE_REPO, KEY_RE };
|
|
167
167
|
|
|
168
168
|
if (require.main === module) {
|
|
169
169
|
main(process.argv.slice(2)).then(code => process.exit(code), err => {
|
package/installer/update.js
CHANGED
|
@@ -28,6 +28,8 @@ const MANIFEST = path.join(IRON_DIR, 'install-manifest.json');
|
|
|
28
28
|
|
|
29
29
|
const args = process.argv.slice(2);
|
|
30
30
|
const DRY = args.includes('--dry-run');
|
|
31
|
+
const SEM_ATALHO = args.includes('--sem-atalho'); // nao cria o alias ironoffice (installer/atalho.js)
|
|
32
|
+
const atalho = (dryRun) => require('./atalho').atalhoNoFim({ HOME, dryRun, semAtalho: SEM_ATALHO });
|
|
31
33
|
const srcIdx = args.indexOf('--source');
|
|
32
34
|
const SOURCE = srcIdx >= 0 && args[srcIdx + 1] ? path.resolve(args[srcIdx + 1]) : path.join(__dirname, '..');
|
|
33
35
|
|
|
@@ -292,7 +294,7 @@ log('');
|
|
|
292
294
|
relatarConfig();
|
|
293
295
|
|
|
294
296
|
|
|
295
|
-
if (DRY) { log(''); log('--dry-run: nada foi escrito.'); process.exit(0); }
|
|
297
|
+
if (DRY) { log(''); log(atalho(true)); log('--dry-run: nada foi escrito.'); process.exit(0); }
|
|
296
298
|
|
|
297
299
|
// Permissao da pasta de segredos, TAMBEM no update. O chmod entrou no install no ciclo 2 e o
|
|
298
300
|
// gate mediu a lacuna: quem ja tinha o Iron instalado seguia com `secrets/` em 755, porque o
|
|
@@ -385,6 +387,7 @@ const versaoDoPacote = process.env.IRON_EDGE_VERSION ? 'iron-edge-' + process.en
|
|
|
385
387
|
const versaoDesatualizada = Boolean(versaoDoPacote) && versaoRegistrada !== versaoDoPacote;
|
|
386
388
|
if (!plano.trocar.length && !plano.adicionar.length && !plano.restaurar.length && !plano.remover.length && !plano.gerar.length && !plano.preservar.length && !versaoDesatualizada && !metadataReparado) {
|
|
387
389
|
log(''); log('Nada a fazer: sua instalacao ja esta na versao deste pacote.');
|
|
390
|
+
log(atalho(false));
|
|
388
391
|
process.exit(0);
|
|
389
392
|
}
|
|
390
393
|
|
|
@@ -519,6 +522,7 @@ log('');
|
|
|
519
522
|
log('================================');
|
|
520
523
|
log(' Atualizado com sucesso');
|
|
521
524
|
log('================================');
|
|
525
|
+
log(atalho(false));
|
|
522
526
|
log('Confira quando quiser: npx iron-edge verify');
|
|
523
527
|
log('Reabra o Claude Code para carregar o que mudou.');
|
|
524
528
|
log('');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iron-edge",
|
|
3
|
-
"version": "2.4.
|
|
4
|
-
"description": "Ecossistema de agentes para Claude Code: 44 agentes de topo, 18 squads e 14 skills, com quality gate, memoria, instalador que nunca sobrescreve
|
|
3
|
+
"version": "2.4.5",
|
|
4
|
+
"description": "Ecossistema de agentes para Claude Code: 44 agentes de topo, 18 squads e 14 skills, com quality gate, memoria, instalador que nunca sobrescreve o comando office (chat do Iron Office no dashboard; confere a chave com o site antes de abrir o Claude) e o doctor (mede a maquina e acende o Setup do site).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"node": ">=18.0.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node scripts/lint.js && node scripts/test-verify.js && node scripts/test-update.js && node scripts/test-office.js",
|
|
35
|
+
"test": "node scripts/lint.js && node scripts/test-verify.js && node scripts/test-update.js && node scripts/test-office.js && node scripts/test-cofre.js && node scripts/test-atalho.js",
|
|
36
36
|
"test:matriz": "node scripts/test-matriz-update.js"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|