ozali 0.4.1 → 0.6.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 -9
- package/cli/bin/ozali.mjs +16 -5
- package/cli/lib/commands.mjs +614 -30
- package/cli/lib/detect.mjs +9 -1
- package/cli/lib/util.mjs +24 -0
- package/cli/test/smoke.test.mjs +124 -0
- package/docs/deploy-cloud-gcloud.md +149 -0
- package/docs/deploy-cloud-vps.md +174 -0
- package/docs/ideas/postgres-vs-mysql-engram-cloud.md +137 -0
- package/docs/team-history.md +52 -0
- package/package.json +2 -1
- package/skill/SKILL.md +48 -3
- package/skill/references/cdk-contract.md +72 -0
- package/skill/references/engram-convention.md +78 -0
- package/skill-commit/SKILL.md +81 -0
package/cli/lib/commands.mjs
CHANGED
|
@@ -1,22 +1,92 @@
|
|
|
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
|
-
SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
|
|
7
|
-
ensureGitignore, tryExec, spawnCmd, which,
|
|
8
|
-
projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME,
|
|
7
|
+
SKILL_SRC, COMMIT_SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
|
|
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 commitSkillTarget(cwd, scope) {
|
|
27
|
+
const base = scope === "global" ? path.join(process.env.HOME || "", ".claude") : path.join(cwd, ".claude");
|
|
28
|
+
return path.join(base, "skills", "ozali-commit");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readTeamCloud(cwd) {
|
|
32
|
+
return readJSON(TEAM_CLOUD_PATH(cwd));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeTeamCloud(cwd, cloud) {
|
|
36
|
+
writeJSON(TEAM_CLOUD_PATH(cwd), {
|
|
37
|
+
server: cloud.server,
|
|
38
|
+
project: cloud.project,
|
|
39
|
+
enrolled: !!cloud.enabled,
|
|
40
|
+
auth_required: cloud.authRequired !== false,
|
|
41
|
+
token_env: cloud.tokenEnv || CLOUD_TOKEN_ENV,
|
|
42
|
+
autosync_env: cloud.autosyncEnv || CLOUD_AUTOSYNC_ENV,
|
|
43
|
+
dashboard: cloudDashboardURL(cloud.server),
|
|
44
|
+
updated_at: new Date().toISOString(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function firstNonEmpty(...values) {
|
|
49
|
+
return values.find((value) => typeof value === "string" && value.trim()) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function firstLine(text, fallback = "") {
|
|
53
|
+
if (!text) return fallback;
|
|
54
|
+
const line = String(text).split(/\r?\n/).map((part) => part.trim()).find(Boolean);
|
|
55
|
+
return line || fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printIndented(text) {
|
|
59
|
+
if (!text) return;
|
|
60
|
+
for (const line of String(text).split(/\r?\n/)) console.log(` ${c.dim(line)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cloudDashboardURL(server) {
|
|
64
|
+
if (!server) return null;
|
|
65
|
+
return server.replace(/\/+$/, "") + "/dashboard";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasCloudToken() {
|
|
69
|
+
return !!firstNonEmpty(process.env[CLOUD_TOKEN_ENV]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractReasonCode(text) {
|
|
73
|
+
if (!text) return null;
|
|
74
|
+
const match = firstLine(text).match(/(?:reason(?:_code)?|reasonCode)\s*[:=]\s*([a-z_]+)/i);
|
|
75
|
+
return match ? match[1].toLowerCase() : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function syncCloudProject(cwd, project) {
|
|
79
|
+
return tryExec("engram", ["sync", "--cloud", "--project", project], { cwd });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cloudStatusSnapshot(cwd, project) {
|
|
83
|
+
return {
|
|
84
|
+
syncStatus: tryExec("engram", ["sync", "--cloud", "--status", "--project", project], { cwd }),
|
|
85
|
+
upgradeStatus: tryExec("engram", ["cloud", "upgrade", "status", "--project", project], { cwd }),
|
|
86
|
+
conflictsStats: tryExec("engram", ["conflicts", "stats", "--project", project], { cwd }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
20
90
|
// ============================================================ init ===========
|
|
21
91
|
export async function init(cwd, opts) {
|
|
22
92
|
step("ozali init — bootstrap del proyecto");
|
|
@@ -88,18 +158,27 @@ export async function init(cwd, opts) {
|
|
|
88
158
|
// Engram Cloud (opt-in) — réplica de equipo además del git-sync. Solo si Engram quedó disponible.
|
|
89
159
|
let cloud = { enabled: false };
|
|
90
160
|
if (memoryMode === "hybrid" && !opts.dryRun) {
|
|
91
|
-
|
|
161
|
+
const teamCloud = readTeamCloud(cwd);
|
|
162
|
+
if (teamCloud && teamCloud.enrolled) {
|
|
163
|
+
// Fase 1: dev nuevo en un repo que ya tiene .ozali/cloud.json del equipo
|
|
164
|
+
cloud = await connectTeamCloud(cwd, teamCloud, opts);
|
|
165
|
+
} else {
|
|
166
|
+
cloud = await maybeEnableEngramCloud(cwd, projectName(cwd), opts);
|
|
167
|
+
}
|
|
92
168
|
}
|
|
93
169
|
|
|
94
170
|
if (opts.dryRun) { warn("--dry-run: no escribo nada. Plan mostrado arriba."); return 0; }
|
|
95
171
|
|
|
96
172
|
// --- acciones ---
|
|
97
173
|
step("Aplicando");
|
|
98
|
-
// 1) copiar skill
|
|
174
|
+
// 1) copiar skill ozali (bootstrap) + ozali-commit (commit convencional)
|
|
99
175
|
const target = skillTarget(cwd, scope);
|
|
100
176
|
ensureDir(path.dirname(target));
|
|
101
177
|
copyDir(SKILL_SRC, target);
|
|
102
178
|
ok(`Skill instalada en ${c.bold(path.relative(cwd, target) || target)}.`);
|
|
179
|
+
const commitTarget = commitSkillTarget(cwd, scope);
|
|
180
|
+
copyDir(COMMIT_SKILL_SRC, commitTarget);
|
|
181
|
+
ok(`Skill ${c.bold("ozali-commit")} instalada en ${c.bold(path.relative(cwd, commitTarget) || commitTarget)}.`);
|
|
103
182
|
|
|
104
183
|
// 2) perfiles base de permisos (idempotentes, merge mínimo) por agente
|
|
105
184
|
if (agent === "claude-code" || agent === "both") {
|
|
@@ -115,7 +194,7 @@ export async function init(cwd, opts) {
|
|
|
115
194
|
if (!opts.noJarvis) {
|
|
116
195
|
const proj = projectName(cwd);
|
|
117
196
|
// Fija el proyecto para escrituras deterministas de memoria (se deriva igual por cada miembro).
|
|
118
|
-
writeJSON(
|
|
197
|
+
writeJSON(ENGRAM_CONFIG_PATH(cwd), { project_name: proj });
|
|
119
198
|
ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`);
|
|
120
199
|
if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
|
|
121
200
|
if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
|
|
@@ -123,7 +202,7 @@ export async function init(cwd, opts) {
|
|
|
123
202
|
|
|
124
203
|
// 3) gitignore del histórico aislado
|
|
125
204
|
if (env.git.isRepo) {
|
|
126
|
-
const { added } = ensureGitignore(cwd, [".ozali/", ".engram/"]);
|
|
205
|
+
const { added } = ensureGitignore(cwd, [".ozali/*", "!.ozali/cloud.json", ".engram/"]);
|
|
127
206
|
if (added.length) ok(`.gitignore actualizado: ${added.join(", ")} (histórico aislado del repo principal).`);
|
|
128
207
|
else info(".gitignore ya aislaba el histórico.");
|
|
129
208
|
}
|
|
@@ -167,14 +246,28 @@ export async function init(cwd, opts) {
|
|
|
167
246
|
|
|
168
247
|
/**
|
|
169
248
|
* Instala Engram con la mejor ruta disponible para el SO actual.
|
|
170
|
-
*
|
|
249
|
+
* Linux: binario precompilado (auto-descarga, sin toolchain) con brew/go como alternativas.
|
|
250
|
+
* macOS: Homebrew → Go → binario precompilado como fallback.
|
|
171
251
|
* Windows: Go install (recomendado) o binario manual.
|
|
172
252
|
* Devuelve true si el binario quedó disponible en PATH.
|
|
173
253
|
*/
|
|
174
254
|
function installEngram() {
|
|
175
255
|
const plat = process.platform;
|
|
176
256
|
|
|
177
|
-
if (plat === "
|
|
257
|
+
if (plat === "linux") {
|
|
258
|
+
// En Linux Homebrew es poco común: el binario precompilado es la vía universal sin toolchain.
|
|
259
|
+
if (installEngramFromTarball()) return true;
|
|
260
|
+
if (which("brew")) {
|
|
261
|
+
info("Instalando: " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
262
|
+
spawnCmd("brew", ["install", "gentleman-programming/tap/engram"]);
|
|
263
|
+
} else if (which("go")) {
|
|
264
|
+
info("Compilando con Go: " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
265
|
+
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
266
|
+
} else {
|
|
267
|
+
warn("No pude instalar Engram automáticamente. Sigue las instrucciones de abajo.");
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
} else if (plat === "darwin") {
|
|
178
271
|
if (which("brew")) {
|
|
179
272
|
info("Instalando: " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
180
273
|
spawnCmd("brew", ["install", "gentleman-programming/tap/engram"]);
|
|
@@ -182,8 +275,10 @@ function installEngram() {
|
|
|
182
275
|
info("Homebrew no encontrado — instalando con Go:");
|
|
183
276
|
info(" " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
184
277
|
spawnCmd("go", ["install", "github.com/Gentleman-Programming/engram/cmd/engram@latest"]);
|
|
278
|
+
} else if (installEngramFromTarball()) {
|
|
279
|
+
return true;
|
|
185
280
|
} else {
|
|
186
|
-
warn("No se encontró Homebrew ni Go. Opciones
|
|
281
|
+
warn("No se encontró Homebrew ni Go y falló la descarga del binario. Opciones:");
|
|
187
282
|
info(" a) Homebrew " + c.dim("(recomendado)") + ": " + c.cyan("https://brew.sh") + " → " + c.bold("brew install gentleman-programming/tap/engram"));
|
|
188
283
|
info(" b) Binario precompilado: " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
|
|
189
284
|
return false;
|
|
@@ -210,11 +305,159 @@ function installEngram() {
|
|
|
210
305
|
return false;
|
|
211
306
|
}
|
|
212
307
|
|
|
308
|
+
const ENGRAM_RELEASES_API = "https://api.github.com/repos/Gentleman-Programming/engram/releases/latest";
|
|
309
|
+
const ENGRAM_RELEASES_DL = "https://github.com/Gentleman-Programming/engram/releases/download";
|
|
310
|
+
|
|
311
|
+
/** Última versión publicada de Engram (sin la "v" inicial), o null si no hay red. */
|
|
312
|
+
function latestEngramVersion() {
|
|
313
|
+
let raw = null;
|
|
314
|
+
if (which("curl")) raw = tryExec("curl", ["-fsSL", ENGRAM_RELEASES_API]);
|
|
315
|
+
else if (which("wget")) raw = tryExec("wget", ["-qO-", ENGRAM_RELEASES_API]);
|
|
316
|
+
if (!raw) return null;
|
|
317
|
+
try {
|
|
318
|
+
const tag = JSON.parse(raw).tag_name;
|
|
319
|
+
return tag ? String(tag).replace(/^v/, "") : null;
|
|
320
|
+
} catch { return null; }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Descarga url → dest con curl (o wget como fallback). Devuelve true si tuvo éxito. */
|
|
324
|
+
function download(url, dest) {
|
|
325
|
+
if (which("curl")) return spawnCmd("curl", ["-fL", "--retry", "2", "-o", dest, url]) === 0;
|
|
326
|
+
if (which("wget")) return spawnCmd("wget", ["-O", dest, url]) === 0;
|
|
327
|
+
warn("No se encontró curl ni wget para descargar el binario de Engram.");
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Busca recursivamente un ejecutable llamado "engram" dentro de dir (1 nivel basta). */
|
|
332
|
+
function findEngramBinary(dir) {
|
|
333
|
+
let entries;
|
|
334
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
|
335
|
+
for (const e of entries) {
|
|
336
|
+
const full = path.join(dir, e.name);
|
|
337
|
+
if (e.isFile() && e.name === "engram") return full;
|
|
338
|
+
if (e.isDirectory()) {
|
|
339
|
+
const nested = findEngramBinary(full);
|
|
340
|
+
if (nested) return nested;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Instala Engram bajando el binario precompilado del release más reciente (linux/macOS).
|
|
348
|
+
* Lo coloca en ~/.local/bin (sin sudo) y lo antepone al PATH del proceso para que los
|
|
349
|
+
* `engram setup <agente>` posteriores lo encuentren en esta misma corrida.
|
|
350
|
+
* Node-14-safe: usa curl/wget + tar vía execFileSync (sin fetch global). Devuelve true si quedó listo.
|
|
351
|
+
*/
|
|
352
|
+
function installEngramFromTarball() {
|
|
353
|
+
const plat = process.platform;
|
|
354
|
+
if (plat !== "linux" && plat !== "darwin") return false;
|
|
355
|
+
|
|
356
|
+
// Falla rápido si la arquitectura no tiene binario publicado (versión irrelevante para esta validación).
|
|
357
|
+
if (!engramAssetName(plat, process.arch, "0")) {
|
|
358
|
+
warn(`Arquitectura no soportada para el binario precompilado (${process.arch}).`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const version = latestEngramVersion();
|
|
363
|
+
if (!version) {
|
|
364
|
+
warn("No pude resolver la última versión de Engram (¿sin red o sin curl/wget?).");
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const asset = engramAssetName(plat, process.arch, version);
|
|
369
|
+
const url = `${ENGRAM_RELEASES_DL}/v${version}/${asset}`;
|
|
370
|
+
info(`Descargando binario precompilado de Engram ${c.bold("v" + version)} (${process.arch})…`);
|
|
371
|
+
|
|
372
|
+
let tmpDir;
|
|
373
|
+
try { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-engram-")); }
|
|
374
|
+
catch { warn("No pude crear un directorio temporal para la descarga."); return false; }
|
|
375
|
+
|
|
376
|
+
const tarball = path.join(tmpDir, asset);
|
|
377
|
+
if (!download(url, tarball)) { warn("Falló la descarga del binario de Engram."); return false; }
|
|
378
|
+
|
|
379
|
+
if (spawnCmd("tar", ["-xzf", tarball, "-C", tmpDir]) !== 0) {
|
|
380
|
+
warn("Falló la extracción del tarball de Engram (¿tar disponible?).");
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const binSrc = findEngramBinary(tmpDir);
|
|
385
|
+
if (!binSrc) { warn("No encontré el binario engram dentro del tarball."); return false; }
|
|
386
|
+
|
|
387
|
+
const destDir = path.join(HOME, ".local", "bin");
|
|
388
|
+
const dest = path.join(destDir, "engram");
|
|
389
|
+
try {
|
|
390
|
+
ensureDir(destDir);
|
|
391
|
+
fs.copyFileSync(binSrc, dest);
|
|
392
|
+
fs.chmodSync(dest, 0o755);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
warn(`No pude instalar el binario en ${destDir} (${e.message}).`);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Disponible en esta corrida para los `engram setup` que vienen después.
|
|
399
|
+
const onPath = (process.env.PATH || "").split(path.delimiter).includes(destDir);
|
|
400
|
+
if (!onPath) process.env.PATH = destDir + path.delimiter + (process.env.PATH || "");
|
|
401
|
+
|
|
402
|
+
ok(`Engram instalado en ${c.bold(dest)}.`);
|
|
403
|
+
if (!onPath) {
|
|
404
|
+
warn(`${c.bold(destDir)} no estaba en tu PATH. Para usar ${c.bold("engram")} fuera del agente, añádelo a tu shell:`);
|
|
405
|
+
console.log(` ${c.dim('export PATH="$HOME/.local/bin:$PATH"')}`);
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Fase 1: Onboarding de equipo. Un dev nuevo hace `ozali init` en un repo que ya tiene
|
|
412
|
+
* .ozali/cloud.json (commiteable). Detecta la cloud del equipo y ofrece conectarse en 1 paso.
|
|
413
|
+
*/
|
|
414
|
+
async function connectTeamCloud(cwd, cloudMeta, opts) {
|
|
415
|
+
const project = cloudMeta.project || projectName(cwd);
|
|
416
|
+
ok(`Engram Cloud del equipo detectado (servidor: ${c.bold(cloudMeta.server || "por defecto")})`);
|
|
417
|
+
info(` → Proyecto: "${project}"`);
|
|
418
|
+
const connect = await confirm("¿Conectarte a la memoria del equipo?", true);
|
|
419
|
+
if (!connect) {
|
|
420
|
+
info("Cloud del equipo omitido. Puedes conectarte después con " + c.bold("ozali cloud config") + ".");
|
|
421
|
+
return { enabled: false, server: cloudMeta.server };
|
|
422
|
+
}
|
|
423
|
+
const token = await ask("Token de autenticación (ENGRAM_CLOUD_TOKEN)");
|
|
424
|
+
if (!token) {
|
|
425
|
+
warn("Sin token no se puede conectar (modo autenticado obligatorio). Continúo con git-sync.");
|
|
426
|
+
return { enabled: false, server: cloudMeta.server };
|
|
427
|
+
}
|
|
428
|
+
const server = cloudMeta.server || "http://127.0.0.1:18080";
|
|
429
|
+
info(`Configurando Engram Cloud → ${server}`);
|
|
430
|
+
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
431
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
432
|
+
info(`Enrolando el proyecto "${project}"…`);
|
|
433
|
+
if (spawnCmd("engram", ["cloud", "enroll", project]) !== 0) {
|
|
434
|
+
warn("No se pudo enrolar. Verifica el token y el servidor. Continúo con git-sync.");
|
|
435
|
+
return { enabled: false, server };
|
|
436
|
+
}
|
|
437
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
438
|
+
configureCloudAutosync(cwd, opts);
|
|
439
|
+
persistCloudToken(token, opts);
|
|
440
|
+
ok("Conectado a Engram Cloud del equipo.");
|
|
441
|
+
// Recibir la memoria del equipo (pull desde cloud)
|
|
442
|
+
info("Recibiendo memoria del equipo…");
|
|
443
|
+
const pullOut = tryExec("engram", ["sync", "--cloud", "--import", "--project", project], { cwd });
|
|
444
|
+
if (pullOut !== null) {
|
|
445
|
+
ok("Memoria del equipo recibida.");
|
|
446
|
+
if (pullOut.trim()) printIndented(pullOut);
|
|
447
|
+
// Importar chunks locales
|
|
448
|
+
if (spawnCmd("engram", ["sync", "--import"], { cwd }) === 0) ok("Memorias importadas a Engram local.");
|
|
449
|
+
} else {
|
|
450
|
+
warn("No se pudo recibir la memoria del equipo. Corre " + c.bold("ozali sync --cloud --import") + " más tarde.");
|
|
451
|
+
}
|
|
452
|
+
return { enabled: true, server, token };
|
|
453
|
+
}
|
|
454
|
+
|
|
213
455
|
/**
|
|
214
456
|
* Engram Cloud opt-in: réplica de equipo en tiempo real, adicional al git-sync.
|
|
215
|
-
*
|
|
457
|
+
* Modo autenticado obligatorio: siempre pide token. Configura autosync en el agente.
|
|
458
|
+
* Devuelve { enabled, server, token }.
|
|
216
459
|
*/
|
|
217
|
-
async function maybeEnableEngramCloud(project, opts) {
|
|
460
|
+
async function maybeEnableEngramCloud(cwd, project, opts) {
|
|
218
461
|
if (opts.yes) return { enabled: false };
|
|
219
462
|
const enable = await confirm("¿Habilitar Engram Cloud para el equipo? (réplica opt-in, requiere un servidor)", false);
|
|
220
463
|
if (!enable) {
|
|
@@ -222,21 +465,100 @@ async function maybeEnableEngramCloud(project, opts) {
|
|
|
222
465
|
return { enabled: false };
|
|
223
466
|
}
|
|
224
467
|
const server = await ask("URL del servidor de Engram Cloud", "http://127.0.0.1:18080");
|
|
468
|
+
const token = await ask("Token de autenticación (ENGRAM_CLOUD_TOKEN)");
|
|
469
|
+
if (!token) {
|
|
470
|
+
warn("Sin token no se puede configurar Engram Cloud (modo autenticado obligatorio). Continúo con git-sync.");
|
|
471
|
+
return { enabled: false, server };
|
|
472
|
+
}
|
|
225
473
|
info(`Configurando Engram Cloud → ${server}`);
|
|
226
474
|
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
475
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
227
476
|
info(`Enrolando el proyecto "${project}"…`);
|
|
228
477
|
if (spawnCmd("engram", ["cloud", "enroll", project]) === 0) {
|
|
478
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
479
|
+
configureCloudAutosync(cwd, opts);
|
|
480
|
+
persistCloudToken(token, opts);
|
|
229
481
|
ok("Engram Cloud habilitado. Replica con " + c.bold("ozali sync --cloud") + ".");
|
|
230
|
-
|
|
482
|
+
const dash = cloudDashboardURL(server);
|
|
483
|
+
if (dash) {
|
|
484
|
+
info(`Dashboard: ${c.cyan(dash)}`);
|
|
485
|
+
if (await confirm("¿Abrir el dashboard en el navegador?", false)) openURL(dash);
|
|
486
|
+
}
|
|
487
|
+
return { enabled: true, server, token };
|
|
231
488
|
}
|
|
232
489
|
warn("No se pudo enrolar el proyecto en Engram Cloud. Continúo solo con git-sync.");
|
|
233
490
|
return { enabled: false, server };
|
|
234
491
|
}
|
|
235
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Configura ENGRAM_CLOUD_AUTOSYNC=1 (y ENGRAM_CLOUD_TOKEN) en el bloque env del MCP
|
|
495
|
+
* de Engram del agente, para que la réplica sea automática e invisible.
|
|
496
|
+
*/
|
|
497
|
+
function configureCloudAutosync(cwd, opts) {
|
|
498
|
+
const agent = opts.agent || "claude-code";
|
|
499
|
+
const token = process.env[CLOUD_TOKEN_ENV] || "";
|
|
500
|
+
// Claude Code: .claude/settings.json → mcpServers.engram.env
|
|
501
|
+
if (agent === "claude-code" || agent === "both") {
|
|
502
|
+
const p = path.join(cwd, ".claude", "settings.json");
|
|
503
|
+
const cfg = readJSON(p, {});
|
|
504
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
505
|
+
cfg.mcpServers.engram = cfg.mcpServers.engram || {};
|
|
506
|
+
cfg.mcpServers.engram.env = cfg.mcpServers.engram.env || {};
|
|
507
|
+
cfg.mcpServers.engram.env[CLOUD_AUTOSYNC_ENV] = "1";
|
|
508
|
+
if (token) cfg.mcpServers.engram.env[CLOUD_TOKEN_ENV] = token;
|
|
509
|
+
writeJSON(p, cfg);
|
|
510
|
+
ok(`Autosync de Engram Cloud configurado en ${c.bold(".claude/settings.json")} (mcpServers.engram.env).`);
|
|
511
|
+
}
|
|
512
|
+
// opencode: opencode.json → mcp.engram.env
|
|
513
|
+
if (agent === "opencode" || agent === "both") {
|
|
514
|
+
const p = path.join(cwd, "opencode.json");
|
|
515
|
+
const cfg = readJSON(p, {});
|
|
516
|
+
cfg.mcp = cfg.mcp || {};
|
|
517
|
+
cfg.mcp.engram = cfg.mcp.engram || {};
|
|
518
|
+
if (cfg.mcp.engram.env === undefined || typeof cfg.mcp.engram.env !== "object") {
|
|
519
|
+
cfg.mcp.engram.env = {};
|
|
520
|
+
} else {
|
|
521
|
+
cfg.mcp.engram.env = { ...cfg.mcp.engram.env };
|
|
522
|
+
}
|
|
523
|
+
cfg.mcp.engram.env[CLOUD_AUTOSYNC_ENV] = "1";
|
|
524
|
+
if (token) cfg.mcp.engram.env[CLOUD_TOKEN_ENV] = token;
|
|
525
|
+
writeJSON(p, cfg);
|
|
526
|
+
ok(`Autosync de Engram Cloud configurado en ${c.bold("opencode.json")} (mcp.engram.env).`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Persiste el token de Engram Cloud para sesiones futuras.
|
|
532
|
+
* 1) ~/.engram/cloud_token (default de Engram)
|
|
533
|
+
* 2) Avisa al usuario que añada la env var a su shell rc si quiere uso fuera del agente.
|
|
534
|
+
*/
|
|
535
|
+
function persistCloudToken(token, opts) {
|
|
536
|
+
const tokenPath = path.join(HOME, ".engram", "cloud_token");
|
|
537
|
+
try {
|
|
538
|
+
ensureDir(path.dirname(tokenPath));
|
|
539
|
+
fs.writeFileSync(tokenPath, token + "\n", { mode: 0o600 });
|
|
540
|
+
ok(`Token guardado en ${c.bold("~/.engram/cloud_token")} (permisos 600).`);
|
|
541
|
+
} catch {
|
|
542
|
+
warn("No pude escribir ~/.engram/cloud_token. Guarda el token manualmente.");
|
|
543
|
+
}
|
|
544
|
+
const shell = process.env.SHELL || "";
|
|
545
|
+
const rc = shell.includes("zsh") ? "~/.zshrc" : shell.includes("bash") ? "~/.bashrc" : null;
|
|
546
|
+
if (rc) {
|
|
547
|
+
info(`Para usar Engram Cloud fuera del agente, añade a ${c.bold(rc)}:`);
|
|
548
|
+
console.log(` ${c.dim(`export ${CLOUD_TOKEN_ENV}=<tu-token>`)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
236
552
|
function printEngramManualInstructions(agent) {
|
|
237
553
|
const plat = process.platform;
|
|
238
554
|
info("Para activar memoria buscable/acumulativa (modo " + c.bold("hybrid") + "):");
|
|
239
|
-
if (plat === "
|
|
555
|
+
if (plat === "linux") {
|
|
556
|
+
info(" 1. Binario precompilado " + c.dim("(recomendado)") + ": baja " + c.bold("engram_<ver>_linux_<amd64|arm64>.tar.gz") + " de");
|
|
557
|
+
info(" " + c.cyan("https://github.com/Gentleman-Programming/engram/releases"));
|
|
558
|
+
info(" " + c.bold("tar -xzf engram_*_linux_*.tar.gz && mv engram ~/.local/bin/ && chmod +x ~/.local/bin/engram"));
|
|
559
|
+
info(" " + c.dim('(asegúrate que ~/.local/bin esté en tu PATH: export PATH="$HOME/.local/bin:$PATH")'));
|
|
560
|
+
info(" " + c.dim("o, con Go 1.24+: ") + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest"));
|
|
561
|
+
} else if (plat === "darwin") {
|
|
240
562
|
info(" 1. " + c.bold("brew install gentleman-programming/tap/engram") + " " + c.dim("(o binario: github.com/Gentleman-Programming/engram/releases)"));
|
|
241
563
|
} else if (plat === "win32") {
|
|
242
564
|
info(" 1. " + c.bold("go install github.com/Gentleman-Programming/engram/cmd/engram@latest") + " " + c.dim("(requiere Go 1.24+)"));
|
|
@@ -447,6 +769,19 @@ export function doctor(cwd) {
|
|
|
447
769
|
add("Node ≥ 16", env.node.ok, env.node.version);
|
|
448
770
|
add("Fuente de verdad", env.sot.found, env.sot.found ? `${env.sot.doc} + ${env.sot.dir}/` : "ausente (corre la skill 'ozali')");
|
|
449
771
|
add("Skill ozali instalada", env.skill.installed, env.skill.installed ? env.skill.paths.map((p) => path.relative(cwd, p) || p).join(", ") : "no instalada (ozali init)");
|
|
772
|
+
// Skill cdk (la genera el agente): versión de contrato vs. la vigente del paquete.
|
|
773
|
+
const cdkInfo = detectCdk(cwd);
|
|
774
|
+
const cdkN = cdkCanonicalVersion();
|
|
775
|
+
if (!cdkInfo.installed) {
|
|
776
|
+
add("Skill cdk", true, "no generada aún (corre la skill 'ozali' en tu agente)");
|
|
777
|
+
} else if (cdkInfo.version != null && cdkInfo.version >= cdkN && !cdkInfo.hasCopsis) {
|
|
778
|
+
add("Skill cdk", true, `contrato v${cdkInfo.version} (al día)`);
|
|
779
|
+
} else {
|
|
780
|
+
const reason = cdkInfo.hasCopsis ? "contiene copsis-commit → migra con la skill 'ozali'"
|
|
781
|
+
: cdkInfo.version == null ? "sin versión de contrato (legado) → migra con la skill 'ozali'"
|
|
782
|
+
: `contrato v${cdkInfo.version} < v${cdkN} → migra con la skill 'ozali'`;
|
|
783
|
+
add("Skill cdk", false, reason);
|
|
784
|
+
}
|
|
450
785
|
add("Engram", env.engram.available, env.engram.available ? env.engram.bin : "no instalado → modo docs");
|
|
451
786
|
if (env.engram.available) {
|
|
452
787
|
const online = tryExec("engram", ["doctor"], { cwd }) !== null;
|
|
@@ -455,9 +790,15 @@ export function doctor(cwd) {
|
|
|
455
790
|
const jarvis = detectJarvis(cwd);
|
|
456
791
|
// jarvis es opt-in (--no-jarvis): informativo, no cuenta como fallo.
|
|
457
792
|
add("ozali-jarvis", true, jarvis.present ? `configurado (${jarvis.where.join(", ")})` : "no configurado (--no-jarvis)");
|
|
458
|
-
const
|
|
793
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
794
|
+
const cloudOn = !!(cfg && cfg.cloud && cfg.cloud.enabled) || !!(cloudMeta && cloudMeta.enrolled);
|
|
459
795
|
// Cloud es opt-in: "off" es un estado válido (no cuenta como fallo).
|
|
460
|
-
|
|
796
|
+
const cloudDetail = cloudOn
|
|
797
|
+
? [`enrolado → ${firstNonEmpty(cloudMeta && cloudMeta.server, cfg && cfg.cloud && cfg.cloud.server) || "server por defecto"}`,
|
|
798
|
+
hasCloudToken() ? c.green("token ✓") : c.yellow("sin token"),
|
|
799
|
+
cloudMeta && cloudMeta.dashboard ? c.cyan(cloudMeta.dashboard) : ""].filter(Boolean).join(" · ")
|
|
800
|
+
: "off (opt-in, git-sync activo)";
|
|
801
|
+
add("Engram Cloud", true, cloudDetail);
|
|
461
802
|
add("Repo de conocimiento", !!(cfg && cfg.knowledgeRepo && exists(cfg.knowledgeRepo)), cfg ? cfg.knowledgeRepo : "sin configurar (ozali init)");
|
|
462
803
|
|
|
463
804
|
// Strict TDD (de la fuente de verdad)
|
|
@@ -494,6 +835,42 @@ export function doctor(cwd) {
|
|
|
494
835
|
}
|
|
495
836
|
}
|
|
496
837
|
|
|
838
|
+
// Estado detallado de Engram Cloud (si está habilitado).
|
|
839
|
+
if (cloudOn && env.engram.available) {
|
|
840
|
+
const project = (cfg && cfg.project) || projectName(cwd);
|
|
841
|
+
const snap = cloudStatusSnapshot(cwd, project);
|
|
842
|
+
const hasData = snap.syncStatus || snap.upgradeStatus || snap.conflictsStats;
|
|
843
|
+
if (hasData) {
|
|
844
|
+
step("Estado de Engram Cloud");
|
|
845
|
+
if (snap.syncStatus) {
|
|
846
|
+
info("Sync:");
|
|
847
|
+
printIndented(snap.syncStatus);
|
|
848
|
+
// Warnings específicos por reason_code
|
|
849
|
+
const reason = extractReasonCode(snap.syncStatus);
|
|
850
|
+
if (reason === "blocked_unenrolled") warn("El proyecto no está enrolado en el servidor cloud. Corre " + c.bold("ozali init") + " para re-enrolarlo.");
|
|
851
|
+
if (reason === "transport_failed") warn("No se pudo conectar al servidor cloud (transport_failed). Verifica la URL y tu conexión.");
|
|
852
|
+
}
|
|
853
|
+
if (snap.upgradeStatus) {
|
|
854
|
+
info("Upgrade:");
|
|
855
|
+
printIndented(snap.upgradeStatus);
|
|
856
|
+
// Fase 3.2: sugerir upgrade si el estado no es bootstrap_verified
|
|
857
|
+
const upgradeReason = extractReasonCode(snap.upgradeStatus);
|
|
858
|
+
if (upgradeReason && upgradeReason !== "bootstrap_verified") {
|
|
859
|
+
warn(`El proyecto requiere upgrade de cloud (estado: ${upgradeReason}). Corre ${c.bold("ozali cloud upgrade")}.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (snap.conflictsStats) {
|
|
863
|
+
info("Conflictos:");
|
|
864
|
+
printIndented(snap.conflictsStats);
|
|
865
|
+
// Fase 4.2: advertir conflictos pendientes
|
|
866
|
+
const pendingMatch = snap.conflictsStats.match(/pending\s*[:=]\s*(\d+)/i);
|
|
867
|
+
const pending = pendingMatch ? parseInt(pendingMatch[1], 10) : 0;
|
|
868
|
+
if (pending > 0) warn(`${pending} conflicto(s) de memoria sin juzgar. Usa ${c.bold("ozali audit --conflicts")}.`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (cloudMeta && cloudMeta.dashboard) info(`Dashboard: ${c.cyan(cloudMeta.dashboard)}`);
|
|
872
|
+
}
|
|
873
|
+
|
|
497
874
|
// Tendencia de uso de tokens (la escribe cdk en cada hito; informativo).
|
|
498
875
|
const metrics = readJSON(path.join(cwd, ".ozali", "metrics", "token-metrics.json"));
|
|
499
876
|
if (metrics && Array.isArray(metrics.hits) && metrics.hits.length) {
|
|
@@ -543,10 +920,15 @@ export function update(cwd, opts = {}) {
|
|
|
543
920
|
}
|
|
544
921
|
|
|
545
922
|
// 1) Skill ozali (incluye las references: la base desde la que el agente regenera cdk)
|
|
923
|
+
// + ozali-commit (commit convencional) como skill hermana — la crea en repos previos.
|
|
546
924
|
if (env.skill.installed) {
|
|
547
925
|
for (const p of env.skill.paths) {
|
|
548
926
|
copyDir(SKILL_SRC, p);
|
|
549
927
|
ok(`Skill ozali actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
|
|
928
|
+
const commitDir = path.join(path.dirname(p), "ozali-commit");
|
|
929
|
+
const freshCommit = !exists(commitDir);
|
|
930
|
+
copyDir(COMMIT_SKILL_SRC, commitDir);
|
|
931
|
+
ok(`Skill ozali-commit ${freshCommit ? "instalada" : "actualizada"}: ${path.relative(cwd, commitDir) || commitDir}`);
|
|
550
932
|
}
|
|
551
933
|
} else {
|
|
552
934
|
warn("Skill ozali no instalada en esta ruta (corre " + c.bold("ozali init") + " para instalarla).");
|
|
@@ -564,19 +946,33 @@ export function update(cwd, opts = {}) {
|
|
|
564
946
|
// 3) ozali-jarvis: crea el orquestador en repos previos a 0.4.0 y refresca el resto.
|
|
565
947
|
if (!opts.noJarvis) {
|
|
566
948
|
const proj = projectName(cwd);
|
|
567
|
-
const engPath =
|
|
949
|
+
const engPath = ENGRAM_CONFIG_PATH(cwd);
|
|
568
950
|
if (!exists(engPath)) { writeJSON(engPath, { project_name: proj }); ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`); }
|
|
569
951
|
if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
|
|
570
952
|
if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
|
|
571
953
|
}
|
|
572
954
|
|
|
573
|
-
// 4) Skill cdk: la genera el AGENTE (Fase 6)
|
|
955
|
+
// 4) Skill cdk: la genera/migra el AGENTE (Fase 0.5/6); el CLI solo detecta versión y guía.
|
|
574
956
|
const cdk = detectCdk(cwd);
|
|
957
|
+
const cdkN = cdkCanonicalVersion();
|
|
575
958
|
if (cdk.installed) {
|
|
576
959
|
step("Skill cdk (generada por el agente)");
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
960
|
+
const upToDate = cdk.version != null && cdk.version >= cdkN && !cdk.hasCopsis;
|
|
961
|
+
if (upToDate) {
|
|
962
|
+
ok(`cdk al día (contrato v${cdk.version}).`);
|
|
963
|
+
} else {
|
|
964
|
+
const reason = cdk.version == null ? "sin versión de contrato (cdk legado)"
|
|
965
|
+
: cdk.version < cdkN ? `contrato v${cdk.version} < v${cdkN} (desactualizado)`
|
|
966
|
+
: "contiene referencias a copsis-commit";
|
|
967
|
+
warn(`cdk desactualizada: ${reason}.`);
|
|
968
|
+
if (cdk.hasCopsis) warn("Detectadas referencias a " + c.bold("copsis-commit") + " (heredadas de versiones anteriores).");
|
|
969
|
+
info("El CLI no regenera cdk. Actualízala manualmente desde tu agente:");
|
|
970
|
+
info(" 1. Abre tu agente en este proyecto.");
|
|
971
|
+
info(" 2. Corre la skill " + c.bold("ozali") + " (escribe " + c.bold('"ozali"') + "): el pre-flight migra cdk al contrato " + c.bold("v" + cdkN) + ", elimina copsis-commit y cablea ozali-commit.");
|
|
972
|
+
info("Tus docs por hito (" + c.bold(".ozali/docs/cdk/") + ") y el plan congelado se conservan.");
|
|
973
|
+
}
|
|
974
|
+
} else {
|
|
975
|
+
info("cdk aún no generada en este repo. Corre la skill " + c.bold("ozali") + " en tu agente para crearla.");
|
|
580
976
|
}
|
|
581
977
|
|
|
582
978
|
// 5) versión del config
|
|
@@ -585,18 +981,44 @@ export function update(cwd, opts = {}) {
|
|
|
585
981
|
return 0;
|
|
586
982
|
}
|
|
587
983
|
|
|
588
|
-
/**
|
|
984
|
+
/**
|
|
985
|
+
* ¿Existe la skill cdk (generada por el agente)? Devuelve además la versión de contrato
|
|
986
|
+
* estampada en su frontmatter (`cdk_contract_version`) y si aún referencia `copsis-commit`.
|
|
987
|
+
* version === null ⇒ cdk legado (sin marcador de versión).
|
|
988
|
+
*/
|
|
589
989
|
function detectCdk(cwd) {
|
|
590
990
|
const paths = [
|
|
591
991
|
path.join(cwd, ".claude", "skills", "cdk", "SKILL.md"),
|
|
592
992
|
path.join(HOME, ".claude", "skills", "cdk", "SKILL.md"),
|
|
593
993
|
].filter(exists);
|
|
594
|
-
return { installed: paths
|
|
994
|
+
if (paths.length === 0) return { installed: false, paths: [], version: null, hasCopsis: false };
|
|
995
|
+
let version = null;
|
|
996
|
+
let hasCopsis = false;
|
|
997
|
+
for (const f of paths) {
|
|
998
|
+
const txt = fs.readFileSync(f, "utf8");
|
|
999
|
+
if (version === null) {
|
|
1000
|
+
const m = txt.match(/cdk_contract_version:\s*(\d+)/i);
|
|
1001
|
+
if (m) version = parseInt(m[1], 10);
|
|
1002
|
+
}
|
|
1003
|
+
if (/copsis-commit/i.test(txt)) hasCopsis = true;
|
|
1004
|
+
}
|
|
1005
|
+
return { installed: true, paths, version, hasCopsis };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** Versión de contrato vigente de cdk (fuente única: skill/references/cdk-contract.md del paquete). */
|
|
1009
|
+
function cdkCanonicalVersion() {
|
|
1010
|
+
try {
|
|
1011
|
+
const txt = fs.readFileSync(path.join(SKILL_SRC, "references", "cdk-contract.md"), "utf8");
|
|
1012
|
+
const m = txt.match(/CDK_CONTRACT_VERSION:\s*(\d+)/i);
|
|
1013
|
+
return m ? parseInt(m[1], 10) : 1;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
595
1017
|
}
|
|
596
1018
|
|
|
597
1019
|
// ============================================================= sync ===========
|
|
598
1020
|
export function sync(cwd, opts) {
|
|
599
|
-
step(`ozali sync${opts.import ? " --import" : ""} — histórico ↔ repo de conocimiento`);
|
|
1021
|
+
step(`ozali sync${opts.import ? " --import" : ""}${opts.cloud ? " --cloud" : ""} — histórico ↔ repo de conocimiento`);
|
|
600
1022
|
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
601
1023
|
if (!cfg || !cfg.knowledgeRepo) { warn("Sin repo de conocimiento configurado. Corre " + c.bold("ozali init") + "."); return 1; }
|
|
602
1024
|
const kRepo = cfg.knowledgeRepo;
|
|
@@ -608,11 +1030,48 @@ export function sync(cwd, opts) {
|
|
|
608
1030
|
|
|
609
1031
|
// Engram Cloud (opt-in): réplica bidireccional adicional al git-sync.
|
|
610
1032
|
if (opts.cloud) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1033
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
1034
|
+
const cloudEnabled = (cfg.cloud && cfg.cloud.enabled) || (cloudMeta && cloudMeta.enrolled);
|
|
1035
|
+
if (cloudEnabled && tryExec("engram", ["--version"])) {
|
|
1036
|
+
if (!hasCloudToken() && cloudMeta && cloudMeta.auth_required !== false) {
|
|
1037
|
+
warn(`El servidor cloud puede requerir autenticación. Define ${c.bold(CLOUD_TOKEN_ENV)} si falla el sync.`);
|
|
1038
|
+
}
|
|
1039
|
+
const cloudServer = firstNonEmpty(cloudMeta && cloudMeta.server, cfg.cloud && cfg.cloud.server);
|
|
1040
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1041
|
+
|
|
1042
|
+
if (opts.import) {
|
|
1043
|
+
// --- onboarding inverso: pull desde cloud ---
|
|
1044
|
+
info(`Recibiendo memoria del equipo desde Engram Cloud (proyecto "${project}")…`);
|
|
1045
|
+
const pullOut = tryExec("engram", ["sync", "--cloud", "--import", "--project", project], { cwd });
|
|
1046
|
+
if (pullOut !== null) {
|
|
1047
|
+
ok("Pull desde Engram Cloud completado.");
|
|
1048
|
+
if (pullOut.trim()) printIndented(pullOut);
|
|
1049
|
+
// Importar los chunks locales que llegaron vía cloud
|
|
1050
|
+
if (spawnCmd("engram", ["sync", "--import"], { cwd }) === 0) ok("Memorias cloud importadas a Engram local.");
|
|
1051
|
+
else warn("engram sync --import no terminó correctamente tras el pull cloud.");
|
|
1052
|
+
// Verificación
|
|
1053
|
+
const verify = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
1054
|
+
if (verify) { info("Estado de cloud:"); printIndented(verify); }
|
|
1055
|
+
} else {
|
|
1056
|
+
const reason = extractReasonCode(pullOut);
|
|
1057
|
+
warn(`engram sync --cloud --import no terminó correctamente${reason ? ` (motivo: ${reason})` : ""}.`);
|
|
1058
|
+
if (reason === "blocked_unenrolled") info("El proyecto no está enrolado en el servidor. Corre " + c.bold("ozali init") + " para enrolarlo.");
|
|
1059
|
+
}
|
|
1060
|
+
} else {
|
|
1061
|
+
// --- push a cloud (default) ---
|
|
1062
|
+
info(`Replicando con Engram Cloud (proyecto "${project}")…`);
|
|
1063
|
+
const out = syncCloudProject(cwd, project);
|
|
1064
|
+
if (out !== null) {
|
|
1065
|
+
ok("Réplica con Engram Cloud completada.");
|
|
1066
|
+
if (out.trim()) printIndented(out);
|
|
1067
|
+
} else {
|
|
1068
|
+
const reason = extractReasonCode(out);
|
|
1069
|
+
warn(`engram sync --cloud no terminó correctamente${reason ? ` (motivo: ${reason})` : ""}. Revisa el output de arriba.`);
|
|
1070
|
+
if (reason === "blocked_unenrolled") info("El proyecto no está enrolado en el servidor. Corre " + c.bold("ozali init") + " para enrolarlo.");
|
|
1071
|
+
if (reason === "transport_failed") info("No se pudo conectar al servidor cloud. Verifica la URL y tu conexión.");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} else if (!cloudEnabled) {
|
|
616
1075
|
warn("--cloud pedido, pero Engram Cloud no está habilitado. Corre " + c.bold("ozali init") + " para habilitarlo, o usa git-sync sin --cloud.");
|
|
617
1076
|
} else {
|
|
618
1077
|
warn("--cloud pedido, pero Engram no responde. Omito la réplica cloud.");
|
|
@@ -692,7 +1151,7 @@ export async function audit(cwd, opts) {
|
|
|
692
1151
|
step("ozali audit — auditoría de memoria (Engram)");
|
|
693
1152
|
const env = detectAll(cwd);
|
|
694
1153
|
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
695
|
-
const engramCfg = readJSON(
|
|
1154
|
+
const engramCfg = readJSON(ENGRAM_CONFIG_PATH(cwd));
|
|
696
1155
|
const project = (engramCfg && engramCfg.project_name) || (cfg && cfg.project) || projectName(cwd);
|
|
697
1156
|
const hasProjectContext = env.git.isRepo || !!cfg || !!engramCfg;
|
|
698
1157
|
|
|
@@ -712,6 +1171,34 @@ export async function audit(cwd, opts) {
|
|
|
712
1171
|
return auditFromDocs(cwd, project);
|
|
713
1172
|
}
|
|
714
1173
|
|
|
1174
|
+
// Fase 4.1: --dashboard abre el dashboard de Engram Cloud
|
|
1175
|
+
if (opts.dashboard) {
|
|
1176
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
1177
|
+
const dash = (cloudMeta && cloudMeta.dashboard) || (cloudMeta && cloudMeta.server ? cloudDashboardURL(cloudMeta.server) : null);
|
|
1178
|
+
if (!dash) { warn("No hay Engram Cloud configurado. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
1179
|
+
info(`Abriendo dashboard: ${c.cyan(dash)}`);
|
|
1180
|
+
openURL(dash);
|
|
1181
|
+
return 0;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Fase 4.1: --conflicts lista/stats conflictos de memoria
|
|
1185
|
+
if (opts.conflicts) {
|
|
1186
|
+
const projArg = scope === "project" ? ["--project", project] : [];
|
|
1187
|
+
if (opts.stats) {
|
|
1188
|
+
step(`Estadísticas de conflictos${scope === "project" ? ` — ${project}` : ""}`);
|
|
1189
|
+
const out = tryExec("engram", ["conflicts", "stats", ...projArg], { cwd });
|
|
1190
|
+
if (out) printIndented(out);
|
|
1191
|
+
else warn("No se pudieron obtener estadísticas de conflictos.");
|
|
1192
|
+
} else {
|
|
1193
|
+
const statusFlag = opts.judged ? ["--status", "judged"] : [];
|
|
1194
|
+
step(`Conflictos${opts.judged ? " juzgados" : " pendientes"}${scope === "project" ? ` — ${project}` : ""}`);
|
|
1195
|
+
const out = tryExec("engram", ["conflicts", "list", ...statusFlag, ...projArg], { cwd });
|
|
1196
|
+
if (out) printIndented(out);
|
|
1197
|
+
else warn("No se pudieron listar conflictos.");
|
|
1198
|
+
}
|
|
1199
|
+
return 0;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
715
1202
|
// Navegador interactivo.
|
|
716
1203
|
if (opts.tui) {
|
|
717
1204
|
info("Abriendo el navegador interactivo de Engram (engram tui)…");
|
|
@@ -757,3 +1244,100 @@ function auditFromDocs(cwd, project) {
|
|
|
757
1244
|
info("Instala Engram para auditoría buscable/acumulativa (" + c.bold("ozali doctor") + ").");
|
|
758
1245
|
return 0;
|
|
759
1246
|
}
|
|
1247
|
+
|
|
1248
|
+
// ============================================================= cloud ===========
|
|
1249
|
+
export async function cloud(cwd, opts) {
|
|
1250
|
+
const sub = opts._[1] || "status";
|
|
1251
|
+
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
1252
|
+
const cloudMeta = readTeamCloud(cwd);
|
|
1253
|
+
const project = (cfg && cfg.project) || projectName(cwd);
|
|
1254
|
+
const cloudServer = firstNonEmpty(cloudMeta && cloudMeta.server, cfg && cfg.cloud && cfg.cloud.server);
|
|
1255
|
+
|
|
1256
|
+
switch (sub) {
|
|
1257
|
+
case "status": {
|
|
1258
|
+
step(`ozali cloud status — proyecto "${project}"`);
|
|
1259
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1260
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1261
|
+
const status = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
1262
|
+
if (status) { info("Estado:"); printIndented(status); }
|
|
1263
|
+
else { warn("No se pudo obtener el estado de cloud."); }
|
|
1264
|
+
const upgrade = tryExec("engram", ["cloud", "upgrade", "status", "--project", project], { cwd });
|
|
1265
|
+
if (upgrade) { info("Upgrade:"); printIndented(upgrade); }
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
case "upgrade": {
|
|
1270
|
+
step(`ozali cloud upgrade — proyecto "${project}"`);
|
|
1271
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1272
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1273
|
+
// 1) doctor (checkpoint)
|
|
1274
|
+
info("Paso 1/3: Verificando estado actual…");
|
|
1275
|
+
const status = tryExec("engram", ["cloud", "status", "--project", project], { cwd });
|
|
1276
|
+
if (status) printIndented(status);
|
|
1277
|
+
// 2) repair --dry-run
|
|
1278
|
+
info("Paso 2/3: Simulando repair (dry-run)…");
|
|
1279
|
+
const dryRun = tryExec("engram", ["cloud", "upgrade", "repair", "--dry-run", "--project", project], { cwd });
|
|
1280
|
+
if (dryRun) printIndented(dryRun);
|
|
1281
|
+
const doApply = await confirm("¿Aplicar el repair ahora?", true);
|
|
1282
|
+
if (!doApply) { info("Upgrade cancelado. Puedes aplicarlo después con " + c.bold("ozali cloud repair") + "."); return 0; }
|
|
1283
|
+
// 3) repair --apply + bootstrap
|
|
1284
|
+
info("Paso 3/3: Aplicando repair…");
|
|
1285
|
+
const repairOut = tryExec("engram", ["cloud", "upgrade", "repair", "--apply", "--project", project], { cwd });
|
|
1286
|
+
if (repairOut !== null) { ok("Repair aplicado."); if (repairOut.trim()) printIndented(repairOut); }
|
|
1287
|
+
else { warn("Repair falló. Revisa el output de arriba."); return 1; }
|
|
1288
|
+
const boot = tryExec("engram", ["cloud", "upgrade", "bootstrap", "--project", project], { cwd });
|
|
1289
|
+
if (boot !== null) { ok("Bootstrap completado."); if (boot.trim()) printIndented(boot); }
|
|
1290
|
+
else warn("Bootstrap falló. Corre " + c.bold("ozali cloud status") + " para ver el estado.");
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
case "repair": {
|
|
1295
|
+
step(`ozali cloud repair — proyecto "${project}"`);
|
|
1296
|
+
if (!tryExec("engram", ["--version"])) { warn("Engram no responde."); return 1; }
|
|
1297
|
+
if (cloudServer && process.env[CLOUD_SERVER_ENV] === undefined) process.env[CLOUD_SERVER_ENV] = cloudServer;
|
|
1298
|
+
const out = tryExec("engram", ["cloud", "upgrade", "repair", "--apply", "--project", project], { cwd });
|
|
1299
|
+
if (out !== null) { ok("Repair aplicado."); if (out.trim()) printIndented(out); return 0; }
|
|
1300
|
+
warn("Repair falló. Revisa el output de arriba.");
|
|
1301
|
+
return 1;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
case "dashboard": {
|
|
1305
|
+
const dash = cloudMeta && cloudMeta.dashboard ? cloudMeta.dashboard : (cloudServer ? cloudDashboardURL(cloudServer) : null);
|
|
1306
|
+
if (!dash) { warn("No hay servidor cloud configurado. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
1307
|
+
info(`Abriendo dashboard: ${c.cyan(dash)}`);
|
|
1308
|
+
openURL(dash);
|
|
1309
|
+
return 0;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
case "config": {
|
|
1313
|
+
step("ozali cloud config");
|
|
1314
|
+
if (cloudMeta && cloudMeta.enrolled) {
|
|
1315
|
+
info(`Servidor: ${c.bold(cloudMeta.server || "no definido")}`);
|
|
1316
|
+
info(`Proyecto: ${c.bold(cloudMeta.project || project)}`);
|
|
1317
|
+
info(`Token: ${hasCloudToken() ? c.green("✓ configurado") : c.yellow("✗ no configurado")}`);
|
|
1318
|
+
info(`Dashboard: ${cloudMeta.dashboard ? c.cyan(cloudMeta.dashboard) : "no disponible"}`);
|
|
1319
|
+
info(`Autosync: ${c.bold(CLOUD_AUTOSYNC_ENV)}=${process.env[CLOUD_AUTOSYNC_ENV] || "no definido"}`);
|
|
1320
|
+
const reconfig = await confirm("¿Reconfigurar el servidor?", false);
|
|
1321
|
+
if (!reconfig) return 0;
|
|
1322
|
+
}
|
|
1323
|
+
const server = await ask("URL del servidor de Engram Cloud", cloudMeta && cloudMeta.server || "http://127.0.0.1:18080");
|
|
1324
|
+
const token = await ask("Token de autenticación");
|
|
1325
|
+
if (!token) { warn("Sin token no se puede configurar (modo autenticado obligatorio)."); return 1; }
|
|
1326
|
+
spawnCmd("engram", ["cloud", "config", "--server", server]);
|
|
1327
|
+
process.env[CLOUD_TOKEN_ENV] = token;
|
|
1328
|
+
if (spawnCmd("engram", ["cloud", "enroll", project]) === 0) {
|
|
1329
|
+
writeTeamCloud(cwd, { enabled: true, server, project, authRequired: true });
|
|
1330
|
+
configureCloudAutosync(cwd, opts);
|
|
1331
|
+
persistCloudToken(token, opts);
|
|
1332
|
+
ok("Engram Cloud reconfigurado.");
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
warn("No se pudo enrolar el proyecto. Verifica el servidor y el token.");
|
|
1336
|
+
return 1;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
default:
|
|
1340
|
+
err(`Subcomando desconocido "${sub}". Usa: status | upgrade | repair | dashboard | config`);
|
|
1341
|
+
return 1;
|
|
1342
|
+
}
|
|
1343
|
+
}
|