@openexpert/opencore 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/README.md +22 -0
- package/bin/opencore.js +24 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +622 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +65 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/model-provider.d.ts +18 -0
- package/dist/model-provider.js +44 -0
- package/dist/model-provider.js.map +1 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.js +9 -0
- package/dist/secrets.js.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +17 -0
- package/dist/storage.js.map +1 -0
- package/dist/tools.d.ts +10 -0
- package/dist/tools.js +21 -0
- package/dist/tools.js.map +1 -0
- package/package.json +97 -0
- package/schema/openexpert.schema.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# OpenCore
|
|
2
|
+
|
|
3
|
+
Motor abierto (MIT) de OpenExpert. Se descarga y funciona en tu máquina, sin
|
|
4
|
+
crear cuentas ni depender de la nube. OpenExpert es **local-first**: no hay
|
|
5
|
+
edición cloud.
|
|
6
|
+
|
|
7
|
+
## Qué incluye
|
|
8
|
+
|
|
9
|
+
- `src/model-provider.ts` — elige modelo (Gemini, BYOK, Ollama local).
|
|
10
|
+
- `src/config.ts` — lee `openexpert.json` + `OPENEXPERT_*`.
|
|
11
|
+
- `src/storage.ts` — interfaz de guardado local.
|
|
12
|
+
- `src/secrets.ts` — interfaz de tokens de Drive (fichero local, `0600`).
|
|
13
|
+
- `src/tools.ts` — catálogo de las 14 herramientas y sus dominios.
|
|
14
|
+
- `src/cli/` — el CLI `opencore` (asistente, doctor, fix, models, serve, update).
|
|
15
|
+
|
|
16
|
+
## Uso
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx @openexpert/opencore
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Ver `openexpert.json.example` en la raíz y `docs/en/11-opencore.md`.
|
package/bin/opencore.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Thin launcher for the OpenCore CLI. The implementation lives in
|
|
4
|
+
// src/cli.ts and is compiled to dist/cli.js. If the package has not been
|
|
5
|
+
// built yet (source checkout), a clear instruction is printed.
|
|
6
|
+
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const dist = join(here, "..", "dist", "cli.js");
|
|
13
|
+
|
|
14
|
+
if (!existsSync(dist)) {
|
|
15
|
+
console.error(
|
|
16
|
+
"OpenCore CLI is not built yet.\n" +
|
|
17
|
+
"Run: npm run build --workspace @openexpert/opencore\n" +
|
|
18
|
+
"(or use `npm run opencore:doctor` from the repository root).",
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mod = await import(dist);
|
|
24
|
+
await mod.run(process.argv.slice(2));
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname } from 'path';
|
|
3
|
+
import { spawn, spawnSync } from 'child_process';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import * as p from '@clack/prompts';
|
|
8
|
+
|
|
9
|
+
// src/cli/commands.ts
|
|
10
|
+
|
|
11
|
+
// src/model-provider.ts
|
|
12
|
+
function ollamaApiBaseUrl(baseUrl) {
|
|
13
|
+
return baseUrl.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
14
|
+
}
|
|
15
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
16
|
+
var DEFAULT_DATA_DIR = "~/.openexpert";
|
|
17
|
+
function expandHome(p2) {
|
|
18
|
+
if (p2 === "~") return homedir();
|
|
19
|
+
if (p2.startsWith("~/")) return join(homedir(), p2.slice(2));
|
|
20
|
+
return p2;
|
|
21
|
+
}
|
|
22
|
+
function packageRoot() {
|
|
23
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
|
+
}
|
|
25
|
+
function packageVersion() {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf8"));
|
|
28
|
+
return pkg.version ?? "0.0.0";
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function findRepoRoot(start = process.cwd()) {
|
|
34
|
+
let dir = resolve(start);
|
|
35
|
+
for (let i = 0; i < 8; i++) {
|
|
36
|
+
if (existsSync(join(dir, "package.json")) && existsSync(join(dir, "packages", "opencore")) && existsSync(join(dir, "vite.config.ts"))) {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
const parent = resolve(dir, "..");
|
|
40
|
+
if (parent === dir) break;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function configPath(cwd = process.cwd()) {
|
|
46
|
+
return join(cwd, "openexpert.json");
|
|
47
|
+
}
|
|
48
|
+
function readConfig(cwd = process.cwd()) {
|
|
49
|
+
try {
|
|
50
|
+
const p2 = configPath(cwd);
|
|
51
|
+
if (!existsSync(p2)) return {};
|
|
52
|
+
return JSON.parse(readFileSync(p2, "utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeConfig(cfg, cwd = process.cwd()) {
|
|
58
|
+
writeFileSync(configPath(cwd), JSON.stringify(cfg, null, 2) + "\n");
|
|
59
|
+
}
|
|
60
|
+
function resolvedDataDir(cwd = process.cwd()) {
|
|
61
|
+
const cfg = readConfig(cwd);
|
|
62
|
+
const raw = process.env["OPENEXPERT_DATA_DIR"] || cfg.dataDir || DEFAULT_DATA_DIR;
|
|
63
|
+
return expandHome(raw);
|
|
64
|
+
}
|
|
65
|
+
function secretsPath(cwd = process.cwd()) {
|
|
66
|
+
return join(resolvedDataDir(cwd), "secrets.json");
|
|
67
|
+
}
|
|
68
|
+
function readSecrets(cwd = process.cwd()) {
|
|
69
|
+
try {
|
|
70
|
+
const p2 = secretsPath(cwd);
|
|
71
|
+
if (!existsSync(p2)) return {};
|
|
72
|
+
return JSON.parse(readFileSync(p2, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writeSecrets(secrets, cwd = process.cwd()) {
|
|
78
|
+
const dir = resolvedDataDir(cwd);
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
const p2 = join(dir, "secrets.json");
|
|
81
|
+
writeFileSync(p2, JSON.stringify(secrets, null, 2), { mode: 384 });
|
|
82
|
+
return p2;
|
|
83
|
+
}
|
|
84
|
+
function secretsAsEnv(cwd = process.cwd()) {
|
|
85
|
+
const s = readSecrets(cwd);
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [k, v] of Object.entries(s)) if (v) out[k] = v;
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
async function httpGetJson(url, opts = {}) {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(url, {
|
|
93
|
+
...opts.headers ? { headers: opts.headers } : {},
|
|
94
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 2500)
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) return null;
|
|
97
|
+
return await res.json();
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function detectOllama(baseUrl = DEFAULT_OLLAMA_URL, timeoutMs = 1500) {
|
|
103
|
+
const root = ollamaApiBaseUrl(baseUrl);
|
|
104
|
+
const data = await httpGetJson(`${root}/api/tags`, {
|
|
105
|
+
timeoutMs
|
|
106
|
+
});
|
|
107
|
+
if (!data) return { up: false, models: [] };
|
|
108
|
+
return { up: true, models: (data.models ?? []).map((m) => m.name) };
|
|
109
|
+
}
|
|
110
|
+
function appCacheDir(version) {
|
|
111
|
+
return join(expandHome(DEFAULT_DATA_DIR), "app", version);
|
|
112
|
+
}
|
|
113
|
+
async function ensureStandaloneApp(version, opts = {}) {
|
|
114
|
+
const dir = appCacheDir(version);
|
|
115
|
+
const entry = join(dir, ".output", "server", "index.mjs");
|
|
116
|
+
if (existsSync(entry)) return dir;
|
|
117
|
+
const repo = opts.repo ?? "OpenExpert-ai/OpenExpert";
|
|
118
|
+
const log = opts.log ?? (() => {
|
|
119
|
+
});
|
|
120
|
+
const tarball = `openexpert-server-${version}.tar.gz`;
|
|
121
|
+
const tags = [`v${version}`, `@openexpert/opencore@${version}`, version];
|
|
122
|
+
for (const tag of tags) {
|
|
123
|
+
const base = `https://github.com/${repo}/releases/download/${tag}`;
|
|
124
|
+
const url = `${base}/${tarball}`;
|
|
125
|
+
log(`downloading ${url}`);
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(6e4) });
|
|
128
|
+
if (!res.ok) continue;
|
|
129
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
130
|
+
try {
|
|
131
|
+
const shaRes = await fetch(`${base}/${tarball}.sha256`, {
|
|
132
|
+
signal: AbortSignal.timeout(15e3)
|
|
133
|
+
});
|
|
134
|
+
if (shaRes.ok) {
|
|
135
|
+
const expected = (await shaRes.text()).trim().split(/\s+/)[0];
|
|
136
|
+
const actual = createHash("sha256").update(buf).digest("hex");
|
|
137
|
+
if (expected && expected !== actual) {
|
|
138
|
+
log("checksum mismatch, aborting");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
mkdirSync(dir, { recursive: true });
|
|
145
|
+
const tmp = join(dir, tarball);
|
|
146
|
+
writeFileSync(tmp, buf);
|
|
147
|
+
const r = spawnSync("tar", ["-xzf", tmp, "-C", dir], { stdio: "ignore" });
|
|
148
|
+
if (r.status !== 0) return null;
|
|
149
|
+
return existsSync(entry) ? dir : null;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function run(cmd, args, opts = {}) {
|
|
156
|
+
return new Promise((res) => {
|
|
157
|
+
const child = spawn(cmd, args, { stdio: "inherit", cwd: opts.cwd, env: opts.env });
|
|
158
|
+
const forward = (sig) => () => child.kill(sig);
|
|
159
|
+
process.on("SIGINT", forward("SIGINT"));
|
|
160
|
+
process.on("SIGTERM", forward("SIGTERM"));
|
|
161
|
+
child.on("exit", (code) => res(code ?? 0));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/cli/commands.ts
|
|
166
|
+
function cmdVersion() {
|
|
167
|
+
console.log(`@openexpert/opencore ${packageVersion()}`);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
function configuredProvider() {
|
|
171
|
+
const raw = process.env["OPENEXPERT_MODEL_PROVIDER"] || readConfig().modelProvider;
|
|
172
|
+
if (raw === "ollama" || raw === "openai-compatible") return raw;
|
|
173
|
+
return "google";
|
|
174
|
+
}
|
|
175
|
+
function cmdDoctor(cwd = process.cwd()) {
|
|
176
|
+
const provider = configuredProvider();
|
|
177
|
+
const dataDir = resolvedDataDir(cwd);
|
|
178
|
+
const secrets = readSecrets(cwd);
|
|
179
|
+
console.log("OpenCore doctor");
|
|
180
|
+
console.log(` provider: ${provider}`);
|
|
181
|
+
console.log(` data dir: ${dataDir}`);
|
|
182
|
+
const problems = [];
|
|
183
|
+
if (provider === "google" && !process.env["GOOGLE_API_KEY"] && !secrets.GOOGLE_API_KEY) {
|
|
184
|
+
problems.push("Falta GOOGLE_API_KEY. Ejecuta `opencore init` o usa Ollama.");
|
|
185
|
+
}
|
|
186
|
+
if (provider === "openai-compatible") {
|
|
187
|
+
if (!process.env["OPENEXPERT_MODEL_KEY"] && !secrets.OPENEXPERT_MODEL_KEY) {
|
|
188
|
+
problems.push("Falta OPENEXPERT_MODEL_KEY.");
|
|
189
|
+
}
|
|
190
|
+
if (!process.env["OPENEXPERT_BASE_URL"] && !secrets.OPENEXPERT_BASE_URL) {
|
|
191
|
+
problems.push("Falta OPENEXPERT_BASE_URL.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (provider === "ollama") {
|
|
195
|
+
console.log(` ollama: ${process.env["OLLAMA_BASE_URL"] || DEFAULT_OLLAMA_URL}`);
|
|
196
|
+
}
|
|
197
|
+
if (!process.env["GOOGLE_CLIENT_ID"] && !secrets.GOOGLE_CLIENT_ID) {
|
|
198
|
+
console.log(" note: sin Google OAuth el chat funciona; Drive queda desconectado.");
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
mkdirSync(dataDir, { recursive: true });
|
|
202
|
+
} catch {
|
|
203
|
+
problems.push(`No se puede escribir en ${dataDir}.`);
|
|
204
|
+
}
|
|
205
|
+
if (!problems.length) {
|
|
206
|
+
console.log(" OK: listo para arrancar.");
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
console.log(" pendiente:");
|
|
210
|
+
for (const x of problems) console.log(` - ${x}`);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
async function cmdFix(cwd = process.cwd()) {
|
|
214
|
+
const dataDir = resolvedDataDir(cwd);
|
|
215
|
+
mkdirSync(dataDir, { recursive: true });
|
|
216
|
+
if (!existsSync(join(cwd, "openexpert.json"))) {
|
|
217
|
+
const ollama = await detectOllama();
|
|
218
|
+
writeConfig(
|
|
219
|
+
{
|
|
220
|
+
modelProvider: "ollama",
|
|
221
|
+
modelId: ollama.models[0] ?? "llama3.1",
|
|
222
|
+
ollamaBaseUrl: DEFAULT_OLLAMA_URL,
|
|
223
|
+
dataDir: "~/.openexpert"
|
|
224
|
+
},
|
|
225
|
+
cwd
|
|
226
|
+
);
|
|
227
|
+
console.log("\u2713 openexpert.json creado (Ollama).");
|
|
228
|
+
} else {
|
|
229
|
+
console.log("\u2022 openexpert.json ya existe.");
|
|
230
|
+
}
|
|
231
|
+
const secrets = readSecrets(cwd);
|
|
232
|
+
if (Object.keys(secrets).length) {
|
|
233
|
+
const p2 = join(dataDir, "secrets.json");
|
|
234
|
+
writeFileSync(p2, JSON.stringify(secrets, null, 2), { mode: 384 });
|
|
235
|
+
console.log(`\u2713 permisos de ${p2} normalizados (0600).`);
|
|
236
|
+
}
|
|
237
|
+
console.log("\u2713 directorio de datos listo:", dataDir);
|
|
238
|
+
cmdDoctor(cwd);
|
|
239
|
+
console.log("\nSiguiente paso: `opencore serve` (o `opencore init` para elegir modelo).");
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
async function cmdModels(cwd = process.cwd()) {
|
|
243
|
+
const provider = configuredProvider();
|
|
244
|
+
const secrets = readSecrets(cwd);
|
|
245
|
+
const cfg = readConfig(cwd);
|
|
246
|
+
if (provider === "ollama") {
|
|
247
|
+
const base = ollamaApiBaseUrl(
|
|
248
|
+
process.env["OLLAMA_BASE_URL"] || cfg.ollamaBaseUrl || DEFAULT_OLLAMA_URL
|
|
249
|
+
);
|
|
250
|
+
const { up, models } = await detectOllama(base);
|
|
251
|
+
if (!up) {
|
|
252
|
+
console.log(`No se puede contactar con Ollama en ${base}.`);
|
|
253
|
+
console.log("Inst\xE1lalo con: curl -fsSL https://ollama.com/install.sh | sh");
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
console.log(`Modelos en Ollama (${base}):`);
|
|
257
|
+
for (const m of models) console.log(` ${m === cfg.modelId ? "*" : " "} ${m}`);
|
|
258
|
+
if (!models.length) console.log(" (ninguno; prueba `ollama pull llama3.1`)");
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
if (provider === "openai-compatible") {
|
|
262
|
+
const base = process.env["OPENEXPERT_BASE_URL"] || secrets.OPENEXPERT_BASE_URL;
|
|
263
|
+
const key = process.env["OPENEXPERT_MODEL_KEY"] || secrets.OPENEXPERT_MODEL_KEY;
|
|
264
|
+
if (!base || !key) {
|
|
265
|
+
console.log("Configura OPENEXPERT_BASE_URL y OPENEXPERT_MODEL_KEY (opencore init).");
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
const data = await httpGetJson(
|
|
269
|
+
`${base.replace(/\/+$/, "")}/models`,
|
|
270
|
+
{ headers: { Authorization: `Bearer ${key}` } }
|
|
271
|
+
);
|
|
272
|
+
if (!data) {
|
|
273
|
+
console.log("No se pudo listar modelos del endpoint.");
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
console.log("Modelos disponibles:");
|
|
277
|
+
for (const m of data.data ?? []) console.log(` ${m.id === cfg.modelId ? "*" : " "} ${m.id}`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
console.log("Proveedor google: usa `gemini-2.5-flash` o consulta Google AI Studio.");
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
async function cmdServe(cwd = process.cwd()) {
|
|
284
|
+
const env = { ...process.env, ...secretsAsEnv(cwd) };
|
|
285
|
+
const repo = findRepoRoot(cwd);
|
|
286
|
+
if (repo) {
|
|
287
|
+
console.log("Arrancando OpenExpert (desde el c\xF3digo fuente)\u2026");
|
|
288
|
+
return run("npm", ["run", "dev"], { cwd: repo, env });
|
|
289
|
+
}
|
|
290
|
+
const version = packageVersion();
|
|
291
|
+
const app = await ensureStandaloneApp(version, { log: (m) => console.log(` ${m}`) });
|
|
292
|
+
if (!app) {
|
|
293
|
+
console.error(
|
|
294
|
+
"No hay un servidor preconstruido disponible y no se pudo descargar.\nOpciones:\n \u2022 Usa Docker: docker run -p 3000:3000 -v openexpert-data:/data ghcr.io/openexpert-ai/openexpert:local\n \u2022 O clona el repositorio y ejecuta `npm ci && npm run dev`."
|
|
295
|
+
);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
console.log("Arrancando OpenCore en http://localhost:3000 \u2026");
|
|
299
|
+
return run(process.execPath, [join(app, ".output", "server", "index.mjs")], {
|
|
300
|
+
cwd: app,
|
|
301
|
+
env: { ...env, PORT: process.env["PORT"] || "3000" }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function cmdUpdate() {
|
|
305
|
+
const dir = appCacheDir(packageVersion());
|
|
306
|
+
if (existsSync(dir)) {
|
|
307
|
+
rmSync(dir, { recursive: true, force: true });
|
|
308
|
+
console.log(`\u2713 cach\xE9 del servidor eliminada (${dir}).`);
|
|
309
|
+
} else {
|
|
310
|
+
console.log("No hay servidor descargado que actualizar.");
|
|
311
|
+
}
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
function port() {
|
|
315
|
+
return process.env["PORT"] || "3000";
|
|
316
|
+
}
|
|
317
|
+
function appUrl() {
|
|
318
|
+
return `http://localhost:${port()}`;
|
|
319
|
+
}
|
|
320
|
+
async function isServerUp(timeoutMs = 1500) {
|
|
321
|
+
try {
|
|
322
|
+
const res = await fetch(appUrl(), { signal: AbortSignal.timeout(timeoutMs) });
|
|
323
|
+
return res.status < 500;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function waitForServer(timeoutMs = 12e4) {
|
|
329
|
+
const start = Date.now();
|
|
330
|
+
while (Date.now() - start < timeoutMs) {
|
|
331
|
+
if (await isServerUp()) return true;
|
|
332
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
function resolveRepo(cwd) {
|
|
337
|
+
const fromEnv = process.env["OPENEXPERT_DESKTOP_REPO"];
|
|
338
|
+
if (fromEnv) {
|
|
339
|
+
const r = findRepoRoot(fromEnv);
|
|
340
|
+
if (r) return r;
|
|
341
|
+
}
|
|
342
|
+
return findRepoRoot(packageRoot()) ?? findRepoRoot(cwd);
|
|
343
|
+
}
|
|
344
|
+
function newestMtime(dir) {
|
|
345
|
+
let newest = 0;
|
|
346
|
+
try {
|
|
347
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
348
|
+
const full = join(dir, entry.name);
|
|
349
|
+
if (entry.isDirectory()) newest = Math.max(newest, newestMtime(full));
|
|
350
|
+
else if (entry.isFile()) newest = Math.max(newest, statSync(full).mtimeMs);
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
return newest;
|
|
355
|
+
}
|
|
356
|
+
function buildIsFresh(repo) {
|
|
357
|
+
const built = join(repo, ".output", "server", "index.mjs");
|
|
358
|
+
if (!existsSync(built)) return false;
|
|
359
|
+
try {
|
|
360
|
+
return statSync(built).mtimeMs >= newestMtime(join(repo, "src"));
|
|
361
|
+
} catch {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function spawnServer(cwd, env) {
|
|
366
|
+
const repo = resolveRepo(cwd);
|
|
367
|
+
if (repo) {
|
|
368
|
+
const built = join(repo, ".output", "server", "index.mjs");
|
|
369
|
+
if (existsSync(built) && buildIsFresh(repo)) {
|
|
370
|
+
return spawn(process.execPath, [built], {
|
|
371
|
+
cwd: repo,
|
|
372
|
+
env: { ...env, NODE_ENV: "production", PORT: port() },
|
|
373
|
+
stdio: "inherit"
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
console.log(
|
|
377
|
+
existsSync(built) ? "La build est\xE1 desactualizada; arrancando en modo desarrollo\u2026" : "Arrancando el servidor en modo desarrollo (no hay build)\u2026"
|
|
378
|
+
);
|
|
379
|
+
return spawn("npm", ["run", "dev"], { cwd: repo, env, stdio: "inherit" });
|
|
380
|
+
}
|
|
381
|
+
const app = await ensureStandaloneApp(packageVersion(), { log: (m) => console.log(` ${m}`) });
|
|
382
|
+
if (!app) return null;
|
|
383
|
+
return spawn(process.execPath, [join(app, ".output", "server", "index.mjs")], {
|
|
384
|
+
cwd: app,
|
|
385
|
+
env: { ...env, NODE_ENV: "production", PORT: port() },
|
|
386
|
+
stdio: "inherit"
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function findDesktopBinary(cwd) {
|
|
390
|
+
const fromEnv = process.env["OPENEXPERT_DESKTOP_BIN"];
|
|
391
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
392
|
+
const candidates = [join(homedir(), ".local", "bin", "openexpert-desktop")];
|
|
393
|
+
const repo = resolveRepo(cwd);
|
|
394
|
+
if (repo) {
|
|
395
|
+
candidates.push(
|
|
396
|
+
join(repo, "src-tauri", "target", "release", "openexpert-desktop"),
|
|
397
|
+
join(repo, "src-tauri", "target", "debug", "openexpert-desktop")
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
401
|
+
}
|
|
402
|
+
async function cmdDesktop(cwd = process.cwd()) {
|
|
403
|
+
const env = { ...process.env, ...secretsAsEnv(cwd) };
|
|
404
|
+
const bin = findDesktopBinary(cwd);
|
|
405
|
+
if (!bin) {
|
|
406
|
+
console.error(
|
|
407
|
+
"No se encontr\xF3 la ventana de escritorio (openexpert-desktop).\nComp\xEDlala con `npx tauri build` o usa `npm run desktop:install`."
|
|
408
|
+
);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
let server = null;
|
|
412
|
+
let startedServer = false;
|
|
413
|
+
if (await isServerUp()) {
|
|
414
|
+
console.log(`\u2022 Usando el servidor ya activo en ${appUrl()}`);
|
|
415
|
+
} else {
|
|
416
|
+
console.log("Arrancando el servidor local\u2026");
|
|
417
|
+
server = await spawnServer(cwd, env);
|
|
418
|
+
if (!server) {
|
|
419
|
+
console.error(
|
|
420
|
+
"No se pudo arrancar el servidor.\nSi usas el c\xF3digo fuente, entra en el repositorio y ejecuta:\n npm ci && npm run build && npm run desktop:install"
|
|
421
|
+
);
|
|
422
|
+
return 1;
|
|
423
|
+
}
|
|
424
|
+
startedServer = true;
|
|
425
|
+
if (!await waitForServer()) {
|
|
426
|
+
console.error("El servidor no respondi\xF3 a tiempo.");
|
|
427
|
+
server.kill("SIGTERM");
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const stopServer = () => {
|
|
432
|
+
if (startedServer && server && !server.killed) {
|
|
433
|
+
try {
|
|
434
|
+
server.kill("SIGTERM");
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
const onSignal = () => {
|
|
440
|
+
stopServer();
|
|
441
|
+
process.exit(0);
|
|
442
|
+
};
|
|
443
|
+
process.once("SIGINT", onSignal);
|
|
444
|
+
process.once("SIGTERM", onSignal);
|
|
445
|
+
console.log("Abriendo la ventana de OpenExpert\u2026");
|
|
446
|
+
const code = await new Promise((resolve2) => {
|
|
447
|
+
const win = spawn(bin, [], { stdio: "inherit", env });
|
|
448
|
+
win.on("exit", (c) => resolve2(c ?? 0));
|
|
449
|
+
});
|
|
450
|
+
stopServer();
|
|
451
|
+
return code;
|
|
452
|
+
}
|
|
453
|
+
function bail() {
|
|
454
|
+
p.cancel("Configuraci\xF3n cancelada.");
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
async function runWizard(opts = {}) {
|
|
458
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
459
|
+
const existing = readConfig(cwd);
|
|
460
|
+
const nonInteractive = opts.assumeYes || !process.stdin.isTTY;
|
|
461
|
+
if (nonInteractive) {
|
|
462
|
+
const ollama2 = await detectOllama();
|
|
463
|
+
const config2 = {
|
|
464
|
+
modelProvider: "ollama",
|
|
465
|
+
modelId: ollama2.models[0] ?? "llama3.1",
|
|
466
|
+
ollamaBaseUrl: DEFAULT_OLLAMA_URL,
|
|
467
|
+
dataDir: "~/.openexpert"
|
|
468
|
+
};
|
|
469
|
+
writeConfig(config2, cwd);
|
|
470
|
+
return { config: config2, secrets: {}, provider: "ollama", modelId: config2.modelId, started: false };
|
|
471
|
+
}
|
|
472
|
+
p.intro("OpenCore \u2014 configuraci\xF3n");
|
|
473
|
+
const spin = p.spinner();
|
|
474
|
+
spin.start("Buscando Ollama local\u2026");
|
|
475
|
+
const ollama = await detectOllama();
|
|
476
|
+
spin.stop(
|
|
477
|
+
ollama.up ? `Ollama detectado (${ollama.models.length} modelo(s))` : "No se ha detectado Ollama"
|
|
478
|
+
);
|
|
479
|
+
const choice = await p.select({
|
|
480
|
+
message: "\xBFQu\xE9 modelo quieres usar?",
|
|
481
|
+
options: [
|
|
482
|
+
...ollama.up ? [
|
|
483
|
+
{
|
|
484
|
+
value: "ollama",
|
|
485
|
+
label: "Modelo local (Ollama)",
|
|
486
|
+
hint: "sin claves, datos en tu equipo"
|
|
487
|
+
}
|
|
488
|
+
] : [],
|
|
489
|
+
{ value: "google", label: "Google Gemini (clave gratuita)", hint: "AI Studio" },
|
|
490
|
+
{ value: "openai-compatible", label: "Otro endpoint compatible", hint: "BYOK" },
|
|
491
|
+
...!ollama.up ? [{ value: "install-ollama", label: "Instalar Ollama", hint: "te muestro c\xF3mo" }] : []
|
|
492
|
+
]
|
|
493
|
+
});
|
|
494
|
+
if (p.isCancel(choice)) bail();
|
|
495
|
+
if (choice === "install-ollama") {
|
|
496
|
+
p.note(
|
|
497
|
+
"Instala Ollama y descarga un modelo:\n\n curl -fsSL https://ollama.com/install.sh | sh\n ollama pull llama3.1\n\nVuelve a ejecutar `opencore init` cuando est\xE9 listo.",
|
|
498
|
+
"Instalar Ollama"
|
|
499
|
+
);
|
|
500
|
+
p.outro("Sin cambios.");
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
const config = { dataDir: "~/.openexpert" };
|
|
504
|
+
const secrets = {};
|
|
505
|
+
let modelId = "llama3.1";
|
|
506
|
+
if (choice === "ollama") {
|
|
507
|
+
const models = ollama.models.length ? ollama.models : ["llama3.1"];
|
|
508
|
+
const picked = await p.select({
|
|
509
|
+
message: "Modelo de Ollama",
|
|
510
|
+
options: models.map((m) => ({ value: m, label: m }))
|
|
511
|
+
});
|
|
512
|
+
if (p.isCancel(picked)) bail();
|
|
513
|
+
modelId = String(picked);
|
|
514
|
+
config.modelProvider = "ollama";
|
|
515
|
+
config.modelId = modelId;
|
|
516
|
+
config.ollamaBaseUrl = DEFAULT_OLLAMA_URL;
|
|
517
|
+
} else if (choice === "google") {
|
|
518
|
+
const apiKey = await p.password({ message: "GOOGLE_API_KEY (AI Studio)" });
|
|
519
|
+
if (p.isCancel(apiKey)) bail();
|
|
520
|
+
const id = await p.text({ message: "Modelo", initialValue: "gemini-2.5-flash" });
|
|
521
|
+
if (p.isCancel(id)) bail();
|
|
522
|
+
modelId = String(id) || "gemini-2.5-flash";
|
|
523
|
+
config.modelProvider = "google";
|
|
524
|
+
config.modelId = modelId;
|
|
525
|
+
secrets.GOOGLE_API_KEY = String(apiKey);
|
|
526
|
+
} else if (choice === "openai-compatible") {
|
|
527
|
+
const baseURL = await p.text({ message: "Base URL (\u2026/v1)", initialValue: "" });
|
|
528
|
+
if (p.isCancel(baseURL)) bail();
|
|
529
|
+
const apiKey = await p.password({ message: "API key" });
|
|
530
|
+
if (p.isCancel(apiKey)) bail();
|
|
531
|
+
const id = await p.text({ message: "Modelo", initialValue: "gpt-4o-mini" });
|
|
532
|
+
if (p.isCancel(id)) bail();
|
|
533
|
+
modelId = String(id) || "gpt-4o-mini";
|
|
534
|
+
config.modelProvider = "openai-compatible";
|
|
535
|
+
config.modelId = modelId;
|
|
536
|
+
secrets.OPENEXPERT_BASE_URL = String(baseURL);
|
|
537
|
+
secrets.OPENEXPERT_MODEL_KEY = String(apiKey);
|
|
538
|
+
}
|
|
539
|
+
writeConfig({ ...existing, ...config }, cwd);
|
|
540
|
+
if (Object.keys(secrets).length) writeSecrets({ ...secrets }, cwd);
|
|
541
|
+
p.outro(
|
|
542
|
+
`Configuraci\xF3n guardada en openexpert.json (modelo: ${config.modelProvider}:${modelId}).`
|
|
543
|
+
);
|
|
544
|
+
const go = await p.confirm({ message: "\xBFArrancar ahora?", initialValue: true });
|
|
545
|
+
if (p.isCancel(go)) bail();
|
|
546
|
+
return {
|
|
547
|
+
config: { ...existing, ...config },
|
|
548
|
+
secrets,
|
|
549
|
+
provider: config.modelProvider,
|
|
550
|
+
modelId,
|
|
551
|
+
started: Boolean(go)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/cli.ts
|
|
556
|
+
function help() {
|
|
557
|
+
console.log(`opencore \u2014 motor abierto de OpenExpert
|
|
558
|
+
|
|
559
|
+
Uso:
|
|
560
|
+
opencore asistente de configuraci\xF3n y arranque
|
|
561
|
+
opencore init igual que sin argumentos
|
|
562
|
+
opencore serve arranca la edici\xF3n local en http://localhost:3000
|
|
563
|
+
opencore desktop arranca el servidor y abre la ventana de escritorio
|
|
564
|
+
opencore doctor revisa tu configuraci\xF3n
|
|
565
|
+
opencore fix auto-configura lo que falte
|
|
566
|
+
opencore models lista los modelos del proveedor actual
|
|
567
|
+
opencore update limpia la cach\xE9 del servidor descargado
|
|
568
|
+
opencore version muestra la versi\xF3n
|
|
569
|
+
|
|
570
|
+
Ejemplos:
|
|
571
|
+
npx @openexpert/opencore
|
|
572
|
+
npx @openexpert/opencore serve
|
|
573
|
+
|
|
574
|
+
Docker (sin Node):
|
|
575
|
+
docker run -p 3000:3000 -v openexpert-data:/data ghcr.io/openexpert-ai/openexpert`);
|
|
576
|
+
}
|
|
577
|
+
async function run2(argv) {
|
|
578
|
+
const cmd = argv[0] ?? "";
|
|
579
|
+
switch (cmd) {
|
|
580
|
+
case "":
|
|
581
|
+
case "init":
|
|
582
|
+
case "setup": {
|
|
583
|
+
const res = await runWizard({});
|
|
584
|
+
if (res.started) process.exit(await cmdServe());
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
case "doctor":
|
|
588
|
+
return process.exit(cmdDoctor());
|
|
589
|
+
case "fix":
|
|
590
|
+
return process.exit(await cmdFix());
|
|
591
|
+
case "models":
|
|
592
|
+
return process.exit(await cmdModels());
|
|
593
|
+
case "serve":
|
|
594
|
+
case "dev":
|
|
595
|
+
return process.exit(await cmdServe());
|
|
596
|
+
case "desktop":
|
|
597
|
+
case "app": {
|
|
598
|
+
const code = await cmdDesktop();
|
|
599
|
+
return process.exit(code);
|
|
600
|
+
}
|
|
601
|
+
case "update":
|
|
602
|
+
return process.exit(cmdUpdate());
|
|
603
|
+
case "version":
|
|
604
|
+
case "--version":
|
|
605
|
+
case "-v":
|
|
606
|
+
return process.exit(cmdVersion());
|
|
607
|
+
case "help":
|
|
608
|
+
case "--help":
|
|
609
|
+
case "-h":
|
|
610
|
+
help();
|
|
611
|
+
return;
|
|
612
|
+
default:
|
|
613
|
+
console.error(`Comando desconocido: ${cmd}
|
|
614
|
+
`);
|
|
615
|
+
help();
|
|
616
|
+
return process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export { run2 as run };
|
|
621
|
+
//# sourceMappingURL=cli.js.map
|
|
622
|
+
//# sourceMappingURL=cli.js.map
|