ozali 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/cli/bin/ozali.mjs +77 -0
- package/cli/lib/commands.mjs +235 -0
- package/cli/lib/detect.mjs +103 -0
- package/cli/lib/prompt.mjs +51 -0
- package/cli/lib/util.mjs +131 -0
- package/cli/test/smoke.test.mjs +88 -0
- package/docs/intended-usage.md +43 -0
- package/docs/security.md +47 -0
- package/docs/team-history.md +66 -0
- package/package.json +39 -0
- package/skill/SKILL.md +396 -0
- package/skill/references/agents-blueprint.md +138 -0
- package/skill/references/calibration-blueprint.md +113 -0
- package/skill/references/doc-templates.md +301 -0
- package/skill/references/engram-convention.md +186 -0
- package/skill/references/harness-template.md +214 -0
- package/skill/references/knowledge-blueprint.md +143 -0
- package/skill/references/permissions-bypass.md +232 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jaime Baños
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ozali
|
|
4
|
+
|
|
5
|
+
**Bootstrap de IA por equipo — calibra tus proyectos, genera una skill de ejecución con TDD/SDD, y mantiene memoria de equipo (Engram) con histórico aislado.**
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Qué es
|
|
12
|
+
|
|
13
|
+
`ozali` lleva a cualquier repositorio existente a un flujo de desarrollo asistido por IA,
|
|
14
|
+
**disciplinado y trazable**, pensado para equipos. No es un chatbot: es un método.
|
|
15
|
+
|
|
16
|
+
- **Diagnostica y calibra** el proyecto: detecta stack, arquitectura, convenciones, capacidades de
|
|
17
|
+
testing y resuelve **Strict TDD** (test-first sí/no) contra lo que el proyecto realmente soporta.
|
|
18
|
+
- **Fuente de verdad** legible: `AI.md` + `.ai/` (el "Project Brain") — generado evaluando el
|
|
19
|
+
código real, nunca inventado.
|
|
20
|
+
- **Genera `cdk`**, la skill de ejecución con **8 subagentes** (owner, manager, analyzer,
|
|
21
|
+
orchestrator, executioners, proposer, documenter, tester), **GATE de aprobación** y plan
|
|
22
|
+
congelado inmutable.
|
|
23
|
+
- **Memoria híbrida**: documentos legibles por humanos **+** Engram buscable/acumulable, para que
|
|
24
|
+
la herramienta **aprenda de lo que el equipo va haciendo**.
|
|
25
|
+
- **Histórico aislado**: el registro que crece vive en un repo de conocimiento aparte, sin
|
|
26
|
+
sobrecargar el repo principal.
|
|
27
|
+
|
|
28
|
+
## Dos piezas
|
|
29
|
+
|
|
30
|
+
1. **Proyecto completo** (este repo): la skill `ozali`, las `references/`, el harness, el CLI y los docs.
|
|
31
|
+
2. **Plug-n-play CLI**: instala y arranca el método en cualquier repo, de forma interogativa.
|
|
32
|
+
|
|
33
|
+
## Instalación (segura por diseño)
|
|
34
|
+
|
|
35
|
+
CLI **Node de cero dependencias y sin lifecycle scripts** → sin vector de supply-chain.
|
|
36
|
+
Detalle en [docs/security.md](docs/security.md).
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Recomendado (pnpm 10 deshabilita postinstall de deps por defecto)
|
|
40
|
+
pnpm dlx ozali@<versión> init
|
|
41
|
+
|
|
42
|
+
# npm con scripts desactivados
|
|
43
|
+
npx --ignore-scripts ozali@<versión> init
|
|
44
|
+
|
|
45
|
+
# Máxima auditabilidad
|
|
46
|
+
git clone <repo> && node ozali/cli/bin/ozali.mjs init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Uso del CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
ozali init # detecta agente, instala la skill, aísla el histórico, configura Engram
|
|
53
|
+
ozali doctor # health-check read-only (fuente de verdad, Engram, Strict TDD, runner…)
|
|
54
|
+
ozali update # actualiza la skill instalada a la versión del paquete
|
|
55
|
+
ozali sync # lleva el histórico (docs + Engram) al repo de conocimiento de equipo
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--agent`, `--scope`,
|
|
59
|
+
`--knowledge-repo`, `--import`/`--push` (sync). Modelo mental completo en
|
|
60
|
+
[docs/intended-usage.md](docs/intended-usage.md).
|
|
61
|
+
|
|
62
|
+
## Agentes soportados
|
|
63
|
+
|
|
64
|
+
Claude Code y opencode (perfiles de permisos para ambos en
|
|
65
|
+
[skill/references/permissions-bypass.md](skill/references/permissions-bypass.md)).
|
|
66
|
+
|
|
67
|
+
## Documentación
|
|
68
|
+
|
|
69
|
+
| Tema | Doc |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Uso previsto (modelo mental) | [docs/intended-usage.md](docs/intended-usage.md) |
|
|
72
|
+
| Seguridad del instalador (npx/pnpm) | [docs/security.md](docs/security.md) |
|
|
73
|
+
| Histórico aislado y memoria de equipo | [docs/team-history.md](docs/team-history.md) |
|
|
74
|
+
| Skill bootstrap | [skill/SKILL.md](skill/SKILL.md) |
|
|
75
|
+
| Calibración de testing + TDD | [skill/references/calibration-blueprint.md](skill/references/calibration-blueprint.md) |
|
|
76
|
+
| Blueprint de agentes | [skill/references/agents-blueprint.md](skill/references/agents-blueprint.md) |
|
|
77
|
+
| Memoria híbrida (docs + Engram) | [skill/references/engram-convention.md](skill/references/engram-convention.md) |
|
|
78
|
+
|
|
79
|
+
## Estado del roadmap
|
|
80
|
+
|
|
81
|
+
- [x] **Fase A** — Calibración + Strict TDD en el bootstrap.
|
|
82
|
+
- [x] **Fase B** — Memoria híbrida (docs + Engram, `state` recuperable, contrato de arranque).
|
|
83
|
+
- [x] **Fase C** — CLI plug-n-play (`init`/`doctor`/`update`/`sync`), Node cero-deps.
|
|
84
|
+
- [x] **Fase D** — Repo distribuible: tests (node:test), CI (matriz Node 16–22 + invariantes de seguridad), publicación npm con provenance.
|
|
85
|
+
|
|
86
|
+
> Siguiente paso natural: crear el repo en GitHub y publicar la primera versión (`pnpm publish` vía tag), más probar el bootstrap en un repo real del equipo.
|
|
87
|
+
|
|
88
|
+
> Linaje: `ozali` evoluciona la skill `copsis-doctor`, nutrida con conceptos de
|
|
89
|
+
> [gentle-ai](https://github.com/Gentleman-Programming/gentle-ai) (Engram, calibración SDD/TDD,
|
|
90
|
+
> distribución plug-n-play).
|
|
91
|
+
|
|
92
|
+
## Licencia
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ozali — CLI plug-n-play (Node, CERO dependencias, SIN lifecycle scripts).
|
|
3
|
+
// Seguridad: sin dependencias ni scripts de instalación. Ejecuta seguro con
|
|
4
|
+
// `pnpm dlx ozali` o `npx --ignore-scripts ozali`. Ver docs/security.md.
|
|
5
|
+
import { c, err, pkgVersion } from "../lib/util.mjs";
|
|
6
|
+
import { init, doctor, update, sync } from "../lib/commands.mjs";
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
${c.bold("ozali")} ${c.dim("v" + pkgVersion())} — bootstrap de IA por equipo (TDD/SDD + memoria Engram)
|
|
10
|
+
|
|
11
|
+
${c.bold("Uso:")}
|
|
12
|
+
ozali <comando> [opciones]
|
|
13
|
+
|
|
14
|
+
${c.bold("Comandos:")}
|
|
15
|
+
init Detecta el agente (Claude Code/opencode), instala la skill ozali,
|
|
16
|
+
aísla el histórico, configura Engram y el repo de conocimiento.
|
|
17
|
+
doctor Health-check read-only del proyecto (fuente de verdad, Engram, TDD…).
|
|
18
|
+
update Actualiza la skill ozali instalada a la versión de este paquete.
|
|
19
|
+
sync Sincroniza el histórico (docs + Engram) con el repo de conocimiento.
|
|
20
|
+
|
|
21
|
+
${c.bold("Opciones comunes:")}
|
|
22
|
+
--yes, -y No interactivo: usa defaults.
|
|
23
|
+
--dry-run (init) Muestra el plan sin escribir nada.
|
|
24
|
+
--agent <a> (init) claude-code | opencode | both.
|
|
25
|
+
--scope <s> (init) project | global.
|
|
26
|
+
--knowledge-repo <p> (init) Ruta del repo de conocimiento.
|
|
27
|
+
--import (sync) Importa del repo de conocimiento a local.
|
|
28
|
+
--push (sync) Hace push al remoto del repo de conocimiento.
|
|
29
|
+
-h, --help Esta ayuda. -v, --version Versión.
|
|
30
|
+
|
|
31
|
+
${c.bold("Instalación segura:")}
|
|
32
|
+
pnpm dlx ozali@<versión> init ${c.dim("# recomendado (pnpm 10: postinstall off)")}
|
|
33
|
+
npx --ignore-scripts ozali@<versión> init
|
|
34
|
+
git clone … && node cli/bin/ozali.mjs init
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const opts = { _: [] };
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const a = argv[i];
|
|
41
|
+
if (a === "--yes" || a === "-y") opts.yes = true;
|
|
42
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
43
|
+
else if (a === "--import") opts.import = true;
|
|
44
|
+
else if (a === "--push") opts.push = true;
|
|
45
|
+
else if (a === "--agent") opts.agent = argv[++i];
|
|
46
|
+
else if (a === "--scope") opts.scope = argv[++i];
|
|
47
|
+
else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
|
|
48
|
+
else if (a === "-h" || a === "--help") opts.help = true;
|
|
49
|
+
else if (a === "-v" || a === "--version") opts.version = true;
|
|
50
|
+
else opts._.push(a);
|
|
51
|
+
}
|
|
52
|
+
return opts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
57
|
+
const cmd = opts._[0];
|
|
58
|
+
|
|
59
|
+
if (opts.version) { console.log(pkgVersion()); return 0; }
|
|
60
|
+
if (!cmd || opts.help || cmd === "help") { console.log(HELP); return 0; }
|
|
61
|
+
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
switch (cmd) {
|
|
64
|
+
case "init": return await init(cwd, opts);
|
|
65
|
+
case "doctor": return doctor(cwd);
|
|
66
|
+
case "update": return update(cwd);
|
|
67
|
+
case "sync": return sync(cwd, opts);
|
|
68
|
+
default:
|
|
69
|
+
err(`comando desconocido "${cmd}". Usa ${c.bold("ozali --help")}.`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().then((code) => process.exit(code || 0)).catch((e) => {
|
|
75
|
+
err(e && e.stack ? e.stack : String(e));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// commands.mjs — implementación de init / doctor / update / sync.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
c, ok, warn, err, info, step,
|
|
6
|
+
SKILL_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
|
|
7
|
+
ensureGitignore, tryExec, projectName, pkgVersion, DEFAULT_KNOWLEDGE,
|
|
8
|
+
} from "./util.mjs";
|
|
9
|
+
import { detectAll } from "./detect.mjs";
|
|
10
|
+
import { ask, confirm, select } from "./prompt.mjs";
|
|
11
|
+
|
|
12
|
+
const CONFIG_PATH = (cwd) => path.join(cwd, ".ozali", "config.json");
|
|
13
|
+
|
|
14
|
+
function skillTarget(cwd, scope) {
|
|
15
|
+
const base = scope === "global" ? path.join(process.env.HOME || "", ".claude") : path.join(cwd, ".claude");
|
|
16
|
+
return path.join(base, "skills", "ozali");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================ init ===========
|
|
20
|
+
export async function init(cwd, opts) {
|
|
21
|
+
step("ozali init — bootstrap del proyecto");
|
|
22
|
+
const env = detectAll(cwd);
|
|
23
|
+
|
|
24
|
+
if (!env.node.ok) warn(`Node ${env.node.version} detectado; ozali y el harness piden ≥16. Continúo, pero actualiza si ves errores.`);
|
|
25
|
+
if (!env.git.isRepo) warn("No estás en un repo git: la trazabilidad por commit y el sync quedarán limitados.");
|
|
26
|
+
|
|
27
|
+
// Fuente de verdad
|
|
28
|
+
if (env.sot.found) ok(`Fuente de verdad detectada: ${c.bold(env.sot.doc)} + ${c.bold(env.sot.dir + "/")} (variante ${env.sot.variant}).`);
|
|
29
|
+
else info(`Sin fuente de verdad aún (se generará al correr la skill 'ozali' en tu agente).`);
|
|
30
|
+
|
|
31
|
+
// Agente
|
|
32
|
+
const agentDefault = env.agents.opencode.present && !env.agents.claudeCode.present ? "opencode"
|
|
33
|
+
: env.agents.claudeCode.present && env.agents.opencode.present ? "both" : "claude-code";
|
|
34
|
+
const agent = opts.agent || await select("¿Para qué agente configuro ozali?", [
|
|
35
|
+
{ value: "claude-code", label: "Claude Code" },
|
|
36
|
+
{ value: "opencode", label: "opencode" },
|
|
37
|
+
{ value: "both", label: "Ambos (Claude Code + opencode)" },
|
|
38
|
+
], { "claude-code": 0, opencode: 1, both: 2 }[agentDefault]);
|
|
39
|
+
|
|
40
|
+
// Scope
|
|
41
|
+
const scope = opts.scope || await select("¿Dónde instalo la skill?", [
|
|
42
|
+
{ value: "project", label: `Proyecto (${c.dim(".claude/skills/ozali")})` },
|
|
43
|
+
{ value: "global", label: `Global (${c.dim("~/.claude/skills/ozali")})` },
|
|
44
|
+
], 0);
|
|
45
|
+
|
|
46
|
+
// Repo de conocimiento (histórico aislado)
|
|
47
|
+
const knowledgeRepo = opts.knowledgeRepo || await ask("Ruta del repo de conocimiento (histórico aislado)", DEFAULT_KNOWLEDGE);
|
|
48
|
+
|
|
49
|
+
// Engram
|
|
50
|
+
let memoryMode = "hybrid";
|
|
51
|
+
if (env.engram.available) {
|
|
52
|
+
ok(`Engram disponible (${env.engram.bin}). Modo de memoria: ${c.bold("hybrid")} (docs + Engram).`);
|
|
53
|
+
} else {
|
|
54
|
+
memoryMode = "docs";
|
|
55
|
+
warn("Engram no está instalado. Arrancamos en modo " + c.bold("docs") + " (solo documentos legibles).");
|
|
56
|
+
info("Para activar memoria buscable/acumulativa instala Engram: " + c.cyan("https://github.com/Gentleman-Programming/engram"));
|
|
57
|
+
info("Tras instalarlo, vuelve a correr " + c.bold("ozali doctor") + " — el modo subirá a hybrid automáticamente.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (opts.dryRun) { warn("--dry-run: no escribo nada. Plan mostrado arriba."); return 0; }
|
|
61
|
+
|
|
62
|
+
// --- acciones ---
|
|
63
|
+
step("Aplicando");
|
|
64
|
+
// 1) copiar skill
|
|
65
|
+
const target = skillTarget(cwd, scope);
|
|
66
|
+
ensureDir(path.dirname(target));
|
|
67
|
+
copyDir(SKILL_SRC, target);
|
|
68
|
+
ok(`Skill instalada en ${c.bold(path.relative(cwd, target) || target)}.`);
|
|
69
|
+
|
|
70
|
+
// 2) opencode: perfil base de permisos (idempotente, merge mínimo)
|
|
71
|
+
if (agent === "opencode" || agent === "both") {
|
|
72
|
+
ensureOpencodeProfile(cwd);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3) gitignore del histórico aislado
|
|
76
|
+
if (env.git.isRepo) {
|
|
77
|
+
const { added } = ensureGitignore(cwd, [".ozali/", ".engram/"]);
|
|
78
|
+
if (added.length) ok(`.gitignore actualizado: ${added.join(", ")} (histórico aislado del repo principal).`);
|
|
79
|
+
else info(".gitignore ya aislaba el histórico.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 4) repo de conocimiento
|
|
83
|
+
ensureDir(knowledgeRepo);
|
|
84
|
+
if (!exists(path.join(knowledgeRepo, ".git"))) {
|
|
85
|
+
if (await confirm(`¿Inicializo git en el repo de conocimiento (${knowledgeRepo})?`, true)) {
|
|
86
|
+
tryExec("git", ["init", "-q"], { cwd: knowledgeRepo });
|
|
87
|
+
ensureDir(path.join(knowledgeRepo, "projects"));
|
|
88
|
+
ensureDir(path.join(knowledgeRepo, "engram"));
|
|
89
|
+
ok("Repo de conocimiento inicializado.");
|
|
90
|
+
}
|
|
91
|
+
} else info("Repo de conocimiento ya existe.");
|
|
92
|
+
|
|
93
|
+
// 5) config local (gitignored)
|
|
94
|
+
const config = {
|
|
95
|
+
version: pkgVersion(), agent, scope, knowledgeRepo, memoryMode,
|
|
96
|
+
project: projectName(cwd), createdAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
writeJSON(CONFIG_PATH(cwd), config);
|
|
99
|
+
ok(`Config local escrita en ${c.bold(".ozali/config.json")} (gitignored).`);
|
|
100
|
+
|
|
101
|
+
// --- siguientes pasos ---
|
|
102
|
+
step("Siguientes pasos");
|
|
103
|
+
console.log(` 1. Abre tu agente en este proyecto.`);
|
|
104
|
+
console.log(` 2. Escribe ${c.bold('"diagnostica el proyecto"')} o ${c.bold('"ozali"')} para arrancar el bootstrap (calibración + generación de la skill ${c.bold("cdk")}).`);
|
|
105
|
+
console.log(` 3. Tras trabajar, corre ${c.bold("ozali sync")} para llevar el histórico al repo de conocimiento.`);
|
|
106
|
+
console.log(` ${c.dim("Salud en cualquier momento:")} ${c.bold("ozali doctor")}`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureOpencodeProfile(cwd) {
|
|
111
|
+
const p = path.join(cwd, "opencode.json");
|
|
112
|
+
const base = readJSON(p, {});
|
|
113
|
+
base["$schema"] = base["$schema"] || "https://opencode.ai/config.json";
|
|
114
|
+
base.permission = base.permission || {};
|
|
115
|
+
const perm = base.permission;
|
|
116
|
+
// Perfil base modo normal: lectura+comandos+fetch libres, ediciones confirman.
|
|
117
|
+
for (const [k, v] of Object.entries({ read: "allow", grep: "allow", glob: "allow", webfetch: "allow", external_directory: "ask", edit: "ask" })) {
|
|
118
|
+
if (perm[k] === undefined) perm[k] = v;
|
|
119
|
+
}
|
|
120
|
+
if (perm.bash === undefined) perm.bash = { "*": "allow", "rm -rf *": "ask", "git push *": "ask" };
|
|
121
|
+
writeJSON(p, base);
|
|
122
|
+
ok(`Perfil base de permisos de opencode en ${c.bold("opencode.json")} (lectura+comandos libres, ediciones confirman).`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =========================================================== doctor ==========
|
|
126
|
+
export function doctor(cwd) {
|
|
127
|
+
step("ozali doctor — health-check (read-only)");
|
|
128
|
+
const env = detectAll(cwd);
|
|
129
|
+
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
130
|
+
const rows = [];
|
|
131
|
+
const add = (label, good, detail) => rows.push({ label, good, detail });
|
|
132
|
+
|
|
133
|
+
add("Repo git", env.git.isRepo, env.git.isRepo ? (env.git.commit ? `${env.git.branch}@${env.git.commit}` : "repo sin commits") : "no es repo git");
|
|
134
|
+
add("Node ≥ 16", env.node.ok, env.node.version);
|
|
135
|
+
add("Fuente de verdad", env.sot.found, env.sot.found ? `${env.sot.doc} + ${env.sot.dir}/` : "ausente (corre la skill 'ozali')");
|
|
136
|
+
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)");
|
|
137
|
+
add("Engram", env.engram.available, env.engram.available ? env.engram.bin : "no instalado → modo docs");
|
|
138
|
+
add("Repo de conocimiento", !!(cfg && cfg.knowledgeRepo && exists(cfg.knowledgeRepo)), cfg ? cfg.knowledgeRepo : "sin configurar (ozali init)");
|
|
139
|
+
|
|
140
|
+
// Strict TDD (de la fuente de verdad)
|
|
141
|
+
const tdd = readStrictTdd(cwd, env.sot);
|
|
142
|
+
add("Strict TDD calibrado", tdd.found, tdd.found ? `strict_tdd: ${tdd.value}` : "sin calibrar (Fase 3.5 del bootstrap)");
|
|
143
|
+
|
|
144
|
+
// Testing signals
|
|
145
|
+
add("Runner de pruebas", env.testing.runners.length > 0, env.testing.runners.join(", ") || "ninguno detectado");
|
|
146
|
+
|
|
147
|
+
const pad = Math.max(...rows.map((r) => r.label.length));
|
|
148
|
+
console.log("");
|
|
149
|
+
for (const r of rows) {
|
|
150
|
+
const mark = r.good ? c.green("✔") : c.yellow("✖");
|
|
151
|
+
console.log(` ${mark} ${r.label.padEnd(pad)} ${c.dim(r.detail)}`);
|
|
152
|
+
}
|
|
153
|
+
const bad = rows.filter((r) => !r.good).length;
|
|
154
|
+
console.log("");
|
|
155
|
+
if (bad === 0) ok("Todo en orden. ozali está listo para trabajar.");
|
|
156
|
+
else warn(`${bad} punto(s) a atender. Revisa los ✖ de arriba.`);
|
|
157
|
+
return bad === 0 ? 0 : 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readStrictTdd(cwd, sot) {
|
|
161
|
+
const f = path.join(cwd, sot.dir, "context", "tech-stack.md");
|
|
162
|
+
if (!exists(f)) return { found: false };
|
|
163
|
+
const txt = fs.readFileSync(f, "utf8");
|
|
164
|
+
const m = txt.match(/Strict\s*TDD[:*\s]+(true|false)/i);
|
|
165
|
+
return m ? { found: true, value: m[1].toLowerCase() } : { found: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =========================================================== update ==========
|
|
169
|
+
export function update(cwd) {
|
|
170
|
+
step("ozali update — actualizar skill instalada");
|
|
171
|
+
const env = detectAll(cwd);
|
|
172
|
+
if (!env.skill.installed) { warn("No hay skill ozali instalada. Corre " + c.bold("ozali init") + " primero."); return 1; }
|
|
173
|
+
for (const p of env.skill.paths) {
|
|
174
|
+
copyDir(SKILL_SRC, p);
|
|
175
|
+
ok(`Actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
|
|
176
|
+
}
|
|
177
|
+
const cfgPath = CONFIG_PATH(cwd);
|
|
178
|
+
const cfg = readJSON(cfgPath);
|
|
179
|
+
if (cfg) { cfg.version = pkgVersion(); cfg.updatedAt = new Date().toISOString(); writeJSON(cfgPath, cfg); }
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================= sync ===========
|
|
184
|
+
export function sync(cwd, opts) {
|
|
185
|
+
step(`ozali sync${opts.import ? " --import" : ""} — histórico ↔ repo de conocimiento`);
|
|
186
|
+
const cfg = readJSON(CONFIG_PATH(cwd));
|
|
187
|
+
if (!cfg || !cfg.knowledgeRepo) { warn("Sin repo de conocimiento configurado. Corre " + c.bold("ozali init") + "."); return 1; }
|
|
188
|
+
const kRepo = cfg.knowledgeRepo;
|
|
189
|
+
if (!exists(kRepo)) { err(`El repo de conocimiento no existe: ${kRepo}`); return 1; }
|
|
190
|
+
const project = cfg.project || projectName(cwd);
|
|
191
|
+
const projDir = path.join(kRepo, "projects", project);
|
|
192
|
+
const docsLocal = path.join(cwd, ".ozali", "docs");
|
|
193
|
+
const engramLocal = path.join(cwd, ".engram");
|
|
194
|
+
|
|
195
|
+
if (opts.import) {
|
|
196
|
+
// Repo de conocimiento → local
|
|
197
|
+
const srcDocs = path.join(projDir, "docs");
|
|
198
|
+
if (exists(srcDocs)) { copyDir(srcDocs, docsLocal); ok("Docs importados a .ozali/docs/."); }
|
|
199
|
+
if (cfg.memoryMode === "hybrid" && tryExec("engram", ["--version"])) {
|
|
200
|
+
info("Ejecutando engram sync --import…");
|
|
201
|
+
tryExec("engram", ["sync", "--import"], { cwd });
|
|
202
|
+
}
|
|
203
|
+
info("Import completo. Revisa .ozali/docs/.");
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Local → repo de conocimiento
|
|
208
|
+
// 1) Engram export (si hybrid + disponible)
|
|
209
|
+
if (cfg.memoryMode === "hybrid" && tryExec("engram", ["--version"])) {
|
|
210
|
+
info("Exportando memorias con engram sync…");
|
|
211
|
+
tryExec("engram", ["sync"], { cwd });
|
|
212
|
+
if (exists(engramLocal)) { copyDir(engramLocal, path.join(kRepo, "engram", project)); ok("Export de Engram copiado al repo de conocimiento."); }
|
|
213
|
+
} else if (cfg.memoryMode === "hybrid") {
|
|
214
|
+
warn("Modo hybrid pero Engram no responde; sincronizo solo docs.");
|
|
215
|
+
}
|
|
216
|
+
// 2) Docs
|
|
217
|
+
if (exists(docsLocal)) { copyDir(docsLocal, path.join(projDir, "docs")); ok(`Docs copiados a projects/${project}/docs/.`); }
|
|
218
|
+
else info("No hay .ozali/docs/ que sincronizar todavía.");
|
|
219
|
+
|
|
220
|
+
// 3) commit (push solo si hay remoto)
|
|
221
|
+
if (exists(path.join(kRepo, ".git"))) {
|
|
222
|
+
tryExec("git", ["add", "-A"], { cwd: kRepo });
|
|
223
|
+
const msg = `sync(${project}): histórico ${new Date().toISOString().slice(0, 19)}`;
|
|
224
|
+
tryExec("git", ["commit", "-m", msg], { cwd: kRepo });
|
|
225
|
+
ok("Commit en el repo de conocimiento.");
|
|
226
|
+
const remote = tryExec("git", ["remote", "get-url", "origin"], { cwd: kRepo });
|
|
227
|
+
if (remote) {
|
|
228
|
+
if (opts.push) { tryExec("git", ["push"], { cwd: kRepo }); ok("Push realizado."); }
|
|
229
|
+
else info("Hay remoto configurado. Usa " + c.bold("ozali sync --push") + " para publicar al equipo.");
|
|
230
|
+
} else {
|
|
231
|
+
info("Sin remoto en el repo de conocimiento. Añade uno (git remote add origin …) para compartir con el equipo.");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// detect.mjs — detección read-only del entorno del proyecto destino.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { exists, which, gitInfo, nodeMajor, HOME } from "./util.mjs";
|
|
5
|
+
|
|
6
|
+
/** Variante de fuente de verdad: {found, doc, dir, variant} */
|
|
7
|
+
export function detectSourceOfTruth(cwd) {
|
|
8
|
+
const variants = [
|
|
9
|
+
{ variant: "AI", doc: "AI.md", dir: ".ai" },
|
|
10
|
+
{ variant: "IA", doc: "IA.md", dir: ".ia" },
|
|
11
|
+
];
|
|
12
|
+
for (const v of variants) {
|
|
13
|
+
const docPath = path.join(cwd, v.doc);
|
|
14
|
+
const dirPath = path.join(cwd, v.dir);
|
|
15
|
+
if (exists(docPath) || exists(dirPath)) {
|
|
16
|
+
return { found: true, ...v, hasDoc: exists(docPath), hasDir: exists(dirPath) };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { found: false, variant: "AI", doc: "AI.md", dir: ".ai" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Agentes presentes: claude-code y/o opencode. */
|
|
23
|
+
export function detectAgents(cwd) {
|
|
24
|
+
const claudeProject = exists(path.join(cwd, ".claude"));
|
|
25
|
+
const claudeGlobal = exists(path.join(HOME, ".claude"));
|
|
26
|
+
const opencodeProject = exists(path.join(cwd, "opencode.json")) || exists(path.join(cwd, ".opencode"));
|
|
27
|
+
const opencodeGlobal = exists(path.join(HOME, ".config", "opencode"));
|
|
28
|
+
return {
|
|
29
|
+
claudeCode: { present: claudeProject || claudeGlobal, project: claudeProject, global: claudeGlobal },
|
|
30
|
+
opencode: { present: opencodeProject || opencodeGlobal, project: opencodeProject, global: opencodeGlobal },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** ¿Está instalada la skill ozali? Devuelve rutas encontradas. */
|
|
35
|
+
export function detectInstalledSkill(cwd) {
|
|
36
|
+
const candidates = [
|
|
37
|
+
path.join(cwd, ".claude", "skills", "ozali"),
|
|
38
|
+
path.join(HOME, ".claude", "skills", "ozali"),
|
|
39
|
+
];
|
|
40
|
+
const found = candidates.filter((p) => exists(path.join(p, "SKILL.md")));
|
|
41
|
+
return { installed: found.length > 0, paths: found };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Engram disponible (binario CLI en PATH). El MCP no se puede sondear desde aquí. */
|
|
45
|
+
export function detectEngram() {
|
|
46
|
+
const bin = which("engram");
|
|
47
|
+
return { available: !!bin, bin: bin || null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Capacidades de testing (heurística read-only). Devuelve runner(s), comando y
|
|
52
|
+
* un conteo aproximado de archivos de prueba. NO resuelve strict_tdd (eso lo
|
|
53
|
+
* hace el bootstrap en la Fase 3.5); aquí solo damos señales.
|
|
54
|
+
*/
|
|
55
|
+
export function detectTesting(cwd) {
|
|
56
|
+
const out = { runners: [], command: null, testFiles: 0, hints: [] };
|
|
57
|
+
const pkg = path.join(cwd, "package.json");
|
|
58
|
+
if (exists(pkg)) {
|
|
59
|
+
try {
|
|
60
|
+
const j = JSON.parse(fs.readFileSync(pkg, "utf8"));
|
|
61
|
+
const deps = { ...j.dependencies, ...j.devDependencies };
|
|
62
|
+
for (const r of ["vitest", "jest", "mocha", "@playwright/test", "cypress", "karma", "ava"]) {
|
|
63
|
+
if (deps[r]) out.runners.push(r);
|
|
64
|
+
}
|
|
65
|
+
if (j.scripts && j.scripts.test) out.command = "npm test";
|
|
66
|
+
} catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
if (exists(path.join(cwd, "go.mod"))) { out.runners.push("go test"); out.command = out.command || "go test ./..."; }
|
|
69
|
+
if (exists(path.join(cwd, "pom.xml"))) { out.runners.push("maven/junit"); out.command = out.command || "mvn test"; }
|
|
70
|
+
if (exists(path.join(cwd, "pyproject.toml")) || exists(path.join(cwd, "pytest.ini"))) {
|
|
71
|
+
out.runners.push("pytest"); out.command = out.command || "pytest";
|
|
72
|
+
}
|
|
73
|
+
out.testFiles = countTestFiles(cwd);
|
|
74
|
+
if (out.runners.length === 0) out.hints.push("No se detectó runner de pruebas (strict_tdd tenderá a false).");
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function countTestFiles(dir, depth = 0, acc = { n: 0 }) {
|
|
79
|
+
if (depth > 6 || acc.n > 9999) return acc.n;
|
|
80
|
+
let entries;
|
|
81
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc.n; }
|
|
82
|
+
for (const e of entries) {
|
|
83
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith(".") && e.isDirectory()) continue;
|
|
84
|
+
const full = path.join(dir, e.name);
|
|
85
|
+
if (e.isDirectory()) countTestFiles(full, depth + 1, acc);
|
|
86
|
+
else if (/\.(spec|test)\.(ts|tsx|js|jsx)$|_test\.go$|Test\.java$|^test_.*\.py$/.test(e.name)) acc.n++;
|
|
87
|
+
}
|
|
88
|
+
return acc.n;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Snapshot completo del entorno. */
|
|
92
|
+
export function detectAll(cwd) {
|
|
93
|
+
return {
|
|
94
|
+
cwd,
|
|
95
|
+
node: { major: nodeMajor(), version: process.versions.node, ok: nodeMajor() >= 16 },
|
|
96
|
+
git: gitInfo(cwd),
|
|
97
|
+
sot: detectSourceOfTruth(cwd),
|
|
98
|
+
agents: detectAgents(cwd),
|
|
99
|
+
skill: detectInstalledSkill(cwd),
|
|
100
|
+
engram: detectEngram(),
|
|
101
|
+
testing: detectTesting(cwd),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// prompt.mjs — prompts interactivos sin dependencias (node:readline callback,
|
|
2
|
+
// compatible con Node 16+; readline/promises no existe en 16, por eso envolvemos).
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import { c } from "./util.mjs";
|
|
5
|
+
|
|
6
|
+
function rl() {
|
|
7
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function question(query) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const i = rl();
|
|
13
|
+
i.question(query, (ans) => { i.close(); resolve(ans); });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const interactive = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
18
|
+
|
|
19
|
+
/** Texto libre con valor por defecto. En no-TTY devuelve el default. */
|
|
20
|
+
export async function ask(message, def = "") {
|
|
21
|
+
if (!interactive()) return def;
|
|
22
|
+
const hint = def ? c.dim(` (${def})`) : "";
|
|
23
|
+
const ans = (await question(`${c.cyan("?")} ${message}${hint}: `)).trim();
|
|
24
|
+
return ans || def;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Confirmación sí/no. En no-TTY devuelve def. */
|
|
28
|
+
export async function confirm(message, def = true) {
|
|
29
|
+
if (!interactive()) return def;
|
|
30
|
+
const hint = def ? "Y/n" : "y/N";
|
|
31
|
+
const ans = (await question(`${c.cyan("?")} ${message} ${c.dim("[" + hint + "]")} `)).trim().toLowerCase();
|
|
32
|
+
if (!ans) return def;
|
|
33
|
+
return ans[0] === "y" || ans[0] === "s"; // acepta sí/yes
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Selección de una opción. options: [{value, label}]. En no-TTY devuelve el
|
|
38
|
+
* primero (o el marcado por defecto).
|
|
39
|
+
*/
|
|
40
|
+
export async function select(message, options, defIndex = 0) {
|
|
41
|
+
if (!interactive()) return options[defIndex].value;
|
|
42
|
+
console.log(`${c.cyan("?")} ${message}`);
|
|
43
|
+
options.forEach((o, i) => {
|
|
44
|
+
const mark = i === defIndex ? c.green("●") : c.dim("○");
|
|
45
|
+
console.log(` ${mark} ${c.bold(String(i + 1))}) ${o.label}`);
|
|
46
|
+
});
|
|
47
|
+
const ans = (await question(` ${c.dim("Elige [" + (defIndex + 1) + "]")}: `)).trim();
|
|
48
|
+
const n = ans ? parseInt(ans, 10) : defIndex + 1;
|
|
49
|
+
const idx = Number.isInteger(n) && n >= 1 && n <= options.length ? n - 1 : defIndex;
|
|
50
|
+
return options[idx].value;
|
|
51
|
+
}
|