@fabioforest/openclaw 3.0.0 → 3.4.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.
Files changed (35) hide show
  1. package/bin/openclaw.js +37 -8
  2. package/lib/cli/assist.js +84 -0
  3. package/lib/cli/doctor.js +37 -3
  4. package/lib/cli/ide.js +218 -0
  5. package/lib/cli/init.js +135 -79
  6. package/lib/cli/inspect.js +58 -0
  7. package/lib/cli/orchestrate.js +43 -15
  8. package/lib/cli/update.js +113 -47
  9. package/lib/context/collector.js +104 -0
  10. package/lib/context/index.js +75 -0
  11. package/lib/router/match.js +107 -0
  12. package/lib/setup/config_wizard.js +2 -0
  13. package/package.json +2 -2
  14. package/templates/.agent/agents/workflow-automator.md +31 -0
  15. package/templates/.agent/rules/CONSENT_FIRST.md +24 -0
  16. package/templates/.agent/rules/DEV_MODE.md +18 -0
  17. package/templates/.agent/rules/ROUTER_PROTOCOL.md +22 -0
  18. package/templates/.agent/rules/WEB_AUTOMATION.md +52 -0
  19. package/templates/.agent/skills/content-sourcer/SKILL.md +48 -0
  20. package/templates/.agent/skills/context-flush/SKILL.md +30 -0
  21. package/templates/.agent/skills/drive-organizer/SKILL.md +40 -0
  22. package/templates/.agent/skills/linkedin-optimizer/SKILL.md +48 -0
  23. package/templates/.agent/skills/mission-control/SKILL.md +37 -0
  24. package/templates/.agent/skills/openclaw-assist/SKILL.md +30 -0
  25. package/templates/.agent/skills/openclaw-dev/SKILL.md +26 -0
  26. package/templates/.agent/skills/openclaw-inspect/SKILL.md +21 -0
  27. package/templates/.agent/skills/openclaw-installation-debugger/scripts/debug.js +16 -2
  28. package/templates/.agent/skills/openclaw-router/SKILL.md +34 -0
  29. package/templates/.agent/skills/openclaw-security/SKILL.md +21 -0
  30. package/templates/.agent/skills/site-tester/SKILL.md +49 -0
  31. package/templates/.agent/skills/smart-router/SKILL.md +116 -0
  32. package/templates/.agent/skills/web-scraper/SKILL.md +51 -0
  33. package/templates/.agent/state/MEMORY.md +8 -0
  34. package/templates/.agent/state/mission_control.json +34 -0
  35. package/templates/.agent/workflows/ai-capture.md +39 -0
package/lib/cli/init.js CHANGED
@@ -10,19 +10,42 @@
10
10
 
11
11
  const fs = require("fs");
12
12
  const path = require("path");
13
+ const readline = require("readline");
13
14
  const { initConfigDefaults, writeJsonSafe } = require("../config");
15
+ const { detectContext, getAuditHeader } = require("../context");
14
16
 
15
17
  // Caminho dos templates incluídos no pacote
16
18
  const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
17
19
 
20
+ function ask(q) {
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
+ return new Promise((res) => rl.question(q, (ans) => { rl.close(); res(ans.trim()); }));
23
+ }
24
+
25
+ function safeRel(targetPath, p) {
26
+ return path.relative(targetPath, p);
27
+ }
28
+
29
+ function writeAudit(targetPath, lines, flags) {
30
+ if (flags.audit === false) return;
31
+ const auditDir = path.join(targetPath, ".agent", "audit");
32
+ if (!fs.existsSync(auditDir)) {
33
+ // Tenta criar apenas se estivermos em modo apply, mas aqui já devemos estar
34
+ try { fs.mkdirSync(auditDir, { recursive: true }); } catch (e) { }
35
+ }
36
+ const filename = `init-${new Date().toISOString().replace(/[:.]/g, "-")}.md`;
37
+ const auditPath = path.join(auditDir, filename);
38
+ try {
39
+ fs.writeFileSync(auditPath, lines.join("\n") + "\n", "utf8");
40
+ } catch (e) {
41
+ console.error("⚠️ Falha ao gravar auditoria:", e.message);
42
+ }
43
+ }
44
+
18
45
  /**
19
- * Copia diretório recursivamente.
20
- * @param {string} src — diretório fonte
21
- * @param {string} dest — diretório destino
22
- * @param {object} [stats] — contador de arquivos copiados
23
- * @returns {object} stats com { files, dirs }
46
+ * Copia diretório recursivamente (Utilitário mantido)
24
47
  */
25
- function copyDirRecursive(src, dest, stats = { files: 0, dirs: 0 }) {
48
+ function copyDirRecursive(src, dest, stats = { files: 0, dirs: 0, skipped: 0 }, merge = false) {
26
49
  if (!fs.existsSync(dest)) {
27
50
  fs.mkdirSync(dest, { recursive: true });
28
51
  stats.dirs++;
@@ -35,110 +58,143 @@ function copyDirRecursive(src, dest, stats = { files: 0, dirs: 0 }) {
35
58
  const destPath = path.join(dest, entry.name);
36
59
 
37
60
  if (entry.isDirectory()) {
38
- copyDirRecursive(srcPath, destPath, stats);
61
+ copyDirRecursive(srcPath, destPath, stats, merge);
39
62
  } else {
40
- fs.copyFileSync(srcPath, destPath);
41
- stats.files++;
63
+ if (merge && fs.existsSync(destPath)) {
64
+ stats.skipped++;
65
+ } else {
66
+ fs.copyFileSync(srcPath, destPath);
67
+ stats.files++;
68
+ }
42
69
  }
43
70
  }
44
-
45
71
  return stats;
46
72
  }
47
73
 
48
74
  /**
49
- * Executa o comando init.
50
- * @param {object} options
51
- * @param {string} options.targetPath — diretório alvo
52
- * @param {object} options.flags — flags do CLI (force, quiet)
75
+ * Executa o comando init com segurança.
53
76
  */
54
77
  async function run({ targetPath, flags }) {
55
78
  const agentDir = path.join(targetPath, ".agent");
56
79
  const configPath = path.join(targetPath, "openclaw.json");
80
+ const ctx = detectContext(targetPath);
57
81
 
58
- // Verificar se existe
59
- if (fs.existsSync(agentDir) && !flags.force) {
60
- console.error("❌ Diretório .agent/ já existe.");
61
- console.error(" Use --force para sobrescrever ou 'openclaw update' para atualizar.");
62
- process.exit(1);
63
- }
82
+ // Default: Plan Mode (read-only), exceto se --apply for passado
83
+ const planMode = !flags.apply;
84
+
85
+ const actions = [];
86
+ const audit = [getAuditHeader(ctx, "init", flags)];
87
+ const errors = [];
64
88
 
65
- // Verificar se templates existem
89
+ // 1. Validar Templates
66
90
  if (!fs.existsSync(TEMPLATES_DIR)) {
67
- console.error("❌ Templates não encontrados. Pacote pode estar corrompido.");
91
+ console.error("❌ Templates não encontrados. Pacote corrompido.");
68
92
  process.exit(1);
69
93
  }
70
94
 
71
- if (!flags.quiet) {
72
- console.log("🦀 OpenClaw — Inicializando projeto...\n");
73
- }
74
-
75
- // Se --force e existe, alertar
76
- if (fs.existsSync(agentDir) && flags.force) {
77
- if (!flags.quiet) {
78
- console.log("⚠️ --force: substituindo .agent/ existente\n");
95
+ // 2. Construir Plano
96
+ if (fs.existsSync(agentDir)) {
97
+ if (!flags.force && !flags.merge) {
98
+ console.error("❌ Diretório .agent/ já existe.");
99
+ console.error(" Use --merge (seguro) ou --force (destrutivo).");
100
+ process.exit(1);
79
101
  }
80
- fs.rmSync(agentDir, { recursive: true, force: true });
102
+ if (flags.force) {
103
+ actions.push({ type: "DELETE_DIR", path: agentDir, reason: "--force requested" });
104
+ actions.push({ type: "CREATE_DIR", path: agentDir });
105
+ actions.push({ type: "COPY_DIR", from: TEMPLATES_DIR, to: agentDir });
106
+ } else if (flags.merge) {
107
+ actions.push({ type: "MERGE_DIR", from: TEMPLATES_DIR, to: agentDir, reason: "--merge requested" });
108
+ }
109
+ } else {
110
+ actions.push({ type: "CREATE_DIR", path: agentDir });
111
+ actions.push({ type: "COPY_DIR", from: TEMPLATES_DIR, to: agentDir });
81
112
  }
82
113
 
83
- // Copiar templates
84
- const stats = copyDirRecursive(TEMPLATES_DIR, agentDir);
114
+ if (!fs.existsSync(configPath)) {
115
+ actions.push({ type: "CREATE_FILE", path: configPath, reason: "Default config" });
116
+ } else {
117
+ actions.push({ type: "NOOP", path: configPath, reason: "Config exists" });
118
+ }
85
119
 
86
- if (!flags.quiet) {
87
- console.log(`✅ .agent/ instalado com sucesso!`);
88
- console.log(` 📁 ${stats.dirs} diretórios criados`);
89
- console.log(` 📄 ${stats.files} arquivos copiados\n`);
120
+ // 3. Exibir Plano
121
+ console.log(`\n🧭 Plano de Execução (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
122
+ console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
123
+
124
+ for (const a of actions) {
125
+ if (a.type === "DELETE_DIR") console.log(` 🔥 DELETE ${safeRel(targetPath, a.path)} (${a.reason})`);
126
+ if (a.type === "CREATE_DIR") console.log(` 📁 CREATE ${safeRel(targetPath, a.path)}`);
127
+ if (a.type === "COPY_DIR") console.log(` 📦 COPY templates -> ${safeRel(targetPath, a.to)}`);
128
+ if (a.type === "MERGE_DIR") console.log(` 🔄 MERGE templates -> ${safeRel(targetPath, a.to)} (Preservando existentes)`);
129
+ if (a.type === "CREATE_FILE") console.log(` 📝 CREATE ${safeRel(targetPath, a.path)}`);
130
+ if (a.type === "NOOP") console.log(` ✅ KEEP ${safeRel(targetPath, a.path)}`);
90
131
  }
91
132
 
92
- // Criar openclaw.json com defaults (se não existir)
93
- if (!fs.existsSync(configPath)) {
94
- const defaults = initConfigDefaults({});
95
- writeJsonSafe(configPath, defaults);
133
+ if (planMode) {
134
+ console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
135
+ console.log(" Para aplicar, rode: npx openclaw init --apply [--merge|--force]");
136
+ return;
137
+ }
96
138
 
97
- if (!flags.quiet) {
98
- console.log("📋 openclaw.json criado com configurações padrão\n");
139
+ // 4. Confirmação
140
+ if (!flags.yes) {
141
+ if (actions.some(a => a.type === "DELETE_DIR")) {
142
+ console.log("\n⚠️ PERIGO: Operação destrutiva detectada (--force).");
143
+ const phrase = await ask("Digite 'DELETE .agent' para confirmar: ");
144
+ if (phrase !== "DELETE .agent") {
145
+ console.log("⏹️ Cancelado.");
146
+ return;
147
+ }
148
+ } else {
149
+ const ok = await ask("\nAplicar este plano? (y/N): ");
150
+ if (ok.toLowerCase() !== "y") {
151
+ console.log("⏹️ Cancelado.");
152
+ return;
153
+ }
99
154
  }
100
- } else if (!flags.quiet) {
101
- console.log("📋 openclaw.json já existe — mantido\n");
102
155
  }
103
156
 
104
- // Resumo final
105
- if (!flags.quiet) {
106
- console.log("📂 Estrutura instalada:");
107
- listInstalledStructure(agentDir, " ");
157
+ // 5. Execução
158
+ try {
159
+ console.log("\n🚀 Executando...");
108
160
 
109
- console.log("\n🚀 Próximos passos:");
110
- console.log(" 1. openclaw setup — configurar ambiente");
111
- console.log(" 2. openclaw doctor — verificar saúde");
112
- console.log(" 3. openclaw status — ver status\n");
113
- }
114
- }
161
+ for (const a of actions) {
162
+ if (a.type === "DELETE_DIR") {
163
+ fs.rmSync(a.path, { recursive: true, force: true });
164
+ audit.push(`- ACT: DELETED ${a.path}`);
165
+ }
166
+ }
115
167
 
116
- /**
117
- * Lista a estrutura instalada de forma visual.
118
- * @param {string} dir — diretório para listar
119
- * @param {string} prefix — prefixo para indentação
120
- */
121
- function listInstalledStructure(dir, prefix = "") {
122
- const entries = fs.readdirSync(dir, { withFileTypes: true })
123
- .sort((a, b) => {
124
- // Diretórios primeiro, depois arquivos
125
- if (a.isDirectory() && !b.isDirectory()) return -1;
126
- if (!a.isDirectory() && b.isDirectory()) return 1;
127
- return a.name.localeCompare(b.name);
128
- });
129
-
130
- for (let i = 0; i < entries.length; i++) {
131
- const entry = entries[i];
132
- const isLast = i === entries.length - 1;
133
- const connector = isLast ? "└── " : "├── ";
134
- const icon = entry.isDirectory() ? "📁" : "📄";
135
-
136
- console.log(`${prefix}${connector}${icon} ${entry.name}`);
168
+ // Executar cópia/merge se necessário
169
+ const copyAction = actions.find(a => a.type === "COPY_DIR" || a.type === "MERGE_DIR");
170
+ if (copyAction) {
171
+ const isMerge = copyAction.type === "MERGE_DIR";
172
+ const stats = copyDirRecursive(TEMPLATES_DIR, agentDir, undefined, isMerge);
173
+ audit.push(`- ACT: ${isMerge ? "MERGED" : "COPIED"} templates (Files: ${stats.files}, Skipped: ${stats.skipped})`);
174
+ console.log(` ✅ Templates processados.`);
175
+ }
137
176
 
138
- if (entry.isDirectory()) {
139
- const childPrefix = prefix + (isLast ? " " : "│ ");
140
- listInstalledStructure(path.join(dir, entry.name), childPrefix);
177
+ // Criar config se necessário
178
+ if (actions.some(a => a.type === "CREATE_FILE" && a.path === configPath)) {
179
+ const defaults = initConfigDefaults({});
180
+ writeJsonSafe(configPath, defaults);
181
+ audit.push(`- ACT: CREATED openclaw.json`);
182
+ console.log(` ✅ Config criada.`);
141
183
  }
184
+
185
+ // Gravar contexto se ainda não existe
186
+ const contextDir = path.join(agentDir, "context");
187
+ if (!fs.existsSync(contextDir)) fs.mkdirSync(contextDir, { recursive: true });
188
+ fs.writeFileSync(path.join(contextDir, "context.json"), JSON.stringify(ctx, null, 2));
189
+
190
+ console.log("\n✨ Concluído com sucesso!");
191
+ writeAudit(targetPath, audit, flags);
192
+
193
+ } catch (err) {
194
+ console.error(`\n❌ Falha na execução: ${err.message}`);
195
+ audit.push(`\n## ERROR: ${err.message}`);
196
+ writeAudit(targetPath, audit, flags);
197
+ process.exit(1);
142
198
  }
143
199
  }
144
200
 
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Comando CLI: inspect
5
+ *
6
+ * 100% Read-Only. Coleta e exibe contexto do ambiente sem alterar nada.
7
+ * Usado como primeiro passo antes de qualquer ação.
8
+ */
9
+
10
+ const path = require("path");
11
+ const collectContext = require("../context/collector");
12
+
13
+ // Caminho dos templates do pacote
14
+ const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates");
15
+
16
+ /**
17
+ * Executa o comando inspect (read-only puro).
18
+ * @param {object} options
19
+ * @param {string} options.targetPath — diretório alvo
20
+ * @param {object} options.flags — flags do CLI
21
+ */
22
+ async function run({ targetPath, flags }) {
23
+ const ctx = collectContext({
24
+ targetPath,
25
+ templatesDir: TEMPLATES_DIR,
26
+ });
27
+
28
+ if (flags.quiet) {
29
+ // Modo silencioso: só JSON
30
+ console.log(JSON.stringify(ctx, null, 2));
31
+ return ctx;
32
+ }
33
+
34
+ console.log("\n🔎 OpenClaw Inspect (Read-Only)\n");
35
+ console.log(` 🖥️ Plataforma: ${ctx.env.platform}`);
36
+ console.log(` 🐳 Docker: ${ctx.env.docker}`);
37
+ console.log(` 🪟 WSL: ${ctx.env.wsl}`);
38
+ console.log(` 💻 IDE: ${ctx.ide}`);
39
+ console.log(` 📂 Path: ${ctx.targetPath}`);
40
+ console.log(` 📦 OpenClaw instalado: ${ctx.openclaw.hasAgentDir ? "Sim" : "Não"}`);
41
+ console.log(` 📋 Config: ${ctx.openclaw.hasConfig ? "Sim" : "Não"}`);
42
+ console.log(` 🐙 Git repo: ${ctx.git.isRepo ? "Sim" : "Não"}`);
43
+
44
+ if (ctx.skillsInstalled.length > 0) {
45
+ console.log(`\n 🧠 Skills instaladas (${ctx.skillsInstalled.length}):`);
46
+ ctx.skillsInstalled.forEach(s => console.log(` • ${s.name}`));
47
+ }
48
+
49
+ if (ctx.skillsInTemplates.length > 0) {
50
+ console.log(`\n 📦 Skills disponíveis nos templates (${ctx.skillsInTemplates.length}):`);
51
+ ctx.skillsInTemplates.forEach(s => console.log(` • ${s.name}`));
52
+ }
53
+
54
+ console.log("\n✅ Inspect concluído (nenhuma alteração feita).\n");
55
+ return ctx;
56
+ }
57
+
58
+ module.exports = { run };
@@ -6,6 +6,19 @@ const initCmd = require("./init");
6
6
  const doctorCmd = require("./doctor");
7
7
  const debugCmd = require("./debug");
8
8
 
9
+ const readline = require("readline");
10
+
11
+ function askQuestion(query) {
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+ return new Promise((resolve) => rl.question(query, (ans) => {
17
+ rl.close();
18
+ resolve(ans);
19
+ }));
20
+ }
21
+
9
22
  module.exports = {
10
23
  run: async function ({ targetPath, flags }) {
11
24
  const agentDir = path.join(targetPath, ".agent");
@@ -15,29 +28,44 @@ module.exports = {
15
28
  if (!fs.existsSync(agentDir)) {
16
29
  console.log("⚠️ Instalação não encontrada.");
17
30
  console.log("🚀 Iniciando processo de instalação (init)...");
18
-
19
- // Repassa flags, força path atual
20
31
  await initCmd.run({ targetPath, flags });
21
32
  return;
22
33
  }
23
34
 
24
- console.log("✅ Instalação detectada.");
25
- console.log("🏥 Executando verificação de saúde (doctor)...");
35
+ console.log("✅ Instalação detectada! Contexto preservado.");
36
+ console.log(" (Arquivos em .agent/ encontrados)\n");
26
37
 
27
- try {
28
- // Doctor lança erro ou exit(1) se falhar?
29
- // Precisamos capturar o exit code do doctor se ele for desenhado para matar o processo.
30
- // O doctor atual usa exit(1) em erros críticos ou apenas loga?
31
- // Vamos checar o doctor depois. Por enquanto, assumimos que ele roda.
38
+ console.log("O que deseja fazer?");
39
+ console.log(" [1] 🏥 Verificar Saúde (Doctor) - Recomendado");
40
+ console.log(" [2] 🔄 Atualizar (Safe Merge) - Adiciona novidades, mantém edições");
41
+ console.log(" [3] ⚠️ Reinstalar (Force) - ALERTA: Sobrescreve TUDO");
42
+ console.log(" [4] 🚪 Sair\n");
32
43
 
33
- await doctorCmd.run({ targetPath, flags });
44
+ const ans = await askQuestion("Escolha uma opção [1-4]: ");
45
+ const choice = ans.trim();
34
46
 
35
- console.log("\n✨ Sistema parece saudável.");
36
- } catch (err) {
37
- console.log("\n❌ Doctor encontrou problemas ou falhou.");
38
- console.log("🔍 Iniciando diagnóstico avançado (debug)...");
47
+ console.log(""); // quebra de linha
39
48
 
40
- await debugCmd.run({ targetPath, flags });
49
+ if (choice === "1" || choice === "") {
50
+ console.log("🏥 Iniciando Doctor...");
51
+ await doctorCmd.run({ targetPath, flags });
52
+ } else if (choice === "2") {
53
+ console.log("🔄 Iniciando Safe Merge...");
54
+ // Como o usuário já confirmou interativamente aqui, passamos apply + yes
55
+ const safeFlags = { ...flags, merge: true, apply: true, yes: true };
56
+ await initCmd.run({ targetPath, flags: safeFlags });
57
+ } else if (choice === "3") {
58
+ const confirm = await askQuestion("Tem certeza? Isso apagará todas as suas customizações em .agent/. Digite 'sim' para confirmar: ");
59
+ if (confirm.toLowerCase() === "sim") {
60
+ console.log("⚠️ Iniciando Reinstalação Forçada...");
61
+ // Aqui passamos force, apply e yes (já confirmou a string "sim")
62
+ const forceFlags = { ...flags, force: true, apply: true, yes: true };
63
+ await initCmd.run({ targetPath, flags: forceFlags });
64
+ } else {
65
+ console.log("⏹️ Cancelado.");
66
+ }
67
+ } else {
68
+ console.log("👋 Saindo.");
41
69
  }
42
70
  }
43
71
  };
package/lib/cli/update.js CHANGED
@@ -12,14 +12,38 @@
12
12
  const fs = require("fs");
13
13
  const path = require("path");
14
14
  const crypto = require("crypto");
15
+ const readline = require("readline");
16
+ const { detectContext, getAuditHeader } = require("../context");
15
17
 
16
18
  // Caminho dos templates incluídos no pacote
17
19
  const TEMPLATES_DIR = path.join(__dirname, "..", "..", "templates", ".agent");
18
20
 
21
+ function ask(q) {
22
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23
+ return new Promise((res) => rl.question(q, (ans) => { rl.close(); res(ans.trim()); }));
24
+ }
25
+
26
+ function safeRel(targetPath, p) {
27
+ return path.relative(targetPath, p);
28
+ }
29
+
30
+ function writeAudit(targetPath, lines, flags) {
31
+ if (flags.audit === false) return;
32
+ const auditDir = path.join(targetPath, ".agent", "audit");
33
+ if (!fs.existsSync(auditDir)) {
34
+ try { fs.mkdirSync(auditDir, { recursive: true }); } catch (e) { }
35
+ }
36
+ const filename = `update-${new Date().toISOString().replace(/[:.]/g, "-")}.md`;
37
+ const auditPath = path.join(auditDir, filename);
38
+ try {
39
+ fs.writeFileSync(auditPath, lines.join("\n") + "\n", "utf8");
40
+ } catch (e) {
41
+ console.error("⚠️ Falha ao gravar auditoria:", e.message);
42
+ }
43
+ }
44
+
19
45
  /**
20
- * Calcula o SHA-256 de um arquivo.
21
- * @param {string} filePath — caminho do arquivo
22
- * @returns {string} hash em hex
46
+ * Calcula o SHA-256 de um arquivo (Utilitário mantido)
23
47
  */
24
48
  function fileHash(filePath) {
25
49
  const content = fs.readFileSync(filePath);
@@ -27,14 +51,13 @@ function fileHash(filePath) {
27
51
  }
28
52
 
29
53
  /**
30
- * Compara e atualiza um diretório recursivamente.
31
- * @param {string} src diretório fonte (template)
32
- * @param {string} dest — diretório destino (instalado)
33
- * @param {object} stats — contadores { updated, skipped, added }
54
+ * Analisa atualizações necessárias.
55
+ * Retorna lista de ações planejadas.
34
56
  */
35
- function updateDirRecursive(src, dest, stats) {
57
+ function planUpdates(src, dest, actions = { added: [], updated: [], skipped: [] }) {
36
58
  if (!fs.existsSync(dest)) {
37
- fs.mkdirSync(dest, { recursive: true });
59
+ // Diretório não existe no destino, será criado implicitamente na cópia
60
+ // Mas a lógica recursiva precisa entrar
38
61
  }
39
62
 
40
63
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -44,79 +67,122 @@ function updateDirRecursive(src, dest, stats) {
44
67
  const destPath = path.join(dest, entry.name);
45
68
 
46
69
  if (entry.isDirectory()) {
47
- updateDirRecursive(srcPath, destPath, stats);
70
+ if (!fs.existsSync(destPath)) {
71
+ // Diretório novo, tudo dentro será added
72
+ // Simplificação: marcar diretório como added e não recursar?
73
+ // Melhor recursar para listar arquivos
74
+ }
75
+ planUpdates(srcPath, destPath, actions);
48
76
  } else {
49
77
  if (!fs.existsSync(destPath)) {
50
- // Arquivo novo copiar
51
- fs.copyFileSync(srcPath, destPath);
52
- stats.added.push(path.relative(dest, destPath) || entry.name);
78
+ actions.added.push({ src: srcPath, dest: destPath });
53
79
  } else {
54
- // Arquivo já existe — comparar hashes
55
80
  const srcHash = fileHash(srcPath);
56
81
  const destHash = fileHash(destPath);
57
-
58
82
  if (srcHash === destHash) {
59
- // Idêntico nada a fazer
60
- stats.skipped.push(path.relative(dest, destPath) || entry.name);
83
+ actions.skipped.push({ src: srcPath, dest: destPath });
61
84
  } else {
62
- // Diferente arquivo foi customizado ou template atualizado
63
- // Preserva o original do usuário fazendo backup
64
- const backupPath = destPath + ".bak";
65
- fs.copyFileSync(destPath, backupPath);
66
- fs.copyFileSync(srcPath, destPath);
67
- stats.updated.push(path.relative(dest, destPath) || entry.name);
85
+ actions.updated.push({ src: srcPath, dest: destPath });
68
86
  }
69
87
  }
70
88
  }
71
89
  }
90
+ return actions;
72
91
  }
73
92
 
74
93
  /**
75
- * Executa o comando update.
76
- * @param {object} options
77
- * @param {string} options.targetPath — diretório alvo
78
- * @param {object} options.flags — flags do CLI
94
+ * Executa o comando update com segurança.
79
95
  */
80
96
  async function run({ targetPath, flags }) {
81
97
  const agentDir = path.join(targetPath, ".agent");
98
+ const ctx = detectContext(targetPath);
99
+
100
+ // Default: Plan Mode
101
+ const planMode = !flags.apply;
82
102
 
83
103
  if (!fs.existsSync(agentDir)) {
84
104
  console.error("❌ Diretório .agent/ não encontrado.");
85
- console.error(" Rode 'openclaw init' primeiro para instalar os templates.");
105
+ console.error(" Rode 'openclaw init' primeiro.");
86
106
  process.exit(1);
87
107
  }
88
-
89
108
  if (!fs.existsSync(TEMPLATES_DIR)) {
90
- console.error("❌ Templates não encontrados. Pacote pode estar corrompido.");
109
+ console.error("❌ Templates não encontrados.");
91
110
  process.exit(1);
92
111
  }
93
112
 
94
- if (!flags.quiet) {
95
- console.log("\n🔄 OpenClaw Update Atualizando templates...\n");
113
+ // 1. Planejar
114
+ const actions = planUpdates(TEMPLATES_DIR, agentDir);
115
+ const audit = [getAuditHeader(ctx, "update", flags)];
116
+
117
+ // 2. Exibir Plano
118
+ console.log(`\n🧭 Plano de Atualização (${planMode ? "SIMULAÇÃO" : "APPLY"}):\n`);
119
+ console.log(` Contexto: ${ctx.env} | IDE: ${ctx.ide}\n`);
120
+
121
+ if (actions.added.length > 0) {
122
+ console.log(`📄 Novos (${actions.added.length}):`);
123
+ actions.added.forEach(a => console.log(` + CREATE ${safeRel(targetPath, a.dest)}`));
124
+ }
125
+ if (actions.updated.length > 0) {
126
+ console.log(`\n🔄 Modificados (${actions.updated.length}):`);
127
+ actions.updated.forEach(a => console.log(` ~ UPDATE ${safeRel(targetPath, a.dest)} (Backup gerado)`));
128
+ }
129
+ if (actions.skipped.length > 0) {
130
+ console.log(`\n⏭️ Ignorados (${actions.skipped.length} arquivos idênticos)`);
96
131
  }
97
132
 
98
- const stats = { updated: [], skipped: [], added: [] };
99
- updateDirRecursive(TEMPLATES_DIR, agentDir, stats);
133
+ if (actions.added.length === 0 && actions.updated.length === 0) {
134
+ console.log("\n✅ Tudo atualizado. Nenhuma alteração necessária.");
135
+ return;
136
+ }
137
+
138
+ if (planMode) {
139
+ console.log("\n🔒 Modo PLAN (Read-Only). Nenhuma alteração feita.");
140
+ console.log(" Para aplicar, rode: npx openclaw update --apply");
141
+ return;
142
+ }
100
143
 
101
- if (!flags.quiet) {
102
- if (stats.added.length > 0) {
103
- console.log(`📄 Novos (${stats.added.length}):`);
104
- stats.added.forEach((f) => console.log(` + ${f}`));
144
+ // 3. Confirmação
145
+ if (!flags.yes) {
146
+ const ok = await ask("\nAplicar este plano? (y/N): ");
147
+ if (ok.toLowerCase() !== "y") {
148
+ console.log("⏹️ Cancelado.");
149
+ return;
150
+ }
151
+ }
152
+
153
+ // 4. Execução
154
+ try {
155
+ console.log("\n🚀 Executando atualizações...");
156
+
157
+ // Criar diretórios necessários
158
+ function ensureDir(p) {
159
+ const dir = path.dirname(p);
160
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
105
161
  }
106
162
 
107
- if (stats.updated.length > 0) {
108
- console.log(`\n🔄 Atualizados (${stats.updated.length}):`);
109
- stats.updated.forEach((f) => console.log(` ~ ${f} (backup: ${f}.bak)`));
163
+ for (const action of actions.added) {
164
+ ensureDir(action.dest);
165
+ fs.copyFileSync(action.src, action.dest);
166
+ audit.push(`- ACT: CREATED ${safeRel(targetPath, action.dest)}`);
110
167
  }
111
168
 
112
- if (stats.skipped.length > 0) {
113
- console.log(`\n⏭️ Sem alteração (${stats.skipped.length}):`);
114
- stats.skipped.forEach((f) => console.log(` = ${f}`));
169
+ for (const action of actions.updated) {
170
+ ensureDir(action.dest);
171
+ const backupPath = action.dest + ".bak";
172
+ fs.copyFileSync(action.dest, backupPath);
173
+ fs.copyFileSync(action.src, action.dest);
174
+ audit.push(`- ACT: UPDATED ${safeRel(targetPath, action.dest)} (Backup: ${path.basename(backupPath)})`);
115
175
  }
116
176
 
117
- const total = stats.added.length + stats.updated.length;
118
- console.log(`\n✅ Update concluído: ${total} alterações, ${stats.skipped.length} mantidos\n`);
177
+ console.log("\n✨ Atualização concluída com sucesso!");
178
+ writeAudit(targetPath, audit, flags);
179
+
180
+ } catch (err) {
181
+ console.error(`\n❌ Falha na execução: ${err.message}`);
182
+ audit.push(`\n## ERROR: ${err.message}`);
183
+ writeAudit(targetPath, audit, flags);
184
+ process.exit(1);
119
185
  }
120
186
  }
121
187
 
122
- module.exports = { run, updateDirRecursive, fileHash };
188
+ module.exports = { run, fileHash };