ozali 0.4.0 → 0.5.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 +21 -3
- package/cli/bin/ozali.mjs +16 -4
- package/cli/lib/commands.mjs +598 -28
- package/cli/lib/detect.mjs +9 -1
- package/cli/lib/util.mjs +23 -0
- package/cli/test/smoke.test.mjs +64 -0
- package/docs/deploy-cloud-gcloud.md +149 -0
- package/docs/deploy-cloud-vps.md +174 -0
- package/docs/guia-de-uso.md +6 -1
- package/docs/ideas/postgres-vs-mysql-engram-cloud.md +137 -0
- package/docs/team-history.md +52 -0
- package/package.json +1 -1
- package/skill/references/engram-convention.md +78 -0
package/cli/lib/commands.mjs
CHANGED
|
@@ -1,22 +1,87 @@
|
|
|
1
1
|
// commands.mjs — implementación de init / doctor / update / sync.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
4
5
|
import {
|
|
5
6
|
c, ok, warn, err, info, step,
|
|
6
7
|
SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
|
|
7
|
-
ensureGitignore, tryExec, spawnCmd, which,
|
|
8
|
-
projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME,
|
|
8
|
+
ensureGitignore, tryExec, spawnCmd, which, engramAssetName,
|
|
9
|
+
projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL,
|
|
9
10
|
} from "./util.mjs";
|
|
10
11
|
import { detectAll } from "./detect.mjs";
|
|
11
12
|
import { ask, confirm, select } from "./prompt.mjs";
|
|
12
13
|
|
|
13
14
|
const CONFIG_PATH = (cwd) => path.join(cwd, ".ozali", "config.json");
|
|
15
|
+
const TEAM_CLOUD_PATH = (cwd) => path.join(cwd, ".ozali", "cloud.json");
|
|
16
|
+
const ENGRAM_CONFIG_PATH = (cwd) => path.join(cwd, ".engram", "config.json");
|
|
17
|
+
const CLOUD_TOKEN_ENV = "ENGRAM_CLOUD_TOKEN";
|
|
18
|
+
const CLOUD_AUTOSYNC_ENV = "ENGRAM_CLOUD_AUTOSYNC";
|
|
19
|
+
const CLOUD_SERVER_ENV = "ENGRAM_CLOUD_SERVER";
|
|
14
20
|
|
|
15
21
|
function skillTarget(cwd, scope) {
|
|
16
22
|
const base = scope === "global" ? path.join(process.env.HOME || "", ".claude") : path.join(cwd, ".claude");
|
|
17
23
|
return path.join(base, "skills", "ozali");
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function readTeamCloud(cwd) {
|
|
27
|
+
return readJSON(TEAM_CLOUD_PATH(cwd));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeTeamCloud(cwd, cloud) {
|
|
31
|
+
writeJSON(TEAM_CLOUD_PATH(cwd), {
|
|
32
|
+
server: cloud.server,
|
|
33
|
+
project: cloud.project,
|
|
34
|
+
enrolled: !!cloud.enabled,
|
|
35
|
+
auth_required: cloud.authRequired !== false,
|
|
36
|
+
token_env: cloud.tokenEnv || CLOUD_TOKEN_ENV,
|
|
37
|
+
autosync_env: cloud.autosyncEnv || CLOUD_AUTOSYNC_ENV,
|
|
38
|
+
dashboard: cloudDashboardURL(cloud.server),
|
|
39
|
+
updated_at: new Date().toISOString(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function firstNonEmpty(...values) {
|
|
44
|
+
return values.find((value) => typeof value === "string" && value.trim()) || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function firstLine(text, fallback = "") {
|
|
48
|
+
if (!text) return fallback;
|
|
49
|
+
const line = String(text).split(/\r?\n/).map((part) => part.trim()).find(Boolean);
|
|
50
|
+
return line || fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printIndented(text) {
|
|
54
|
+
if (!text) return;
|
|
55
|
+
for (const line of String(text).split(/\r?\n/)) console.log(` ${c.dim(line)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cloudDashboardURL(server) {
|
|
59
|
+
if (!server) return null;
|
|
60
|
+
return server.replace(/\/+$/, "") + "/dashboard";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasCloudToken() {
|
|
64
|
+
return !!firstNonEmpty(process.env[CLOUD_TOKEN_ENV]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractReasonCode(text) {
|
|
68
|
+
if (!text) return null;
|
|
69
|
+
const match = firstLine(text).match(/(?:reason(?:_code)?|reasonCode)\s*[:=]\s*([a-z_]+)/i);
|
|
70
|
+
return match ? match[1].toLowerCase() : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function syncCloudProject(cwd, project) {
|
|
74
|
+
return tryExec("engram", ["sync", "--cloud", "--project", project], { cwd });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cloudStatusSnapshot(cwd, project) {
|
|
78
|
+
return {
|
|
79
|
+
syncStatus: tryExec("engram", ["sync", "--cloud", "--status", "--project", project], { cwd }),
|
|
80
|
+
upgradeStatus: tryExec("engram", ["cloud", "upgrade", "status", "--project", project], { cwd }),
|
|
81
|
+
conflictsStats: tryExec("engram", ["conflicts", "stats", "--project", project], { cwd }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
20
85
|
// ============================================================ init ===========
|
|
21
86
|
export async function init(cwd, opts) {
|
|
22
87
|
step("ozali init — bootstrap del proyecto");
|
|
@@ -88,7 +153,13 @@ export async function init(cwd, opts) {
|
|
|
88
153
|
// Engram Cloud (opt-in) — réplica de equipo además del git-sync. Solo si Engram quedó disponible.
|
|
89
154
|
let cloud = { enabled: false };
|
|
90
155
|
if (memoryMode === "hybrid" && !opts.dryRun) {
|
|
91
|
-
|
|
156
|
+
const teamCloud = readTeamCloud(cwd);
|
|
157
|
+
if (teamCloud && teamCloud.enrolled) {
|
|
158
|
+
// Fase 1: dev nuevo en un repo que ya tiene .ozali/cloud.json del equipo
|
|
159
|
+
cloud = await connectTeamCloud(cwd, teamCloud, opts);
|
|
160
|
+
} else {
|
|
161
|
+
cloud = await maybeEnableEngramCloud(cwd, projectName(cwd), opts);
|
|
162
|
+
}
|
|
92
163
|
}
|
|
93
164
|
|
|
94
165
|
if (opts.dryRun) { warn("--dry-run: no escribo nada. Plan mostrado arriba."); return 0; }
|
|
@@ -115,7 +186,7 @@ export async function init(cwd, opts) {
|
|
|
115
186
|
if (!opts.noJarvis) {
|
|
116
187
|
const proj = projectName(cwd);
|
|
117
188
|
// Fija el proyecto para escrituras deterministas de memoria (se deriva igual por cada miembro).
|
|
118
|
-
writeJSON(
|
|
189
|
+
writeJSON(ENGRAM_CONFIG_PATH(cwd), { project_name: proj });
|
|
119
190
|
ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`);
|
|
120
191
|
if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
|
|
121
192
|
if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
|
|
@@ -123,7 +194,7 @@ export async function init(cwd, opts) {
|
|
|
123
194
|
|
|
124
195
|
// 3) gitignore del histórico aislado
|
|
125
196
|
if (env.git.isRepo) {
|
|
126
|
-
const { added } = ensureGitignore(cwd, [".ozali/", ".engram/"]);
|
|
197
|
+
const { added } = ensureGitignore(cwd, [".ozali/*", "!.ozali/cloud.json", ".engram/"]);
|
|
127
198
|
if (added.length) ok(`.gitignore actualizado: ${added.join(", ")} (histórico aislado del repo principal).`);
|
|
128
199
|
else info(".gitignore ya aislaba el histórico.");
|
|
129
200
|
}
|
|
@@ -167,14 +238,28 @@ export async function init(cwd, opts) {
|
|
|
167
238
|
|
|
168
239
|
/**
|
|
169
240
|
* Instala Engram con la mejor ruta disponible para el SO actual.
|
|
170
|
-
*
|
|
241
|
+
* Linux: binario precompilado (auto-descarga, sin toolchain) con brew/go como alternativas.
|
|
242
|
+
* macOS: Homebrew → Go → binario precompilado como fallback.
|
|
171
243
|
* Windows: Go install (recomendado) o binario manual.
|
|
172
244
|
* Devuelve true si el binario quedó disponible en PATH.
|
|
173
245
|
*/
|
|
174
246
|
function installEngram() {
|
|
175
247
|
const plat = process.platform;
|
|
176
248
|
|
|
177
|
-
if (plat === "
|
|
249
|
+
if (plat === "linux") {
|
|
250
|
+
// En Linux Homebrew es poco común: el binario precompilado es la vía universal sin toolchain.
|
|
251
|
+
if (installEngramFromTarball()) return true;
|
|
252
|
+
if (which("brew")) {
|
|
253
|
+
info("Instalando: " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
254
|
+
spawnCmd("brew", ["install", "gentleman-programming/tap/engram"]);
|
|
255
|
+
} else if (which("go")) {
|
|
256
|
+
info("Compilando con Go: " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
257
|
+
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
258
|
+
} else {
|
|
259
|
+
warn("No pude instalar Engram automáticamente. Sigue las instrucciones de abajo.");
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
} else if (plat === "darwin") {
|
|
178
263
|
if (which("brew")) {
|
|
179
264
|
info("Instalando: " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
180
265
|
spawnCmd("brew", ["install", "gentleman-programming/tap/engram"]);
|
|
@@ -182,8 +267,10 @@ function installEngram() {
|
|
|
182
267
|
info("Homebrew no encontrado — instalando con Go:");
|
|
183
268
|
info(" " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
184
269
|
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
270
|
+
} else if (installEngramFromTarball()) {
|
|
271
|
+
return true;
|
|
185
272
|
} else {
|
|
186
|
-
warn("No se encontró Homebrew ni Go. Opciones
|
|
273
|
+
warn("No se encontró Homebrew ni Go y falló la descarga del binario. Opciones:");
|
|
187
274
|
info(" a) Homebrew " + c.dim("(recomendado)") + ": " + c.cyan("https://brew.sh") + " → " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
188
275
|
info(" b) Binario precompilado: " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
|
|
189
276
|
return false;
|
|
@@ -210,11 +297,159 @@ function installEngram() {
|
|
|
210
297
|
return false;
|
|
211
298
|
}
|
|
212
299
|
|
|
300
|
+
const ENGRAM_RELEASES_API = "https://api.github.com/repos/Gentleman-Programming/engram/releases/latest";
|
|
301
|
+
const ENGRAM_RELEASES_DL = "https://github.com/Gentleman-Programming/engram/releases/download";
|
|
302
|
+
|
|
303
|
+
/** Última versión publicada de Engram (sin la "v" inicial), o null si no hay red. */
|
|
304
|
+
function latestEngramVersion() {
|
|
305
|
+
let raw = null;
|
|
306
|
+
if (which("curl")) raw = tryExec("curl", ["-fsSL", ENGRAM_RELEASES_API]);
|
|
307
|
+
else if (which("wget")) raw = tryExec("wget", ["-qO-", ENGRAM_RELEASES_API]);
|
|
308
|
+
if (!raw) return null;
|
|
309
|
+
try {
|
|
310
|
+
const tag = JSON.parse(raw).tag_name;
|
|
311
|
+
return tag ? String(tag).replace(/^v/, "") : null;
|
|
312
|
+
} catch { return null; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Descarga url → dest con curl (o wget como fallback). Devuelve true si tuvo éxito. */
|
|
316
|
+
function download(url, dest) {
|
|
317
|
+
if (which("curl")) return spawnCmd("curl", ["-fL", "--retry", "2", "-o", dest, url]) === 0;
|
|
318
|
+
if (which("wget")) return spawnCmd("wget", ["-O", dest, url]) === 0;
|
|
319
|
+
warn("No se encontró curl ni wget para descargar el binario de Engram.");
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Busca recursivamente un ejecutable llamado "engram" dentro de dir (1 nivel basta). */
|
|
324
|
+
function findEngramBinary(dir) {
|
|
325
|
+
let entries;
|
|
326
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
|
327
|
+
for (const e of entries) {
|
|
328
|
+
const full = path.join(dir, e.name);
|
|
329
|
+
if (e.isFile() && e.name === "engram") return full;
|
|
330
|
+
if (e.isDirectory()) {
|
|
331
|
+
const nested = findEngramBinary(full);
|
|
332
|
+
if (nested) return nested;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Instala Engram bajando el binario precompilado del release más reciente (linux/macOS).
|
|
340
|
+
* Lo coloca en ~/.local/bin (sin sudo) y lo antepone al PATH del proceso para que los
|
|
341
|
+
* `engram setup <agente>` posteriores lo encuentren en esta misma corrida.
|
|
342
|
+
* Node-14-safe: usa curl/wget + tar vía execFileSync (sin fetch global). Devuelve true si quedó listo.
|
|
343
|
+
*/
|
|
344
|
+
function installEngramFromTarball() {
|
|
345
|
+
const plat = process.platform;
|
|
346
|
+
if (plat !== "linux" && plat !== "darwin") return false;
|
|
347
|
+
|
|
348
|
+
// Falla rápido si la arquitectura no tiene binario publicado (versión irrelevante para esta validación).
|
|
349
|
+
if (!engramAssetName(plat, process.arch, "0")) {
|
|
350
|
+
warn(`Arquitectura no soportada para el binario precompilado (${process.arch}).`);
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const version = latestEngramVersion();
|
|
355
|
+
if (!version) {
|
|
356
|
+
warn("No pude resolver la última versión de Engram (¿sin red o sin curl/wget?).");
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const asset = engramAssetName(plat, process.arch, version);
|
|
361
|
+
const url = `${ENGRAM_RELEASES_DL}/v${version}/${asset}`;
|
|
362
|
+
info(`Descargando binario precompilado de Engram ${c.bold("v" + version)} (${process.arch})…`);
|
|
363
|
+
|
|
364
|
+
let tmpDir;
|
|
365
|
+
try { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-engram-")); }
|
|
366
|
+
catch { warn("No pude crear un directorio temporal para la descarga."); return false; }
|
|
367
|
+
|
|
368
|
+
const tarball = path.join(tmpDir, asset);
|
|
369
|
+
if (!download(url, tarball)) { warn("Falló la descarga del binario de Engram."); return false; }
|
|
370
|
+
|
|
371
|
+
if (spawnCmd("tar", ["-xzf", tarball, "-C", tmpDir]) !== 0) {
|
|
372
|
+
warn("Falló la extracción del tarball de Engram (¿tar disponible?).");
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const binSrc = findEngramBinary(tmpDir);
|
|
377
|
+
if (!binSrc) { warn("No encontré el binario engram dentro del tarball."); return false; }
|
|
378
|
+
|
|
379
|
+
const destDir = path.join(HOME, ".local", "bin");
|
|
380
|
+
const dest = path.join(destDir, "engram");
|
|
381
|
+
try {
|
|
382
|
+
ensureDir(destDir);
|
|
383
|
+
fs.copyFileSync(binSrc, dest);
|
|
384
|
+
fs.chmodSync(dest, 0o755);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
warn(`No pude instalar el binario en ${destDir} (${e.message}).`);
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Disponible en esta corrida para los `engram setup` que vienen después.
|
|
391
|
+
const onPath = (process.env.PATH || "").split(path.delimiter).includes(destDir);
|
|
392
|
+
if (!onPath) process.env.PATH = destDir + path.delimiter + (process.env.PATH || "");
|
|
393
|
+
|
|
394
|
+
ok(`Engram instalado en ${c.bold(dest)}.`);
|
|
395
|
+
if (!onPath) {
|
|
396
|
+
warn(`${c.bold(destDir)} no estaba en tu PATH. Para usar ${c.bold("engram")} fuera del agente, añádelo a tu shell:`);
|
|
397
|
+
console.log(` ${c.dim('export PATH="$HOME/.local/bin:$PATH"')}`);
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Fase 1: Onboarding de equipo. Un dev nuevo hace `ozali init` en un repo que ya tiene
|
|
404
|
+
* .ozali/cloud.json (commiteable). Detecta la cloud del equipo y ofrece conectarse en 1 paso.
|
|
405
|
+
*/
|
|
406
|
+
async function connectTeamCloud(cwd, cloudMeta, opts) {
|
|
407
|
+
const project = cloudMeta.project || projectName(cwd);
|
|
408
|
+
ok(`Engram Cloud del equipo detectado (servidor: ${c.bold(cloudMeta.server || "por defecto")})`);
|
|
409
|
+
info(` → Proyecto: "${project}"`);
|
|
410
|
+
const connect = await confirm("¿Conectarte a la memoria del equipo?", true);
|
|
411
|
+
if (!connect) {
|
|
412
|
+
info("Cloud del equipo omitido. Puedes conectarte después con " + c.bold("ozali cloud config") + ".");
|
|
413
|
+
return { enabled: false, server: cloudMeta.server };
|
|
414
|
+
}
|
|
415
|
+
const token = await ask("Token de autenticación (ENGRAM_CLOUD_TOKEN)");
|
|
416
|
+
if (!token) {
|
|
417
|
+
warn("Sin token no se puede conectar (modo autenticado obligatorio). Continúo con git-sync.");
|
|
418
|
+
return { enabled: false, server: cloudMeta.server };
|
|
419
|
+
}
|
|
420
|
+
const server = cloudMeta.server || "http://127.0.0.1:18080";
|
|
421
|
+
info(`Configurando Engram Cloud → ${server}`);
|
|
422
|
+
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
423
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
424
|
+
info(`Enrolando el proyecto "${project}"…`);
|
|
425
|
+
if (spawnCmd("engram", ["cloud", "enroll", project]) !== 0) {
|
|
426
|
+
warn("No se pudo enrolar. Verifica el token y el servidor. Continúo con git-sync.");
|
|
427
|
+
return { enabled: false, server };
|
|
428
|
+
}
|
|
429
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
430
|
+
configureCloudAutosync(cwd, opts);
|
|
431
|
+
persistCloudToken(token, opts);
|
|
432
|
+
ok("Conectado a Engram Cloud del equipo.");
|
|
433
|
+
// Recibir la memoria del equipo (pull desde cloud)
|
|
434
|
+
info("Recibiendo memoria del equipo…");
|
|
435
|
+
const pullOut = tryExec("engram", ["sync", "--cloud", "--import", "--project", project], { cwd });
|
|
436
|
+
if (pullOut !== null) {
|
|
437
|
+
ok("Memoria del equipo recibida.");
|
|
438
|
+
if (pullOut.trim()) printIndented(pullOut);
|
|
439
|
+
// Importar chunks locales
|
|
440
|
+
if (spawnCmd("engram", ["sync", "--import"], { cwd }) === 0) ok("Memorias importadas a Engram local.");
|
|
441
|
+
} else {
|
|
442
|
+
warn("No se pudo recibir la memoria del equipo. Corre " + c.bold("ozali sync --cloud --import") + " más tarde.");
|
|
443
|
+
}
|
|
444
|
+
return { enabled: true, server, token };
|
|
445
|
+
}
|
|
446
|
+
|
|
213
447
|
/**
|
|
214
448
|
* Engram Cloud opt-in: réplica de equipo en tiempo real, adicional al git-sync.
|
|
215
|
-
*
|
|
449
|
+
* Modo autenticado obligatorio: siempre pide token. Configura autosync en el agente.
|
|
450
|
+
* Devuelve { enabled, server, token }.
|
|
216
451
|
*/
|
|
217
|
-
async function maybeEnableEngramCloud(project, opts) {
|
|
452
|
+
async function maybeEnableEngramCloud(cwd, project, opts) {
|
|
218
453
|
if (opts.yes) return { enabled: false };
|
|
219
454
|
const enable = await confirm("¿Habilitar Engram Cloud para el equipo? (réplica opt-in, requiere un servidor)", false);
|
|
220
455
|
if (!enable) {
|
|
@@ -222,21 +457,100 @@ async function maybeEnableEngramCloud(project, opts) {
|
|
|
222
457
|
return { enabled: false };
|
|
223
458
|
}
|
|
224
459
|
const server = await ask("URL del servidor de Engram Cloud", "http://127.0.0.1:18080");
|
|
460
|
+
const token = await ask("Token de autenticación (ENGRAM_CLOUD_TOKEN)");
|
|
461
|
+
if (!token) {
|
|
462
|
+
warn("Sin token no se puede configurar Engram Cloud (modo autenticado obligatorio). Continúo con git-sync.");
|
|
463
|
+
return { enabled: false, server };
|
|
464
|
+
}
|
|
225
465
|
info(`Configurando Engram Cloud → ${server}`);
|
|
226
466
|
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
467
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
227
468
|
info(`Enrolando el proyecto "${project}"…`);
|
|
228
469
|
if (spawnCmd("engram", ["cloud", "enroll", project]) === 0) {
|
|
470
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
471
|
+
configureCloudAutosync(cwd, opts);
|
|
472
|
+
persistCloudToken(token, opts);
|
|
229
473
|
ok("Engram Cloud habilitado. Replica con " + c.bold("ozali sync --cloud") + ".");
|
|
230
|
-
|
|
474
|
+
const dash = cloudDashboardURL(server);
|
|
475
|
+
if (dash) {
|
|
476
|
+
info(`Dashboard: ${c.cyan(dash)}`);
|
|
477
|
+
if (await confirm("¿Abrir el dashboard en el navegador?", false)) openURL(dash);
|
|
478
|
+
}
|
|
479
|
+
return { enabled: true, server, token };
|
|
231
480
|
}
|
|
232
481
|
warn("No se pudo enrolar el proyecto en Engram Cloud. Continúo solo con git-sync.");
|
|
233
482
|
return { enabled: false, server };
|
|
234
483
|
}
|
|
235
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Configura ENGRAM_CLOUD_AUTOSYNC=1 (y ENGRAM_CLOUD_TOKEN) en el bloque env del MCP
|
|
487
|
+
* de Engram del agente, para que la réplica sea automática e invisible.
|
|
488
|
+
*/
|
|
489
|
+
function configureCloudAutosync(cwd, opts) {
|
|
490
|
+
const agent = opts.agent || "claude-code";
|
|
491
|
+
const token = process.env[CLOUD_TOKEN_ENV] || "";
|
|
492
|
+
// Claude Code: .claude/settings.json → mcpServers.engram.env
|
|
493
|
+
if (agent === "claude-code" || agent === "both") {
|
|
494
|
+
const p = path.join(cwd, ".claude", "settings.json");
|
|
495
|
+
const cfg = readJSON(p, {});
|
|
496
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
497
|
+
cfg.mcpServers.engram = cfg.mcpServers.engram || {};
|
|
498
|
+
cfg.mcpServers.engram.env = cfg.mcpServers.engram.env || {};
|
|
499
|
+
cfg.mcpServers.engram.env[CLOUD_AUTOSYNC_ENV] = "1";
|
|
500
|
+
if (token) cfg.mcpServers.engram.env[CLOUD_TOKEN_ENV] = token;
|
|
501
|
+
writeJSON(p, cfg);
|
|
502
|
+
ok(`Autosync de Engram Cloud configurado en ${c.bold(".claude/settings.json")} (mcpServers.engram.env).`);
|
|
503
|
+
}
|
|
504
|
+
// opencode: opencode.json → mcp.engram.env
|
|
505
|
+
if (agent === "opencode" || agent === "both") {
|
|
506
|
+
const p = path.join(cwd, "opencode.json");
|
|
507
|
+
const cfg = readJSON(p, {});
|
|
508
|
+
cfg.mcp = cfg.mcp || {};
|
|
509
|
+
cfg.mcp.engram = cfg.mcp.engram || {};
|
|
510
|
+
if (cfg.mcp.engram.env === undefined || typeof cfg.mcp.engram.env !== "object") {
|
|
511
|
+
cfg.mcp.engram.env = {};
|
|
512
|
+
} else {
|
|
513
|
+
cfg.mcp.engram.env = { ...cfg.mcp.engram.env };
|
|
514
|
+
}
|
|
515
|
+
cfg.mcp.engram.env[CLOUD_AUTOSYNC_ENV] = "1";
|
|
516
|
+
if (token) cfg.mcp.engram.env[CLOUD_TOKEN_ENV] = token;
|
|
517
|
+
writeJSON(p, cfg);
|
|
518
|
+
ok(`Autosync de Engram Cloud configurado en ${c.bold("opencode.json")} (mcp.engram.env).`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Persiste el token de Engram Cloud para sesiones futuras.
|
|
524
|
+
* 1) ~/.engram/cloud_token (default de Engram)
|
|
525
|
+
* 2) Avisa al usuario que añada la env var a su shell rc si quiere uso fuera del agente.
|
|
526
|
+
*/
|
|
527
|
+
function persistCloudToken(token, opts) {
|
|
528
|
+
const tokenPath = path.join(HOME, ".engram", "cloud_token");
|
|
529
|
+
try {
|
|
530
|
+
ensureDir(path.dirname(tokenPath));
|
|
531
|
+
fs.writeFileSync(tokenPath, token + "\n", { mode: 0o600 });
|
|
532
|
+
ok(`Token guardado en ${c.bold("~/.engram/cloud_token")} (permisos 600).`);
|
|
533
|
+
} catch {
|
|
534
|
+
warn("No pude escribir ~/.engram/cloud_token. Guarda el token manualmente.");
|
|
535
|
+
}
|
|
536
|
+
const shell = process.env.SHELL || "";
|
|
537
|
+
const rc = shell.includes("zsh") ? "~/.zshrc" : shell.includes("bash") ? "~/.bashrc" : null;
|
|
538
|
+
if (rc) {
|
|
539
|
+
info(`Para usar Engram Cloud fuera del agente, añade a ${c.bold(rc)}:`);
|
|
540
|
+
console.log(` ${c.dim(`export ${CLOUD_TOKEN_ENV}=<tu-token>`)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
236
544
|
function printEngramManualInstructions(agent) {
|
|
237
545
|
const plat = process.platform;
|
|
238
546
|
info("Para activar memoria buscable/acumulativa (modo " + c.bold("hybrid") + "):");
|
|
239
|
-
if (plat === "
|
|
547
|
+
if (plat === "linux") {
|
|
548
|
+
info(" 1. Binario precompilado " + c.dim("(recomendado)") + ": baja " + c.bold("engram_<ver>_linux_<amd64|arm64>.tar.gz") + " de");
|
|
549
|
+
info(" " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
|
|
550
|
+
info(" " + c.bold("tar -xzf engram_*_linux_*.tar.gz && mv engram ~/.local/bin/ && chmod +x ~/.local/bin/engram"));
|
|
551
|
+
info(" " + c.dim('(asegúrate que ~/.local/bin esté en tu PATH: export PATH="$HOME/.local/bin:$PATH")'));
|
|
552
|
+
info(" " + c.dim("o, con Go 1.24+: ") + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
553
|
+
} else if (plat === "darwin") {
|
|
240
554
|
info(" 1. " + c.bold("brew install gentleman-programming/tap/engram") + " " + c.dim("(o binario: github.com/Gentleman-Programming/engram/releases)"));
|
|
241
555
|
} else if (plat === "win32") {
|
|
242
556
|
info(" 1. " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest") + " " + c.dim("(requiere Go 1.24+)"));
|
|
@@ -455,9 +769,15 @@ export function doctor(cwd) {
|
|
|
455
769
|
const jarvis = detectJarvis(cwd);
|
|
456
770
|
// jarvis es opt-in (--no-jarvis): informativo, no cuenta como fallo.
|
|
457
771
|
add("ozali-jarvis", true, jarvis.present ? `configurado (${jarvis.where.join(", ")})` : "no configurado (--no-jarvis)");
|
|
458
|
-
const
|
|
772
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
773
|
+
const cloudOn = !!(cfg && cfg.cloud && cfg.cloud.enabled) || !!(cloudMeta && cloudMeta.enrolled);
|
|
459
774
|
// Cloud es opt-in: "off" es un estado válido (no cuenta como fallo).
|
|
460
|
-
|
|
775
|
+
const cloudDetail = cloudOn
|
|
776
|
+
? [`enrolado → ${firstNonEmpty(cloudMeta && cloudMeta.server, cfg && cfg.cloud && cfg.cloud.server) || "server por defecto"}`,
|
|
777
|
+
hasCloudToken() ? c.green("token ✓") : c.yellow("sin token"),
|
|
778
|
+
cloudMeta && cloudMeta.dashboard ? c.cyan(cloudMeta.dashboard) : ""].filter(Boolean).join(" · ")
|
|
779
|
+
: "off (opt-in, git-sync activo)";
|
|
780
|
+
add("Engram Cloud", true, cloudDetail);
|
|
461
781
|
add("Repo de conocimiento", !!(cfg && cfg.knowledgeRepo && exists(cfg.knowledgeRepo)), cfg ? cfg.knowledgeRepo : "sin configurar (ozali init)");
|
|
462
782
|
|
|
463
783
|
// Strict TDD (de la fuente de verdad)
|
|
@@ -494,6 +814,42 @@ export function doctor(cwd) {
|
|
|
494
814
|
}
|
|
495
815
|
}
|
|
496
816
|
|
|
817
|
+
// Estado detallado de Engram Cloud (si está habilitado).
|
|
818
|
+
if (cloudOn && env.engram.available) {
|
|
819
|
+
const project = (cfg && cfg.project) || projectName(cwd);
|
|
820
|
+
const snap = cloudStatusSnapshot(cwd, project);
|
|
821
|
+
const hasData = snap.syncStatus || snap.upgradeStatus || snap.conflictsStats;
|
|
822
|
+
if (hasData) {
|
|
823
|
+
step("Estado de Engram Cloud");
|
|
824
|
+
if (snap.syncStatus) {
|
|
825
|
+
info("Sync:");
|
|
826
|
+
printIndented(snap.syncStatus);
|
|
827
|
+
// Warnings específicos por reason_code
|
|
828
|
+
const reason = extractReasonCode(snap.syncStatus);
|
|
829
|
+
if (reason === "blocked_unenrolled") warn("El proyecto no está enrolado en el servidor cloud. Corre " + c.bold("ozali init") + " para re-enrolarlo.");
|
|
830
|
+
if (reason === "transport_failed") warn("No se pudo conectar al servidor cloud (transport_failed). Verifica la URL y tu conexión.");
|
|
831
|
+
}
|
|
832
|
+
if (snap.upgradeStatus) {
|
|
833
|
+
info("Upgrade:");
|
|
834
|
+
printIndented(snap.upgradeStatus);
|
|
835
|
+
// Fase 3.2: sugerir upgrade si el estado no es bootstrap_verified
|
|
836
|
+
const upgradeReason = extractReasonCode(snap.upgradeStatus);
|
|
837
|
+
if (upgradeReason && upgradeReason !== "bootstrap_verified") {
|
|
838
|
+
warn(`El proyecto requiere upgrade de cloud (estado: ${upgradeReason}). Corre ${c.bold("ozali cloud upgrade")}.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (snap.conflictsStats) {
|
|
842
|
+
info("Conflictos:");
|
|
843
|
+
printIndented(snap.conflictsStats);
|
|
844
|
+
// Fase 4.2: advertir conflictos pendientes
|
|
845
|
+
const pendingMatch = snap.conflictsStats.match(/pending\s*[:=]\s*(\d+)/i);
|
|
846
|
+
const pending = pendingMatch ? parseInt(pendingMatch[1], 10) : 0;
|
|
847
|
+
if (pending > 0) warn(`${pending} conflicto(s) de memoria sin juzgar. Usa ${c.bold("ozali audit --conflicts")}.`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (cloudMeta && cloudMeta.dashboard) info(`Dashboard: ${c.cyan(cloudMeta.dashboard)}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
497
853
|
// Tendencia de uso de tokens (la escribe cdk en cada hito; informativo).
|
|
498
854
|
const metrics = readJSON(path.join(cwd, ".ozali", "metrics", "token-metrics.json"));
|
|
499
855
|
if (metrics && Array.isArray(metrics.hits) && metrics.hits.length) {
|
|
@@ -528,23 +884,75 @@ function readStrictTdd(cwd, sot) {
|
|
|
528
884
|
}
|
|
529
885
|
|
|
530
886
|
// =========================================================== update ==========
|
|
531
|
-
|
|
532
|
-
|
|
887
|
+
// Lleva una instalación existente al paquete actual: refresca la skill ozali (con sus
|
|
888
|
+
// references), los perfiles de permisos y **crea/refresca ozali-jarvis** (clave para repos
|
|
889
|
+
// inicializados antes de 0.4.0). La skill `cdk` la regenera el AGENTE (no el CLI): se detecta
|
|
890
|
+
// y se guía la regeneración.
|
|
891
|
+
export function update(cwd, opts = {}) {
|
|
892
|
+
step("ozali update — actualizar la instalación al paquete actual");
|
|
533
893
|
const env = detectAll(cwd);
|
|
534
|
-
if (!env.skill.installed) { warn("No hay skill ozali instalada. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
535
|
-
for (const p of env.skill.paths) {
|
|
536
|
-
copyDir(SKILL_SRC, p);
|
|
537
|
-
ok(`Actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
|
|
538
|
-
}
|
|
539
894
|
const cfgPath = CONFIG_PATH(cwd);
|
|
540
895
|
const cfg = readJSON(cfgPath);
|
|
896
|
+
if (!env.skill.installed && !cfg) {
|
|
897
|
+
warn("No hay instalación de ozali en esta ruta. Corre " + c.bold("ozali init") + " primero.");
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// 1) Skill ozali (incluye las references: la base desde la que el agente regenera cdk)
|
|
902
|
+
if (env.skill.installed) {
|
|
903
|
+
for (const p of env.skill.paths) {
|
|
904
|
+
copyDir(SKILL_SRC, p);
|
|
905
|
+
ok(`Skill ozali actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
warn("Skill ozali no instalada en esta ruta (corre " + c.bold("ozali init") + " para instalarla).");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Agente/scope: del config; si falta, infiere del entorno.
|
|
912
|
+
const agent = (cfg && cfg.agent) || (env.agents.opencode.present && !env.agents.claudeCode.present ? "opencode"
|
|
913
|
+
: env.agents.claudeCode.present && env.agents.opencode.present ? "both" : "claude-code");
|
|
914
|
+
const scope = (cfg && cfg.scope) || "project";
|
|
915
|
+
|
|
916
|
+
// 2) Perfiles base de permisos (idempotente: recoge defaults nuevos del paquete)
|
|
917
|
+
if (agent === "claude-code" || agent === "both") ensureClaudeCodeProfile(cwd, scope);
|
|
918
|
+
if (agent === "opencode" || agent === "both") ensureOpencodeProfile(cwd);
|
|
919
|
+
|
|
920
|
+
// 3) ozali-jarvis: crea el orquestador en repos previos a 0.4.0 y refresca el resto.
|
|
921
|
+
if (!opts.noJarvis) {
|
|
922
|
+
const proj = projectName(cwd);
|
|
923
|
+
const engPath = ENGRAM_CONFIG_PATH(cwd);
|
|
924
|
+
if (!exists(engPath)) { writeJSON(engPath, { project_name: proj }); ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`); }
|
|
925
|
+
if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
|
|
926
|
+
if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 4) Skill cdk: la genera el AGENTE (Fase 6), el CLI no la regenera.
|
|
930
|
+
const cdk = detectCdk(cwd);
|
|
931
|
+
if (cdk.installed) {
|
|
932
|
+
step("Skill cdk (generada por el agente)");
|
|
933
|
+
warn("cdk no se actualiza desde el CLI: la regenera tu agente con el contrato nuevo.");
|
|
934
|
+
info("Abre tu agente y vuelve a correr la skill " + c.bold("ozali") + " para regenerar cdk (ozali-jarvis, recall-first §7, telemetría).");
|
|
935
|
+
info("Tus docs por hito (" + c.bold(".ozali/docs/cdk/") + ") y el plan congelado se conservan.");
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 5) versión del config
|
|
541
939
|
if (cfg) { cfg.version = pkgVersion(); cfg.updatedAt = new Date().toISOString(); writeJSON(cfgPath, cfg); }
|
|
940
|
+
ok(`Instalación al día con ozali v${pkgVersion()}.`);
|
|
542
941
|
return 0;
|
|
543
942
|
}
|
|
544
943
|
|
|
944
|
+
/** ¿Existe la skill cdk (generada por el agente)? */
|
|
945
|
+
function detectCdk(cwd) {
|
|
946
|
+
const paths = [
|
|
947
|
+
path.join(cwd, ".claude", "skills", "cdk", "SKILL.md"),
|
|
948
|
+
path.join(HOME, ".claude", "skills", "cdk", "SKILL.md"),
|
|
949
|
+
].filter(exists);
|
|
950
|
+
return { installed: paths.length > 0, paths };
|
|
951
|
+
}
|
|
952
|
+
|
|
545
953
|
// ============================================================= sync ===========
|
|
546
954
|
export function sync(cwd, opts) {
|
|
547
|
-
step(`ozali sync${opts.import ? " --import" : ""} — histórico ↔ repo de conocimiento`);
|
|
955
|
+
step(`ozali sync${opts.import ? " --import" : ""}${opts.cloud ? " --cloud" : ""} — histórico ↔ repo de conocimiento`);
|
|
548
956
|
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
549
957
|
if (!cfg || !cfg.knowledgeRepo) { warn("Sin repo de conocimiento configurado. Corre " + c.bold("ozali init") + "."); return 1; }
|
|
550
958
|
const kRepo = cfg.knowledgeRepo;
|
|
@@ -556,11 +964,48 @@ export function sync(cwd, opts) {
|
|
|
556
964
|
|
|
557
965
|
// Engram Cloud (opt-in): réplica bidireccional adicional al git-sync.
|
|
558
966
|
if (opts.cloud) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
967
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
968
|
+
const cloudEnabled = (cfg.cloud && cfg.cloud.enabled) || (cloudMeta && cloudMeta.enrolled);
|
|
969
|
+
if (cloudEnabled && tryExec("engram", ["--version"])) {
|
|
970
|
+
if (!hasCloudToken() && cloudMeta && cloudMeta.auth_required !== false) {
|
|
971
|
+
warn(`El servidor cloud puede requerir autenticación. Define ${c.bold(CLOUD_TOKEN_ENV)} si falla el sync.`);
|
|
972
|
+
}
|
|
973
|
+
const cloudServer = firstNonEmpty(cloudMeta && cloudMeta.server, cfg.cloud && cfg.cloud.server);
|
|
974
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
975
|
+
|
|
976
|
+
if (opts.import) {
|
|
977
|
+
// --- onboarding inverso: pull desde cloud ---
|
|
978
|
+
info(`Recibiendo memoria del equipo desde Engram Cloud (proyecto "${project}")…`);
|
|
979
|
+
const pullOut = tryExec("engram", ["sync", "--cloud", "--import", "--project", project], { cwd });
|
|
980
|
+
if (pullOut !== null) {
|
|
981
|
+
ok("Pull desde Engram Cloud completado.");
|
|
982
|
+
if (pullOut.trim()) printIndented(pullOut);
|
|
983
|
+
// Importar los chunks locales que llegaron vía cloud
|
|
984
|
+
if (spawnCmd("engram", ["sync", "--import"], { cwd }) === 0) ok("Memorias cloud importadas a Engram local.");
|
|
985
|
+
else warn("engram sync --import no terminó correctamente tras el pull cloud.");
|
|
986
|
+
// Verificación
|
|
987
|
+
const verify = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
988
|
+
if (verify) { info("Estado de cloud:"); printIndented(verify); }
|
|
989
|
+
} else {
|
|
990
|
+
const reason = extractReasonCode(pullOut);
|
|
991
|
+
warn(`engram sync --cloud --import no terminó correctamente${reason ? ` (motivo: ${reason})` : ""}.`);
|
|
992
|
+
if (reason === "blocked_unenrolled") info("El proyecto no está enrolado en el servidor. Corre " + c.bold("ozali init") + " para enrolarlo.");
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
// --- push a cloud (default) ---
|
|
996
|
+
info(`Replicando con Engram Cloud (proyecto "${project}")…`);
|
|
997
|
+
const out = syncCloudProject(cwd, project);
|
|
998
|
+
if (out !== null) {
|
|
999
|
+
ok("Réplica con Engram Cloud completada.");
|
|
1000
|
+
if (out.trim()) printIndented(out);
|
|
1001
|
+
} else {
|
|
1002
|
+
const reason = extractReasonCode(out);
|
|
1003
|
+
warn(`engram sync --cloud no terminó correctamente${reason ? ` (motivo: ${reason})` : ""}. Revisa el output de arriba.`);
|
|
1004
|
+
if (reason === "blocked_unenrolled") info("El proyecto no está enrolado en el servidor. Corre " + c.bold("ozali init") + " para enrolarlo.");
|
|
1005
|
+
if (reason === "transport_failed") info("No se pudo conectar al servidor cloud. Verifica la URL y tu conexión.");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} else if (!cloudEnabled) {
|
|
564
1009
|
warn("--cloud pedido, pero Engram Cloud no está habilitado. Corre " + c.bold("ozali init") + " para habilitarlo, o usa git-sync sin --cloud.");
|
|
565
1010
|
} else {
|
|
566
1011
|
warn("--cloud pedido, pero Engram no responde. Omito la réplica cloud.");
|
|
@@ -640,7 +1085,7 @@ export async function audit(cwd, opts) {
|
|
|
640
1085
|
step("ozali audit — auditoría de memoria (Engram)");
|
|
641
1086
|
const env = detectAll(cwd);
|
|
642
1087
|
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
643
|
-
const engramCfg = readJSON(
|
|
1088
|
+
const engramCfg = readJSON(ENGRAM_CONFIG_PATH(cwd));
|
|
644
1089
|
const project = (engramCfg && engramCfg.project_name) || (cfg && cfg.project) || projectName(cwd);
|
|
645
1090
|
const hasProjectContext = env.git.isRepo || !!cfg || !!engramCfg;
|
|
646
1091
|
|
|
@@ -660,6 +1105,34 @@ export async function audit(cwd, opts) {
|
|
|
660
1105
|
return auditFromDocs(cwd, project);
|
|
661
1106
|
}
|
|
662
1107
|
|
|
1108
|
+
// Fase 4.1: --dashboard abre el dashboard de Engram Cloud
|
|
1109
|
+
if (opts.dashboard) {
|
|
1110
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
1111
|
+
const dash = (cloudMeta && cloudMeta.dashboard) || (cloudMeta && cloudMeta.server ? cloudDashboardURL(cloudMeta.server) : null);
|
|
1112
|
+
if (!dash) { warn("No hay Engram Cloud configurado. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
1113
|
+
info(`Abriendo dashboard: ${c.cyan(dash)}`);
|
|
1114
|
+
openURL(dash);
|
|
1115
|
+
return 0;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Fase 4.1: --conflicts lista/stats conflictos de memoria
|
|
1119
|
+
if (opts.conflicts) {
|
|
1120
|
+
const projArg = scope === "project" ? ["--project", project] : [];
|
|
1121
|
+
if (opts.stats) {
|
|
1122
|
+
step(`Estadísticas de conflictos${scope === "project" ? ` — ${project}` : ""}`);
|
|
1123
|
+
const out = tryExec("engram", ["conflicts", "stats", ...projArg], { cwd });
|
|
1124
|
+
if (out) printIndented(out);
|
|
1125
|
+
else warn("No se pudieron obtener estadísticas de conflictos.");
|
|
1126
|
+
} else {
|
|
1127
|
+
const statusFlag = opts.judged ? ["--status", "judged"] : [];
|
|
1128
|
+
step(`Conflictos${opts.judged ? " juzgados" : " pendientes"}${scope === "project" ? ` — ${project}` : ""}`);
|
|
1129
|
+
const out = tryExec("engram", ["conflicts", "list", ...statusFlag, ...projArg], { cwd });
|
|
1130
|
+
if (out) printIndented(out);
|
|
1131
|
+
else warn("No se pudieron listar conflictos.");
|
|
1132
|
+
}
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
663
1136
|
// Navegador interactivo.
|
|
664
1137
|
if (opts.tui) {
|
|
665
1138
|
info("Abriendo el navegador interactivo de Engram (engram tui)…");
|
|
@@ -705,3 +1178,100 @@ function auditFromDocs(cwd, project) {
|
|
|
705
1178
|
info("Instala Engram para auditoría buscable/acumulativa (" + c.bold("ozali doctor") + ").");
|
|
706
1179
|
return 0;
|
|
707
1180
|
}
|
|
1181
|
+
|
|
1182
|
+
// ============================================================= cloud ===========
|
|
1183
|
+
export async function cloud(cwd, opts) {
|
|
1184
|
+
const sub = opts._[1] || "status";
|
|
1185
|
+
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
1186
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
1187
|
+
const project = (cfg && cfg.project) || projectName(cwd);
|
|
1188
|
+
const cloudServer = firstNonEmpty(cloudMeta && cloudMeta.server, cfg && cfg.cloud && cfg.cloud.server);
|
|
1189
|
+
|
|
1190
|
+
switch (sub) {
|
|
1191
|
+
case "status": {
|
|
1192
|
+
step(`ozali cloud status — proyecto "${project}"`);
|
|
1193
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1194
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1195
|
+
const status = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
1196
|
+
if (status) { info("Estado:"); printIndented(status); }
|
|
1197
|
+
else { warn("No se pudo obtener el estado de cloud."); }
|
|
1198
|
+
const upgrade = tryExec("engram", ["cloud", "upgrade", "status", "--project", project], { cwd });
|
|
1199
|
+
if (upgrade) { info("Upgrade:"); printIndented(upgrade); }
|
|
1200
|
+
return 0;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
case "upgrade": {
|
|
1204
|
+
step(`ozali cloud upgrade — proyecto "${project}"`);
|
|
1205
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1206
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1207
|
+
// 1) doctor (checkpoint)
|
|
1208
|
+
info("Paso 1/3: Verificando estado actual…");
|
|
1209
|
+
const status = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
1210
|
+
if (status) printIndented(status);
|
|
1211
|
+
// 2) repair --dry-run
|
|
1212
|
+
info("Paso 2/3: Simulando repair (dry-run)…");
|
|
1213
|
+
const dryRun = tryExec("engram", ["cloud", "upgrade", "repair", "--dry-run", "--project", project], { cwd });
|
|
1214
|
+
if (dryRun) printIndented(dryRun);
|
|
1215
|
+
const doApply = await confirm("¿Aplicar el repair ahora?", true);
|
|
1216
|
+
if (!doApply) { info("Upgrade cancelado. Puedes aplicarlo después con " + c.bold("ozali cloud repair") + "."); return 0; }
|
|
1217
|
+
// 3) repair --apply + bootstrap
|
|
1218
|
+
info("Paso 3/3: Aplicando repair…");
|
|
1219
|
+
const repairOut = tryExec("engram", ["cloud", "upgrade", "repair", "--apply", "--project", project], { cwd });
|
|
1220
|
+
if (repairOut !== null) { ok("Repair aplicado."); if (repairOut.trim()) printIndented(repairOut); }
|
|
1221
|
+
else { warn("Repair falló. Revisa el output de arriba."); return 1; }
|
|
1222
|
+
const boot = tryExec("engram", ["cloud", "upgrade", "bootstrap", "--project", project], { cwd });
|
|
1223
|
+
if (boot !== null) { ok("Bootstrap completado."); if (boot.trim()) printIndented(boot); }
|
|
1224
|
+
else warn("Bootstrap falló. Corre " + c.bold("ozali cloud status") + " para ver el estado.");
|
|
1225
|
+
return 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
case "repair": {
|
|
1229
|
+
step(`ozali cloud repair — proyecto "${project}"`);
|
|
1230
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1231
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1232
|
+
const out = tryExec("engram", ["cloud", "upgrade", "repair", "--apply", "--project", project], { cwd });
|
|
1233
|
+
if (out !== null) { ok("Repair aplicado."); if (out.trim()) printIndented(out); return 0; }
|
|
1234
|
+
warn("Repair falló. Revisa el output de arriba.");
|
|
1235
|
+
return 1;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
case "dashboard": {
|
|
1239
|
+
const dash = cloudMeta && cloudMeta.dashboard ? cloudMeta.dashboard : (cloudServer ? cloudDashboardURL(cloudServer) : null);
|
|
1240
|
+
if (!dash) { warn("No hay servidor cloud configurado. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
1241
|
+
info(`Abriendo dashboard: ${c.cyan(dash)}`);
|
|
1242
|
+
openURL(dash);
|
|
1243
|
+
return 0;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
case "config": {
|
|
1247
|
+
step("ozali cloud config");
|
|
1248
|
+
if (cloudMeta && cloudMeta.enrolled) {
|
|
1249
|
+
info(`Servidor: ${c.bold(cloudMeta.server || "no definido")}`);
|
|
1250
|
+
info(`Proyecto: ${c.bold(cloudMeta.project || project)}`);
|
|
1251
|
+
info(`Token: ${hasCloudToken() ? c.green("✓ configurado") : c.yellow("✗ no configurado")}`);
|
|
1252
|
+
info(`Dashboard: ${cloudMeta.dashboard ? c.cyan(cloudMeta.dashboard) : "no disponible"}`);
|
|
1253
|
+
info(`Autosync: ${c.bold(CLOUD_AUTOSYNC_ENV)}=${process.env[CLOUD_AUTOSYNC_ENV] || "no definido"}`);
|
|
1254
|
+
const reconfig = await confirm("¿Reconfigurar el servidor?", false);
|
|
1255
|
+
if (!reconfig) return 0;
|
|
1256
|
+
}
|
|
1257
|
+
const server = await ask("URL del servidor de Engram Cloud", cloudMeta && cloudMeta.server || "http://127.0.0.1:18080");
|
|
1258
|
+
const token = await ask("Token de autenticación");
|
|
1259
|
+
if (!token) { warn("Sin token no se puede configurar (modo autenticado obligatorio)."); return 1; }
|
|
1260
|
+
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
1261
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
1262
|
+
if (spawnCmd("engram", ["cloud", "enroll", project]) === 0) {
|
|
1263
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
1264
|
+
configureCloudAutosync(cwd, opts);
|
|
1265
|
+
persistCloudToken(token, opts);
|
|
1266
|
+
ok("Engram Cloud reconfigurado.");
|
|
1267
|
+
return 0;
|
|
1268
|
+
}
|
|
1269
|
+
warn("No se pudo enrolar el proyecto. Verifica el servidor y el token.");
|
|
1270
|
+
return 1;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
default:
|
|
1274
|
+
err(`Subcomando desconocido "${sub}". Usa: status | upgrade | repair | dashboard | config`);
|
|
1275
|
+
return 1;
|
|
1276
|
+
}
|
|
1277
|
+
}
|