@synap-core/eve 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.
Files changed (64) hide show
  1. package/README.md +251 -0
  2. package/dist/add-R6RGUYER.js +22 -0
  3. package/dist/add-R6RGUYER.js.map +1 -0
  4. package/dist/auth-7FFZNARZ-4OIRGZ7R.js +31 -0
  5. package/dist/auth-7FFZNARZ-4OIRGZ7R.js.map +1 -0
  6. package/dist/chunk-22RIHU4J.js +975 -0
  7. package/dist/chunk-22RIHU4J.js.map +1 -0
  8. package/dist/chunk-23XQDB7K.js +1773 -0
  9. package/dist/chunk-23XQDB7K.js.map +1 -0
  10. package/dist/chunk-5TBV2VHZ.js +6067 -0
  11. package/dist/chunk-5TBV2VHZ.js.map +1 -0
  12. package/dist/chunk-D7P3IYYR.js +44 -0
  13. package/dist/chunk-D7P3IYYR.js.map +1 -0
  14. package/dist/chunk-EHPC7CKE.js +757 -0
  15. package/dist/chunk-EHPC7CKE.js.map +1 -0
  16. package/dist/chunk-FAE5UZFE.js +17 -0
  17. package/dist/chunk-FAE5UZFE.js.map +1 -0
  18. package/dist/chunk-HRF6TNXX.js +53 -0
  19. package/dist/chunk-HRF6TNXX.js.map +1 -0
  20. package/dist/chunk-HVJEW7TU.js +495 -0
  21. package/dist/chunk-HVJEW7TU.js.map +1 -0
  22. package/dist/chunk-JO5H5G6U.js +1529 -0
  23. package/dist/chunk-JO5H5G6U.js.map +1 -0
  24. package/dist/chunk-MUEQNBE6.js +1140 -0
  25. package/dist/chunk-MUEQNBE6.js.map +1 -0
  26. package/dist/chunk-O5WTCVMZ.js +3391 -0
  27. package/dist/chunk-O5WTCVMZ.js.map +1 -0
  28. package/dist/chunk-ROKIQIEX.js +65 -0
  29. package/dist/chunk-ROKIQIEX.js.map +1 -0
  30. package/dist/chunk-T7GCAJK5.js +4895 -0
  31. package/dist/chunk-T7GCAJK5.js.map +1 -0
  32. package/dist/chunk-UOQWJ3S4.js +138 -0
  33. package/dist/chunk-UOQWJ3S4.js.map +1 -0
  34. package/dist/chunk-YQWV5W4U.js +3479 -0
  35. package/dist/chunk-YQWV5W4U.js.map +1 -0
  36. package/dist/chunk-ZPDNZRVW.js +272 -0
  37. package/dist/chunk-ZPDNZRVW.js.map +1 -0
  38. package/dist/config-store-KTOWIKAS-XVZHNPKW.js +11 -0
  39. package/dist/config-store-KTOWIKAS-XVZHNPKW.js.map +1 -0
  40. package/dist/dist-2VRFSXMO.js +40 -0
  41. package/dist/dist-2VRFSXMO.js.map +1 -0
  42. package/dist/dist-C2QB4IKT.js +340 -0
  43. package/dist/dist-C2QB4IKT.js.map +1 -0
  44. package/dist/dist-DEIUVZ57.js +299 -0
  45. package/dist/dist-DEIUVZ57.js.map +1 -0
  46. package/dist/dist-DSKHGECB.js +36 -0
  47. package/dist/dist-DSKHGECB.js.map +1 -0
  48. package/dist/dist-GN2DN7CI.js +95 -0
  49. package/dist/dist-GN2DN7CI.js.map +1 -0
  50. package/dist/dist-TZL4XW2R.js +58 -0
  51. package/dist/dist-TZL4XW2R.js.map +1 -0
  52. package/dist/dist-ZP2APOYV.js +53 -0
  53. package/dist/dist-ZP2APOYV.js.map +1 -0
  54. package/dist/index.js +15083 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/operational-5RT6AZJI-ZEMPD32Q.js +12 -0
  57. package/dist/operational-5RT6AZJI-ZEMPD32Q.js.map +1 -0
  58. package/dist/reconcile-A4I3Z7VX-GQMEN4GC.js +12 -0
  59. package/dist/reconcile-A4I3Z7VX-GQMEN4GC.js.map +1 -0
  60. package/dist/rsshub-GOJBTNOD-UNNVKBFL.js +18 -0
  61. package/dist/rsshub-GOJBTNOD-UNNVKBFL.js.map +1 -0
  62. package/dist/synap-image-install-X36REYQ5-UGO4MAOP.js +17 -0
  63. package/dist/synap-image-install-X36REYQ5-UGO4MAOP.js.map +1 -0
  64. package/package.json +44 -0
@@ -0,0 +1,1140 @@
1
+ import { createRequire as __eveCreateRequire } from 'module'; const require = __eveCreateRequire(import.meta.url);
2
+ import {
3
+ materializeTargets,
4
+ normalizeBareDomain
5
+ } from "./chunk-YQWV5W4U.js";
6
+ import {
7
+ runBrainInit,
8
+ runInferenceInit
9
+ } from "./chunk-22RIHU4J.js";
10
+ import {
11
+ installDashboardContainer,
12
+ runLegsProxySetup,
13
+ verifyComponent
14
+ } from "./chunk-23XQDB7K.js";
15
+ import {
16
+ resolveSynapDelegate
17
+ } from "./chunk-HVJEW7TU.js";
18
+ import {
19
+ entityStateManager
20
+ } from "./chunk-5TBV2VHZ.js";
21
+ import {
22
+ COMPONENTS,
23
+ resolveComponent
24
+ } from "./chunk-O5WTCVMZ.js";
25
+ import {
26
+ readEveSecrets,
27
+ writeEveSecrets
28
+ } from "./chunk-T7GCAJK5.js";
29
+
30
+ // src/commands/add.ts
31
+ import { execa } from "execa";
32
+ import { execFile } from "child_process";
33
+ import { promisify } from "util";
34
+ import { existsSync } from "fs";
35
+ import { join } from "path";
36
+ import { select, text, isCancel, cancel } from "@clack/prompts";
37
+
38
+ // src/lib/ui.ts
39
+ import chalk from "chalk";
40
+ import boxen from "boxen";
41
+ var colors = {
42
+ primary: chalk.hex("#6366f1"),
43
+ success: chalk.green,
44
+ warning: chalk.yellow,
45
+ error: chalk.red,
46
+ info: chalk.blue,
47
+ muted: chalk.gray,
48
+ brain: chalk.hex("#f59e0b"),
49
+ arms: chalk.green,
50
+ builder: chalk.blue,
51
+ eyes: chalk.magenta,
52
+ legs: chalk.cyan
53
+ };
54
+ var emojis = {
55
+ brain: "\u{1F9E0}",
56
+ arms: "\u{1F9BE}",
57
+ builder: "\u{1F3D7}\uFE0F",
58
+ eyes: "\u{1F441}\uFE0F",
59
+ legs: "\u{1F9BF}",
60
+ entity: "\u{1F33F}",
61
+ success: "\u2705",
62
+ error: "\u274C",
63
+ warning: "\u26A0\uFE0F",
64
+ info: "\u2139\uFE0F",
65
+ sparkles: "\u2728",
66
+ check: "\u2713",
67
+ cross: "\u2717",
68
+ bullet: "\u2022"
69
+ };
70
+ var CLEAR_LINE = "\r\x1B[2K";
71
+ function createSpinner(text2) {
72
+ let interval;
73
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
74
+ let i = 0;
75
+ return {
76
+ start() {
77
+ process.stdout.write(`${CLEAR_LINE}${colors.info(`${frames[0]} ${text2}`)}`);
78
+ interval = setInterval(() => {
79
+ process.stdout.write(`${CLEAR_LINE}${colors.info(`${frames[i]} ${text2}`)}`);
80
+ i = (i + 1) % frames.length;
81
+ }, 80);
82
+ },
83
+ succeed(msg) {
84
+ clearInterval(interval);
85
+ console.log(`${CLEAR_LINE}${colors.success(`${emojis.check} ${msg || text2}`)}`);
86
+ },
87
+ fail(msg) {
88
+ clearInterval(interval);
89
+ console.log(`${CLEAR_LINE}${colors.error(`${emojis.cross} ${msg || text2}`)}`);
90
+ },
91
+ warn(msg) {
92
+ clearInterval(interval);
93
+ console.log(`${CLEAR_LINE}${colors.warning(`${emojis.warning} ${msg || text2}`)}`);
94
+ }
95
+ };
96
+ }
97
+ function printHeader(title, emoji) {
98
+ console.log();
99
+ console.log(colors.primary.bold(`${emoji ? emoji + " " : ""}${title}`));
100
+ console.log(colors.primary("\u2500".repeat(title.length + (emoji ? 2 : 0))));
101
+ }
102
+ function printSuccess(message) {
103
+ console.log(colors.success(`${emojis.success} ${message}`));
104
+ }
105
+ function printError(message) {
106
+ console.log(colors.error(`${emojis.error} ${message}`));
107
+ }
108
+ function printWarning(message) {
109
+ console.log(colors.warning(`${emojis.warning} ${message}`));
110
+ }
111
+ function printInfo(message) {
112
+ console.log(colors.info(`${emojis.info} ${message}`));
113
+ }
114
+ function printKeyValue(key, value, keyWidth = 20) {
115
+ const paddedKey = key.padEnd(keyWidth);
116
+ console.log(`${colors.muted(paddedKey)} ${value}`);
117
+ }
118
+ function formatOrgan(organ) {
119
+ const organColors = {
120
+ brain: colors.brain,
121
+ arms: colors.arms,
122
+ builder: colors.builder,
123
+ eyes: colors.eyes,
124
+ legs: colors.legs
125
+ };
126
+ const color = organColors[organ] || colors.info;
127
+ const emoji = emojis[organ] || emojis.bullet;
128
+ return color(`${emoji} ${organ.charAt(0).toUpperCase() + organ.slice(1)}`);
129
+ }
130
+ function printBox(title, lines) {
131
+ const body = lines.join("\n");
132
+ console.log(
133
+ boxen(`${colors.primary.bold(title)}
134
+
135
+ ${body}`, {
136
+ padding: 1,
137
+ margin: { top: 0, bottom: 1, left: 0, right: 0 },
138
+ borderStyle: "round",
139
+ borderColor: "#6366f1"
140
+ })
141
+ );
142
+ }
143
+
144
+ // src/commands/add.ts
145
+ var execFileAsync = promisify(execFile);
146
+ async function containerExists(name) {
147
+ try {
148
+ const { stdout } = await execFileAsync(
149
+ "docker",
150
+ ["ps", "-a", "--filter", `name=^${name}$`, "--format", "{{.Names}}"],
151
+ { timeout: 4e3 }
152
+ );
153
+ return stdout.trim() === name;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ async function findSynapComposeContainer(service) {
159
+ try {
160
+ const { stdout } = await execFileAsync(
161
+ "docker",
162
+ [
163
+ "ps",
164
+ "--filter",
165
+ "label=com.docker.compose.project=synap-backend",
166
+ "--filter",
167
+ `label=com.docker.compose.service=${service}`,
168
+ "--format",
169
+ "{{.Names}}"
170
+ ],
171
+ { timeout: 4e3 }
172
+ );
173
+ return stdout.trim().split("\n")[0]?.trim() || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ async function findSynapBackendContainer() {
179
+ return findSynapComposeContainer("backend");
180
+ }
181
+ function hasCanonicalSynapDeployment() {
182
+ const delegate = resolveSynapDelegate();
183
+ return Boolean(delegate && existsSync(join(delegate.deployDir, ".env")));
184
+ }
185
+ async function reportExistingSynapDeployment() {
186
+ const delegate = resolveSynapDelegate();
187
+ if (!delegate || !hasCanonicalSynapDeployment()) return false;
188
+ const [backend, podAdmin] = await Promise.all([
189
+ findSynapComposeContainer("backend"),
190
+ findSynapComposeContainer("pod-admin")
191
+ ]);
192
+ printWarning(`Synap Data Pod is already installed at ${delegate.repoRoot}.`);
193
+ if (backend && podAdmin) {
194
+ await entityStateManager.updateComponentEntry("synap", { state: "ready" });
195
+ printInfo(` Backend (${backend}) and Pod Admin (${podAdmin}) are running.`);
196
+ } else {
197
+ await entityStateManager.updateComponentEntry("synap", { state: "error" });
198
+ const missing = [!backend && "backend", !podAdmin && "pod-admin"].filter(Boolean).join(", ");
199
+ printWarning(` Pod service${missing.includes(",") ? "s" : ""} not running: ${missing}.`);
200
+ printInfo(" Eve will not reinstall the Pod or replace its identity.");
201
+ printInfo(" Recover it with: eve update synap");
202
+ }
203
+ return true;
204
+ }
205
+ async function findSynapPostgresContainer() {
206
+ try {
207
+ const { stdout } = await execFileAsync(
208
+ "docker",
209
+ [
210
+ "ps",
211
+ "--filter",
212
+ "label=com.docker.compose.service=postgres",
213
+ "--filter",
214
+ "ancestor=postgres",
215
+ "--format",
216
+ "{{.Names}}"
217
+ ],
218
+ { timeout: 4e3 }
219
+ );
220
+ return stdout.trim().split("\n")[0]?.trim() || null;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ async function isBrainReady() {
226
+ const state = await entityStateManager.getState();
227
+ if (state.organs.brain.state === "ready") return true;
228
+ const container = await findSynapBackendContainer();
229
+ if (!container) return false;
230
+ printInfo(`Synap container (${container}) is running \u2014 reconciling state to ready.`);
231
+ await entityStateManager.updateOrgan("brain", "ready");
232
+ return true;
233
+ }
234
+ async function addTraefik() {
235
+ await runLegsProxySetup({ standalone: true });
236
+ }
237
+ async function addSynap() {
238
+ const flags = process.argv.slice(2);
239
+ const flagRepoIdx = flags.indexOf("--synap-repo");
240
+ const flagRepo = flagRepoIdx >= 0 ? flags[flagRepoIdx + 1] : void 0;
241
+ const envRepo = process.env.SYNAP_REPO_ROOT;
242
+ const delegate = resolveSynapDelegate();
243
+ const repoRoot = flagRepo || envRepo || delegate?.repoRoot;
244
+ if (!repoRoot || !existsSync(repoRoot)) {
245
+ printWarning(
246
+ `Synap installation requires a synap-backend checkout.
247
+ Pass --synap-repo <path>, set SYNAP_REPO_ROOT, or clone to /opt/synap-backend.
248
+ See: https://github.com/synap/synap-backend`
249
+ );
250
+ process.exit(1);
251
+ }
252
+ const secrets = await readEveSecrets(process.env.EVE_HOME ?? process.cwd()).catch(() => null);
253
+ const flagDomainIdx = flags.indexOf("--domain");
254
+ const flagDomain = flagDomainIdx >= 0 ? flags[flagDomainIdx + 1] : void 0;
255
+ const domain = normalizeBareDomain(flagDomain) || normalizeBareDomain(secrets?.domain?.primary) || "localhost";
256
+ const email = process.env.LETSENCRYPT_EMAIL?.trim() || secrets?.domain?.email?.trim() || (domain !== "localhost" ? `noreply@${domain}` : void 0);
257
+ await runBrainInit({
258
+ synapRepo: repoRoot,
259
+ domain,
260
+ email,
261
+ adminBootstrapMode: "token",
262
+ withAi: false,
263
+ withOpenclaw: false,
264
+ withRsshub: false
265
+ });
266
+ }
267
+ async function addOllama(model) {
268
+ await runInferenceInit({ model, withGateway: true, internalOllamaOnly: true });
269
+ }
270
+ async function addOpenclaw() {
271
+ if (!await isBrainReady()) {
272
+ printError("Brain is not ready. Please install Synap first: `eve add synap`");
273
+ process.exit(1);
274
+ }
275
+ const synapScript = process.env.SYNAP_SETUP_SCRIPT;
276
+ if (synapScript && existsSync(synapScript)) {
277
+ await execa("bash", [synapScript, "profiles", "enable", "openclaw"], {
278
+ env: { ...process.env, SYNAP_DEPLOY_DIR: process.env.SYNAP_DEPLOY_DIR || "", SYNAP_ASSUME_YES: "1" },
279
+ stdio: "inherit"
280
+ });
281
+ await execa("bash", [synapScript, "services", "add", "openclaw"], {
282
+ env: { ...process.env, SYNAP_DEPLOY_DIR: process.env.SYNAP_DEPLOY_DIR || "", SYNAP_ASSUME_YES: "1" },
283
+ stdio: "inherit"
284
+ });
285
+ } else {
286
+ printWarning("OpenClaw add via Synap delegate not available.");
287
+ printInfo(" Set SYNAP_SETUP_SCRIPT to point to synap-backend/setup.sh for auto-provisioning.");
288
+ printInfo(" Otherwise install OpenClaw manually: https://github.com/danielmiessler/openclaw");
289
+ }
290
+ }
291
+ async function applyNangoConnectUiPolyfill(originalDockerRunArgs) {
292
+ const hostPath = "/opt/nango/connect-ui-index.html";
293
+ const containerPath = "/app/nango/packages/connect-ui/dist/index.html";
294
+ const containerName = "eve-arms-nango";
295
+ const image = "nangohq/nango-server:hosted";
296
+ printInfo("Applying setImmediate polyfill to Nango Connect UI...");
297
+ const extractAndPatch = `
298
+ const fs = require('fs');
299
+ const p = '${containerPath}';
300
+ let h = fs.readFileSync(p, 'utf8');
301
+ if (h.includes('window.setImmediate = window.setImmediate ||')) {
302
+ console.log('already patched'); process.exit(0);
303
+ }
304
+ const poly = '<script>\\n(function() {\\n' +
305
+ ' window.setImmediate = window.setImmediate || function(fn) {\\n' +
306
+ ' var args = Array.prototype.slice.call(arguments, 1);\\n' +
307
+ ' var id = setTimeout(function() { fn.apply(null, args); }, 0);\\n' +
308
+ ' return { _id: id, type: \\'Immediate\\' };\\n' +
309
+ ' };\\n' +
310
+ ' window.clearImmediate = window.clearImmediate || function(h) { clearTimeout(h && h._id !== undefined ? h._id : h); };\\n' +
311
+ '})();\\n' +
312
+ '<\\/script>';
313
+ // Insert as first element in <head> so it runs before any module scripts
314
+ h = h.replace('<head>', '<head>\\n' + poly);
315
+ fs.writeFileSync(p, h);
316
+ console.log('patched ok');
317
+ `.trim();
318
+ const tmpContainer = "eve-nango-polyfill-tmp";
319
+ try {
320
+ await execFileAsync("docker", ["rm", "-f", tmpContainer], { timeout: 1e4 }).catch(() => {
321
+ });
322
+ await execFileAsync("docker", ["create", "--name", tmpContainer, image], { timeout: 15e3 });
323
+ await execFileAsync("docker", ["exec", tmpContainer, "node", "-e", extractAndPatch], { timeout: 15e3 });
324
+ await execFileAsync("sh", ["-c", `mkdir -p "$(dirname ${hostPath})"`], { timeout: 5e3 }).catch(() => {
325
+ });
326
+ await execFileAsync("docker", ["cp", `${tmpContainer}:${containerPath}`, hostPath], { timeout: 1e4 });
327
+ await execFileAsync("docker", ["rm", "-f", tmpContainer], { timeout: 1e4 }).catch(() => {
328
+ });
329
+ } catch (err) {
330
+ printWarning(` Could not patch via temp container: ${err.message}`);
331
+ try {
332
+ await execFileAsync("docker", ["exec", containerName, "node", "-e", extractAndPatch], { timeout: 15e3 });
333
+ await execFileAsync("docker", ["cp", `${containerName}:${containerPath}`, hostPath], { timeout: 1e4 });
334
+ } catch (err2) {
335
+ printWarning(` Fallback also failed: ${err2.message}`);
336
+ return;
337
+ }
338
+ }
339
+ printInfo(" Recreating container with polyfill volume mount...");
340
+ await execFileAsync("docker", ["rm", "-f", containerName], { timeout: 1e4 }).catch(() => {
341
+ });
342
+ const newArgs = [
343
+ ...originalDockerRunArgs,
344
+ "-v",
345
+ `${hostPath}:${containerPath}:ro`
346
+ ];
347
+ await execFileAsync("docker", newArgs, { timeout: 3e4 });
348
+ printInfo(" Container recreated \u2014 polyfill is now permanent.");
349
+ }
350
+ async function nangoAutoSignup(secretKey, ownerEmail) {
351
+ const email = ownerEmail ?? "admin@eve.local";
352
+ const pw = `Nango_${secretKey.slice(0, 12)}`;
353
+ const node = `
354
+ const attempt = (n) => fetch('http://127.0.0.1:3003/api/v1/account/signup', {
355
+ method: 'POST',
356
+ headers: { 'Content-Type': 'application/json' },
357
+ body: JSON.stringify({ name: 'Admin', email: ${JSON.stringify(email)}, password: ${JSON.stringify(pw)} }),
358
+ }).then(r => r.json()).then(d => {
359
+ const code = d?.error?.code ?? d?.error;
360
+ if (d?.data?.uuid || code === 'account_already_exists' || code === 'email_not_verified') process.exit(0);
361
+ if (n > 0) setTimeout(() => attempt(n - 1), 2000); else process.exit(1);
362
+ }).catch(() => { if (n > 0) setTimeout(() => attempt(n - 1), 2000); else process.exit(1); });
363
+ attempt(15);
364
+ `;
365
+ await execFileAsync("docker", ["exec", "eve-arms-nango", "node", "-e", node], { timeout: 4e4 }).catch(() => {
366
+ });
367
+ const pgContainer = await findSynapPostgresContainer();
368
+ if (pgContainer) {
369
+ await execFileAsync("docker", [
370
+ "exec",
371
+ pgContainer,
372
+ "psql",
373
+ "-U",
374
+ "synap",
375
+ "-d",
376
+ "nango",
377
+ "-c",
378
+ `UPDATE nango._nango_users SET email_verified = true WHERE email = '${email}';`
379
+ ], { timeout: 1e4 }).catch(() => {
380
+ });
381
+ }
382
+ const fetchEnvKey = `
383
+ const signin = await fetch('http://127.0.0.1:3003/api/v1/account/signin', {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({ email: ${JSON.stringify(email)}, password: ${JSON.stringify(pw)} }),
387
+ }).then(r => r.json()).catch(() => null);
388
+ const token = signin?.data?.token;
389
+ if (!token) { process.stdout.write(''); process.exit(0); }
390
+ const env = await fetch('http://127.0.0.1:3003/api/v1/environment', {
391
+ headers: { Authorization: 'Bearer ' + token },
392
+ }).then(r => r.json()).catch(() => null);
393
+ const key = env?.data?.secret_key ?? env?.secret_key ?? '';
394
+ process.stdout.write(key);
395
+ `;
396
+ try {
397
+ const { stdout } = await execFileAsync("docker", ["exec", "eve-arms-nango", "node", "--input-type=module", "-e", fetchEnvKey], { timeout: 15e3 });
398
+ const envKey = stdout.trim();
399
+ if (envKey && envKey.length > 10) {
400
+ printInfo(` Nango environment secret key retrieved: ${envKey.slice(0, 8)}...`);
401
+ return envKey;
402
+ }
403
+ } catch {
404
+ }
405
+ return null;
406
+ }
407
+ async function addNango() {
408
+ if (!await isBrainReady()) {
409
+ printError("Brain is not ready. Install Synap first: `eve add synap`");
410
+ process.exit(1);
411
+ }
412
+ const { randomUUID } = await import("crypto");
413
+ const { readFile, writeFile } = await import("fs/promises");
414
+ const { existsSync: existsSync2 } = await import("fs");
415
+ const { join: pathJoin } = await import("path");
416
+ const secrets = await readEveSecrets(process.cwd()).catch(() => null);
417
+ const existingKey = secrets?.connectors?.nango?.secretKey;
418
+ const uuidV4Re = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
419
+ const secretKey = existingKey && uuidV4Re.test(existingKey) ? existingKey : randomUUID();
420
+ const _autoDetectDeployDir = async () => {
421
+ const container = await findSynapBackendContainer();
422
+ if (!container) return null;
423
+ try {
424
+ const { stdout } = await execFileAsync("docker", [
425
+ "inspect",
426
+ container,
427
+ "--format",
428
+ '{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
429
+ ], { timeout: 4e3 });
430
+ const dir = stdout.trim();
431
+ return dir || null;
432
+ } catch {
433
+ return null;
434
+ }
435
+ };
436
+ const deployDir = process.env.SYNAP_DEPLOY_DIR || (existsSync2("/opt/synap-backend/deploy") ? "/opt/synap-backend/deploy" : null) || await _autoDetectDeployDir();
437
+ let podPublicUrl = "";
438
+ let pgUser = "synap";
439
+ let pgPassword = "synap";
440
+ if (deployDir) {
441
+ const { readFile: rf } = await import("fs/promises");
442
+ const envPath = pathJoin(deployDir, ".env");
443
+ try {
444
+ const envContent = await rf(envPath, "utf8");
445
+ const matchUrl = envContent.match(/^PUBLIC_URL=(.+)$/m);
446
+ if (matchUrl?.[1]) podPublicUrl = matchUrl[1].trim();
447
+ const matchUser = envContent.match(/^POSTGRES_USER=(.+)$/m);
448
+ if (matchUser?.[1]) pgUser = matchUser[1].trim();
449
+ const matchPass = envContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
450
+ if (matchPass?.[1]) pgPassword = matchPass[1].trim();
451
+ } catch {
452
+ }
453
+ }
454
+ const postgresContainer = await findSynapPostgresContainer();
455
+ if (!postgresContainer) {
456
+ printWarning(" Could not find synap-backend postgres container \u2014 skipping database creation.");
457
+ } else {
458
+ printInfo(`Creating Nango database in ${postgresContainer}...`);
459
+ try {
460
+ await execFileAsync("docker", [
461
+ "exec",
462
+ postgresContainer,
463
+ "psql",
464
+ "-U",
465
+ pgUser,
466
+ "-c",
467
+ "CREATE DATABASE nango;"
468
+ ], { timeout: 1e4 });
469
+ } catch {
470
+ }
471
+ try {
472
+ await execFileAsync("docker", [
473
+ "network",
474
+ "connect",
475
+ "--alias",
476
+ "eve-brain-postgres",
477
+ "eve-network",
478
+ postgresContainer
479
+ ], { timeout: 1e4 });
480
+ printInfo(` Connected ${postgresContainer} to eve-network as eve-brain-postgres.`);
481
+ } catch {
482
+ }
483
+ }
484
+ printInfo("Pulling nangohq/nango-server:hosted...");
485
+ await execFileAsync("docker", ["pull", "nangohq/nango-server:hosted"], { timeout: 12e4 });
486
+ const { stdout: runningOut } = await execFileAsync(
487
+ "docker",
488
+ ["ps", "--filter", "name=eve-arms-nango", "--format", "{{.Names}}"],
489
+ { timeout: 4e3 }
490
+ ).catch(() => ({ stdout: "" }));
491
+ const alreadyRunning = runningOut.trim().split("\n").includes("eve-arms-nango");
492
+ const _nangoDomain = secrets?.domain?.primary;
493
+ const _nangoSsl = secrets?.domain?.ssl !== false;
494
+ const nangoHost = _nangoDomain ? `${_nangoSsl ? "https" : "http"}://nango.${_nangoDomain}` : "http://eve-arms-nango:3003";
495
+ const connectUrl = _nangoDomain ? `${_nangoSsl ? "https" : "http"}://connect.${_nangoDomain}` : "";
496
+ if (!alreadyRunning) {
497
+ await execFileAsync("docker", ["rm", "-f", "eve-arms-nango"], { timeout: 1e4 }).catch(() => {
498
+ });
499
+ const { createHash } = await import("crypto");
500
+ const nangoEncryptionKey = createHash("sha256").update(secretKey).digest("base64");
501
+ const dockerRunArgs = [
502
+ "run",
503
+ "-d",
504
+ "--name",
505
+ "eve-arms-nango",
506
+ "--network",
507
+ "eve-network",
508
+ "--restart",
509
+ "unless-stopped",
510
+ "-e",
511
+ `NANGO_SECRET_KEY=${secretKey}`,
512
+ // Self-hosted Nango authenticates Bearer tokens via NANGO_SECRET_KEY_<envname> env vars,
513
+ // NOT via the api_secrets DB table. Setting these makes our secretKey the API key for
514
+ // both the prod and dev environments created during account signup.
515
+ "-e",
516
+ `NANGO_SECRET_KEY_PROD=${secretKey}`,
517
+ "-e",
518
+ `NANGO_SECRET_KEY_DEV=${secretKey}`,
519
+ // Required by @nangohq/keystore for AES encryption of session tokens — throws without it.
520
+ "-e",
521
+ `NANGO_ENCRYPTION_KEY=${nangoEncryptionKey}`,
522
+ "-e",
523
+ "SERVER_PORT=3003",
524
+ "-e",
525
+ `NANGO_DATABASE_URL=postgresql://${pgUser}:${pgPassword}@eve-brain-postgres:5432/nango`,
526
+ "-e",
527
+ "NODE_ENV=production",
528
+ "-e",
529
+ "NANGO_EMAIL_ACCOUNT_VERIFICATION_REQUIRED=false",
530
+ "-e",
531
+ "FLAG_SERVE_CONNECT_UI=true",
532
+ "-e",
533
+ "NANGO_CONNECT_UI_PORT=3009",
534
+ ...nangoHost ? ["-e", `NANGO_SERVER_URL=${nangoHost}`] : [],
535
+ // NANGO_PUBLIC_CONNECT_URL: read by Nango server to populate env.js connectUrl field
536
+ // NANGO_CONNECT_URL: read by our NangoConnector to build redirect URLs for the pod backend
537
+ ...connectUrl ? ["-e", `NANGO_PUBLIC_CONNECT_URL=${connectUrl}`, "-e", `NANGO_CONNECT_URL=${connectUrl}`] : [],
538
+ ...podPublicUrl ? ["-e", `NANGO_WEBHOOK_URL=${podPublicUrl}/api/connectors/nango-webhook`] : [],
539
+ "-v",
540
+ "eve-arms-nango-data:/var/lib/nango",
541
+ "nangohq/nango-server:hosted"
542
+ ];
543
+ printInfo("Starting Nango container...");
544
+ await execFileAsync("docker", dockerRunArgs, { timeout: 3e4 });
545
+ await applyNangoConnectUiPolyfill(dockerRunArgs);
546
+ } else {
547
+ printInfo("Nango container already running.");
548
+ printInfo(" View admin credentials: eve connectors admin");
549
+ printInfo(" Update admin email: eve connectors admin --set-email <email>");
550
+ printInfo(" Change admin password: eve connectors admin --reset-password");
551
+ }
552
+ let ownerEmail;
553
+ if (!alreadyRunning) {
554
+ ownerEmail = secrets?.synap?.userSession?.email ?? secrets?.builder?.openwebui?.adminEmail;
555
+ if (!ownerEmail) {
556
+ console.log();
557
+ ownerEmail = await text({
558
+ message: "Nango admin email (used to sign in to the dashboard)",
559
+ placeholder: "you@example.com",
560
+ validate: (v) => v && v.includes("@") ? void 0 : "Enter a valid email address"
561
+ });
562
+ if (isCancel(ownerEmail)) {
563
+ cancel("Setup cancelled.");
564
+ process.exit(0);
565
+ }
566
+ }
567
+ }
568
+ let effectiveSecretKey = secretKey;
569
+ if (!alreadyRunning) {
570
+ const nangoEnvKey = await nangoAutoSignup(secretKey, ownerEmail);
571
+ if (nangoEnvKey) effectiveSecretKey = nangoEnvKey;
572
+ if (nangoEnvKey && nangoEnvKey !== secretKey) {
573
+ printInfo(` Using Nango environment key for backend API calls: ${nangoEnvKey.slice(0, 8)}...`);
574
+ }
575
+ }
576
+ if (!podPublicUrl) {
577
+ printWarning(" PUBLIC_URL not found in deploy/.env \u2014 NANGO_WEBHOOK_URL not set.");
578
+ printWarning(" After setting PUBLIC_URL, re-run: eve add nango");
579
+ }
580
+ await writeEveSecrets({
581
+ ...secrets ?? {},
582
+ connectors: {
583
+ ...secrets?.connectors ?? {},
584
+ nango: {
585
+ secretKey: effectiveSecretKey,
586
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
587
+ oauthApps: secrets?.connectors?.nango?.oauthApps ?? {}
588
+ }
589
+ }
590
+ }, process.cwd());
591
+ if (deployDir) {
592
+ const envPath = pathJoin(deployDir, ".env");
593
+ let envContent = "";
594
+ try {
595
+ envContent = await readFile(envPath, "utf8");
596
+ } catch {
597
+ }
598
+ const setEnvVar = (content, key, value) => {
599
+ const re = new RegExp(`^${key}=.*$`, "m");
600
+ const line = `${key}=${value}`;
601
+ return re.test(content) ? content.replace(re, line) : `${content}
602
+ ${line}`;
603
+ };
604
+ envContent = setEnvVar(envContent, "NANGO_HOST", "http://eve-arms-nango:3003");
605
+ envContent = setEnvVar(envContent, "NANGO_SECRET_KEY", effectiveSecretKey);
606
+ if (connectUrl) envContent = setEnvVar(envContent, "NANGO_CONNECT_URL", connectUrl);
607
+ await writeFile(envPath, envContent.trimStart(), "utf8");
608
+ printInfo(` Wrote NANGO_HOST=${nangoHost} + NANGO_SECRET_KEY${connectUrl ? ` + NANGO_CONNECT_URL=${connectUrl}` : ""} to ${envPath}`);
609
+ const backendContainer = await findSynapBackendContainer();
610
+ if (backendContainer) {
611
+ printInfo(` Recreating ${backendContainer} to apply NANGO_SECRET_KEY...`);
612
+ const { stdout: svcOut } = await execFileAsync("docker", [
613
+ "inspect",
614
+ backendContainer,
615
+ "--format",
616
+ '{{index .Config.Labels "com.docker.compose.service"}}'
617
+ ], { timeout: 4e3 }).catch(() => ({ stdout: "backend" }));
618
+ const serviceName = svcOut.trim() || "backend";
619
+ await execFileAsync("docker", [
620
+ "compose",
621
+ "--project-directory",
622
+ deployDir,
623
+ "up",
624
+ "-d",
625
+ "--no-deps",
626
+ "--force-recreate",
627
+ serviceName
628
+ ], { timeout: 12e4 });
629
+ printInfo(" Backend recreated with updated env.");
630
+ } else {
631
+ printWarning(" Could not find synap-backend container \u2014 run `docker compose up -d --force-recreate backend` in your deploy dir to apply NANGO_SECRET_KEY.");
632
+ }
633
+ } else {
634
+ printWarning(" Could not locate deploy/.env \u2014 set SYNAP_DEPLOY_DIR and rerun to write env vars.");
635
+ printInfo(` Add manually: NANGO_HOST=http://eve-arms-nango:3003 NANGO_SECRET_KEY=${secretKey}`);
636
+ }
637
+ await materializeTargets(null, ["traefik-routes"]);
638
+ if (!alreadyRunning) {
639
+ const adminPw = `Nango_${secretKey.slice(0, 12)}`;
640
+ printSuccess("Nango installed.");
641
+ printInfo(` Dashboard: ${nangoHost}`);
642
+ printInfo(` Admin email: ${ownerEmail ?? "admin@eve.local"}`);
643
+ printInfo(` Admin password: ${adminPw}`);
644
+ printInfo(' Email verification is bypassed \u2014 sign in directly, do not use the "sign up" link.');
645
+ } else {
646
+ printSuccess("Nango is running.");
647
+ printInfo(` Dashboard: ${nangoHost}`);
648
+ printInfo(" Manage your account: eve connectors admin");
649
+ }
650
+ console.log();
651
+ printInfo(" Next: register an OAuth app so users can connect their accounts:");
652
+ printInfo(" eve connectors setup google # or github, notion, slack, linear");
653
+ printInfo(' Until a provider is set up, `synap connect <provider>` returns "Integration does not exist".');
654
+ }
655
+ async function addRsshub() {
656
+ if (!await isBrainReady()) {
657
+ printError("Brain is not ready. Please install Synap first: `eve add synap`");
658
+ process.exit(1);
659
+ }
660
+ const { RSSHubService } = await import("./dist-2VRFSXMO.js");
661
+ const rsshub = new RSSHubService();
662
+ if (await rsshub.isInstalled()) {
663
+ printInfo("RSSHub is already installed. Use `eve eyes:start` to start it.");
664
+ return;
665
+ }
666
+ await rsshub.install({ port: 1200 });
667
+ await entityStateManager.updateOrgan("eyes", "ready");
668
+ printSuccess("RSSHub installed successfully!");
669
+ printInfo(" URL: http://localhost:1200");
670
+ }
671
+ async function runAdd(componentId, opts = {}) {
672
+ const comp = resolveComponent(componentId);
673
+ if (componentId === "synap" && await reportExistingSynapDeployment()) {
674
+ return;
675
+ }
676
+ const existing = await entityStateManager.isComponentInstalled(componentId);
677
+ if (existing) {
678
+ printWarning(`${comp.label} is already installed.`);
679
+ const statusCmd = comp.organ ? `eve ${comp.organ} status` : "eve status";
680
+ printInfo(` Run "${statusCmd}" to check its state.`);
681
+ const containerName = comp.service?.containerName;
682
+ if (containerName && !await containerExists(containerName)) {
683
+ printWarning(` \u2026but the ${containerName} container is missing. Reinstalling...`);
684
+ await entityStateManager.updateComponentEntry(componentId, { state: "error" });
685
+ } else {
686
+ printInfo(` Or "eve update ${componentId}" to pull the latest image.`);
687
+ return;
688
+ }
689
+ }
690
+ const currentComponents = await entityStateManager.getInstalledComponents();
691
+ const missingDeps = (comp.requires ?? []).filter((dep) => !currentComponents.includes(dep));
692
+ if (missingDeps.length > 0) {
693
+ const depNames = missingDeps.map((dep) => {
694
+ const info = COMPONENTS.find((c) => c.id === dep);
695
+ return info ? info.label : dep;
696
+ });
697
+ printError(`Missing prerequisites: ${depNames.join(", ")}`);
698
+ printInfo(` Install them first: ${missingDeps.map((d) => `eve add ${d}`).join(" / ")}`);
699
+ process.exit(1);
700
+ }
701
+ if (opts.synapRepo) {
702
+ process.env.SYNAP_REPO_ROOT = opts.synapRepo;
703
+ }
704
+ printHeader(`Adding ${comp.label}`, comp.emoji);
705
+ console.log();
706
+ printInfo(comp.description.split("\n")[0]);
707
+ console.log();
708
+ let step;
709
+ try {
710
+ step = buildAddStep(comp.id, opts);
711
+ } catch (err) {
712
+ printError(String(err));
713
+ process.exit(1);
714
+ }
715
+ const spinner = createSpinner(step.label);
716
+ spinner.start();
717
+ try {
718
+ await step.fn();
719
+ spinner.succeed(step.label);
720
+ } catch (err) {
721
+ spinner.fail(step.label);
722
+ printError(`Failed to add ${comp.label}: ${err instanceof Error ? err.message : String(err)}`);
723
+ process.exit(1);
724
+ }
725
+ const verifySpinner = createSpinner(`Verifying ${comp.label} is reachable...`);
726
+ verifySpinner.start();
727
+ const verification = await verifyComponent(comp.id);
728
+ if (verification.ok) {
729
+ verifySpinner.succeed(verification.summary);
730
+ } else {
731
+ verifySpinner.warn(verification.summary);
732
+ for (const c of verification.checks) {
733
+ if (!c.ok && c.detail) {
734
+ printWarning(` \u2022 ${c.name}: ${c.detail}`);
735
+ }
736
+ }
737
+ printInfo(` Component installed but not yet responding. Check logs: docker logs ${comp.id}`);
738
+ }
739
+ await updateStateAfterAdd(comp.id, verification.ok ? "ready" : "error");
740
+ const [refresh] = await materializeTargets(null, ["traefik-routes"]);
741
+ if (refresh?.changed) {
742
+ printInfo(refresh.summary);
743
+ } else if (refresh && !refresh.ok) {
744
+ printWarning(`Could not refresh Traefik routes: ${refresh.error ?? refresh.summary}`);
745
+ }
746
+ console.log();
747
+ printSuccess(`${comp.label} added successfully!`);
748
+ console.log();
749
+ printInfo("Next steps:");
750
+ printInfo(` - Run "eve status" to check entity state`);
751
+ if (comp.organ) printInfo(` - Run "eve ${comp.organ} status" for ${comp.label} status`);
752
+ if (refresh?.changed) printInfo(` - Run "eve domain check" to verify routing`);
753
+ console.log();
754
+ }
755
+ function buildAddStep(componentId, opts) {
756
+ const model = opts.model || "llama3.1:8b";
757
+ switch (componentId) {
758
+ case "traefik":
759
+ return {
760
+ label: "Setting up Traefik routing...",
761
+ fn: addTraefik
762
+ };
763
+ case "synap":
764
+ return {
765
+ label: "Installing Synap Data Pod...",
766
+ fn: addSynap
767
+ };
768
+ case "ollama":
769
+ return {
770
+ label: "Setting up Ollama + AI gateway...",
771
+ fn: () => addOllama(model)
772
+ };
773
+ case "openclaw":
774
+ return {
775
+ label: "Installing OpenClaw...",
776
+ fn: addOpenclaw
777
+ };
778
+ case "nango":
779
+ return {
780
+ label: "Installing Nango (self-hosted OAuth platform)...",
781
+ fn: addNango
782
+ };
783
+ case "rsshub":
784
+ return {
785
+ label: "Installing RSSHub...",
786
+ fn: addRsshub
787
+ };
788
+ case "openwebui": {
789
+ return {
790
+ label: "Installing Open WebUI...",
791
+ async fn() {
792
+ const { mkdirSync, writeFileSync, existsSync: existsSync2 } = await import("fs");
793
+ const { join: pathJoin } = await import("path");
794
+ const { readAgentKeyOrLegacy, readEveSecrets: readEveSecrets2 } = await import("./dist-C2QB4IKT.js");
795
+ const { randomBytes } = await import("crypto");
796
+ const { execa: execa2 } = await import("execa");
797
+ const deployDir = "/opt/openwebui";
798
+ mkdirSync(deployDir, { recursive: true });
799
+ const secrets = await readEveSecrets2(process.cwd());
800
+ const synapApiKey = await readAgentKeyOrLegacy("openwebui-pipelines", process.cwd()) || process.env.SYNAP_API_KEY || "";
801
+ const isUrl = process.env.SYNAP_IS_URL ?? "http://intelligence-hub:3001";
802
+ const composeYaml = `# Open WebUI \u2014 generated by Eve CLI
803
+ # Self-contained compose. Joins eve-network so Traefik can route chat.<domain>
804
+ # to this container. Uses SQLite by default (no external DB).
805
+
806
+ services:
807
+ openwebui:
808
+ image: ghcr.io/open-webui/open-webui:main
809
+ container_name: hestia-openwebui
810
+ restart: unless-stopped
811
+ environment:
812
+ - ENV=production
813
+ - WEBUI_SECRET_KEY=\${WEBUI_SECRET_KEY:-change-me}
814
+ - SCARF_NO_ANALYTICS=true
815
+ - DO_NOT_TRACK=true
816
+ # Synap IS as the OpenAI-compat backend
817
+ - ENABLE_OPENAI_API=true
818
+ - OPENAI_API_BASE_URLS=\${OPENAI_API_BASE_URLS:-http://eve-brain-synap:4000/v1}
819
+ - OPENAI_API_KEYS=\${OPENAI_API_KEYS:-}
820
+ # Local Ollama as fallback
821
+ - OLLAMA_BASE_URL=\${OLLAMA_BASE_URL:-http://eve-brain-ollama:11434}
822
+ # Features
823
+ - ENABLE_RAG=true
824
+ - ENABLE_WEB_SEARCH=true
825
+ - WEB_SEARCH_ENGINE=duckduckgo
826
+ - ENABLE_SIGNUP=\${ENABLE_SIGNUP:-true}
827
+ - DEFAULT_USER_ROLE=\${DEFAULT_USER_ROLE:-user}
828
+ ports:
829
+ - "3011:8080"
830
+ volumes:
831
+ - openwebui-data:/app/backend/data
832
+ networks:
833
+ - eve-network
834
+
835
+ networks:
836
+ eve-network:
837
+ external: true
838
+
839
+ volumes:
840
+ openwebui-data:
841
+ `;
842
+ writeFileSync(pathJoin(deployDir, "docker-compose.yml"), composeYaml);
843
+ const envPath = pathJoin(deployDir, ".env");
844
+ if (!existsSync2(envPath)) {
845
+ writeFileSync(envPath, [
846
+ "# Open WebUI \u2014 generated by Eve CLI",
847
+ `SYNAP_API_KEY=${synapApiKey}`,
848
+ `SYNAP_IS_URL=${isUrl}`,
849
+ `WEBUI_SECRET_KEY=${randomBytes(32).toString("hex")}`,
850
+ `OLLAMA_BASE_URL=http://eve-brain-ollama:11434`,
851
+ `ENABLE_SIGNUP=true`,
852
+ `DEFAULT_USER_ROLE=user`
853
+ ].join("\n"), { mode: 384 });
854
+ }
855
+ try {
856
+ await execa2("docker", ["network", "inspect", "eve-network"], { stdio: "ignore" });
857
+ } catch {
858
+ await execa2("docker", ["network", "create", "eve-network"], { stdio: "inherit" });
859
+ }
860
+ console.log(` Config: ${deployDir}/docker-compose.yml`);
861
+ await execa2("docker", ["compose", "up", "-d"], {
862
+ cwd: deployDir,
863
+ stdio: "inherit"
864
+ });
865
+ }
866
+ };
867
+ }
868
+ case "openwebui-pipelines":
869
+ return {
870
+ label: "Installing Open WebUI Pipelines sidecar...",
871
+ async fn() {
872
+ const { runActionToCompletion } = await import("./dist-GN2DN7CI.js");
873
+ const result = await runActionToCompletion("openwebui-pipelines", "install");
874
+ if (!result.ok) {
875
+ throw new Error(result.error ?? "Pipelines install failed");
876
+ }
877
+ for (const line of result.logs) console.log(" " + line);
878
+ }
879
+ };
880
+ case "eve-dashboard":
881
+ return {
882
+ label: "Installing Eve Dashboard...",
883
+ async fn() {
884
+ const { randomBytes } = await import("crypto");
885
+ let secrets = await readEveSecrets(process.cwd());
886
+ let secret = secrets?.dashboard?.secret;
887
+ if (!secret) {
888
+ secret = randomBytes(32).toString("hex");
889
+ await writeEveSecrets({ dashboard: { secret, port: 7979 } });
890
+ console.log();
891
+ console.log(colors.primary.bold("Dashboard key generated \u2014 save this somewhere safe:"));
892
+ console.log(colors.muted("\u2500".repeat(66)));
893
+ console.log(colors.primary.bold(secret));
894
+ console.log(colors.muted("\u2500".repeat(66)));
895
+ } else {
896
+ console.log();
897
+ console.log(colors.muted("Reusing existing dashboard key."));
898
+ }
899
+ installDashboardContainer({
900
+ workspaceRoot: process.cwd(),
901
+ secret
902
+ });
903
+ }
904
+ };
905
+ // Thin delegate to the ONE install recipe in @eve/lifecycle — deliberately
906
+ // NOT a second inline compose string here. `openwebui` has exactly that
907
+ // (a different YAML in this file from the one lifecycle writes), and the
908
+ // two have drifted; adding a third copy is how that becomes permanent.
909
+ case "freellmapi":
910
+ return {
911
+ label: "Installing FreeLLMAPI gateway\u2026",
912
+ async fn() {
913
+ const { runActionToCompletion } = await import("./dist-GN2DN7CI.js");
914
+ const result = await runActionToCompletion("freellmapi", "install");
915
+ if (!result.ok) throw new Error(result.error ?? "FreeLLMAPI install failed");
916
+ for (const line of result.logs) console.log(" " + line);
917
+ }
918
+ };
919
+ case "hermes":
920
+ return {
921
+ label: "Installing Hermes AI agent\u2026",
922
+ async fn() {
923
+ const { runActionToCompletion } = await import("./dist-GN2DN7CI.js");
924
+ const result = await runActionToCompletion("hermes", "install");
925
+ if (!result.ok) throw new Error(result.error ?? "Hermes install failed");
926
+ for (const line of result.logs) console.log(" " + line);
927
+ }
928
+ };
929
+ case "dokploy":
930
+ case "opencode":
931
+ case "openclaude":
932
+ return {
933
+ label: `Installing ${COMPONENTS.find((c) => c.id === componentId)?.label ?? componentId}\u2026`,
934
+ async fn() {
935
+ const { runActionToCompletion } = await import("./dist-GN2DN7CI.js");
936
+ const result = await runActionToCompletion(componentId, "install");
937
+ if (!result.ok) throw new Error(result.error ?? "Install failed");
938
+ for (const line of result.logs) console.log(" " + line);
939
+ }
940
+ };
941
+ case "t3code":
942
+ return {
943
+ label: "Installing T3 Code server\u2026",
944
+ async fn() {
945
+ const { select: clackSelect, text: clackInput, password } = await import("@clack/prompts");
946
+ const secrets = await readEveSecrets(process.cwd()).catch(() => null);
947
+ const existing = secrets?.builder?.t3code ?? {};
948
+ const currentBaseUrl = existing.openaiBaseUrl ?? "";
949
+ const podApiUrl = secrets?.synap?.apiUrl ? `${secrets.synap.apiUrl}/v1` : "";
950
+ const isPodIs = currentBaseUrl && podApiUrl && currentBaseUrl === podApiUrl;
951
+ const currentProvider = isPodIs ? "pod" : currentBaseUrl.includes("openrouter") ? "openrouter" : currentBaseUrl ? "custom" : podApiUrl ? "pod" : "openai";
952
+ const provider = await clackSelect({
953
+ message: "AI provider for Codex (T3 Code uses the OpenAI-compatible API)",
954
+ options: [
955
+ ...podApiUrl ? [{ value: "pod", label: "Pod IS", hint: `${podApiUrl} \u2014 already configured` }] : [],
956
+ { value: "openai", label: "OpenAI", hint: "api.openai.com \u2014 official" },
957
+ { value: "openrouter", label: "OpenRouter", hint: "openrouter.ai \u2014 200+ models, unified key" },
958
+ { value: "custom", label: "Custom", hint: "Any OpenAI-compatible base URL" }
959
+ ],
960
+ initialValue: currentProvider
961
+ });
962
+ if (typeof provider !== "string") throw new Error("Cancelled");
963
+ let openaiBaseUrl = "";
964
+ if (provider === "pod") {
965
+ openaiBaseUrl = podApiUrl;
966
+ } else if (provider === "openrouter") {
967
+ openaiBaseUrl = "https://openrouter.ai/api/v1";
968
+ } else if (provider === "custom") {
969
+ const url = await clackInput({
970
+ message: "Base URL (e.g. https://my-proxy.example.com/v1)",
971
+ initialValue: currentBaseUrl,
972
+ validate: (v) => v.trim() ? void 0 : "URL is required"
973
+ });
974
+ if (typeof url !== "string") throw new Error("Cancelled");
975
+ openaiBaseUrl = url.trim();
976
+ }
977
+ const podKey = secrets?.synap?.agentApiKey ?? "";
978
+ const existingKey = existing.openaiApiKey ?? process.env.OPENAI_API_KEY ?? "";
979
+ let openaiApiKey = provider === "pod" ? podKey : existingKey;
980
+ if (!openaiApiKey) {
981
+ const keyLabel = provider === "openrouter" ? "OpenRouter API key" : "API key";
982
+ const val = await password({ message: keyLabel });
983
+ if (typeof val !== "string" || !val.trim()) throw new Error("API key is required");
984
+ openaiApiKey = val.trim();
985
+ }
986
+ await writeEveSecrets({
987
+ builder: { t3code: { ...existing, openaiApiKey, openaiBaseUrl: openaiBaseUrl || void 0 } }
988
+ });
989
+ const { runActionToCompletion } = await import("./dist-GN2DN7CI.js");
990
+ const result = await runActionToCompletion("t3code", "install");
991
+ if (!result.ok) throw new Error(result.error ?? "T3 Code install failed");
992
+ for (const line of result.logs) console.log(" " + line);
993
+ }
994
+ };
995
+ default:
996
+ throw new Error(`No add handler for component: ${componentId}`);
997
+ }
998
+ }
999
+ async function updateStateAfterAdd(componentId, finalState = "ready") {
1000
+ const organMap = {
1001
+ synap: "brain",
1002
+ ollama: "brain",
1003
+ openclaw: "arms",
1004
+ hermes: "arms",
1005
+ // Hermes is the primary agent, same organ as OpenClaw
1006
+ rsshub: "eyes",
1007
+ traefik: "legs",
1008
+ openwebui: "eyes",
1009
+ "openwebui-pipelines": "eyes",
1010
+ dokploy: "builder",
1011
+ opencode: "builder",
1012
+ openclaude: "builder",
1013
+ t3code: "builder",
1014
+ "eve-dashboard": "legs"
1015
+ };
1016
+ const organ = organMap[componentId];
1017
+ if (organ) {
1018
+ await entityStateManager.updateOrgan(organ, finalState, { version: "0.1.0" });
1019
+ }
1020
+ await entityStateManager.updateComponentEntry(componentId, {
1021
+ state: finalState,
1022
+ version: "0.1.0",
1023
+ managedBy: "eve"
1024
+ });
1025
+ const current = await entityStateManager.getInstalledComponents();
1026
+ if (!current.includes(componentId)) {
1027
+ await entityStateManager.updateSetupProfile({ components: [...current, componentId] });
1028
+ }
1029
+ }
1030
+ var BUILDER_OPTIONS = [
1031
+ {
1032
+ id: "opencode",
1033
+ label: "OpenCode",
1034
+ hint: "AI-powered code editor running on your server"
1035
+ },
1036
+ {
1037
+ id: "openclaude",
1038
+ label: "OpenClaude",
1039
+ hint: "Claude Code as a service \u2014 delegate hard coding tasks to Claude"
1040
+ },
1041
+ {
1042
+ id: "dokploy",
1043
+ label: "Dokploy",
1044
+ hint: "Visual PaaS for deploying apps (like a self-hosted Railway)"
1045
+ },
1046
+ {
1047
+ id: "t3code",
1048
+ label: "T3 Code",
1049
+ hint: "External code execution backend \u2014 store connection details for DevPlane pipeline"
1050
+ }
1051
+ ];
1052
+ async function pickBuilder() {
1053
+ console.log();
1054
+ const choice = await select({
1055
+ message: "Which builder would you like to install?",
1056
+ options: BUILDER_OPTIONS.map((b) => ({
1057
+ value: b.id,
1058
+ label: b.label,
1059
+ hint: b.hint
1060
+ }))
1061
+ });
1062
+ if (isCancel(choice)) {
1063
+ printInfo("Cancelled.");
1064
+ return null;
1065
+ }
1066
+ return choice;
1067
+ }
1068
+ function addCommand(program) {
1069
+ program.command("add").description("Add a component to an existing entity").argument("[components...]", "One component ID or category to add (hermes, synap, ollama, openclaw, rsshub, openwebui, builder, opencode, openclaude, dokploy, \u2026)").option("--synap-repo <path>", "Path to synap-backend checkout (for synap component)").option("--model <model>", "Ollama model (for ollama component)", "llama3.1:8b").action(async (components, opts) => {
1070
+ const requested = components ?? [];
1071
+ if (requested.length > 1) {
1072
+ if (requested.includes("synap") && requested.includes("pod-admin")) {
1073
+ printInfo("Pod Admin ships with the Synap Data Pod; it is not a separately added component.");
1074
+ printInfo("Use `eve add synap` only for a new Pod, or `eve update synap` to recover an existing one.");
1075
+ } else {
1076
+ printWarning("`eve add` installs one component at a time.");
1077
+ printInfo(`Received: ${requested.join(", ")}`);
1078
+ }
1079
+ process.exitCode = 1;
1080
+ return;
1081
+ }
1082
+ let component = requested[0];
1083
+ if (component === "builder") {
1084
+ const picked = await pickBuilder();
1085
+ if (!picked) process.exit(0);
1086
+ component = picked;
1087
+ }
1088
+ if (!component) {
1089
+ console.log();
1090
+ printHeader("Eve \u2014 Add Component", emojis.entity);
1091
+ console.log();
1092
+ printInfo("Usage: eve add <component>");
1093
+ console.log();
1094
+ const groups = [
1095
+ { heading: "AI agents", ids: ["hermes", "openclaw"] },
1096
+ { heading: "Data & inference", ids: ["synap", "ollama", "openwebui", "openwebui-pipelines", "rsshub"] },
1097
+ { heading: "Builders", ids: ["opencode", "openclaude", "dokploy", "t3code"] },
1098
+ { heading: "Infrastructure", ids: ["traefik", "eve-dashboard"] }
1099
+ ];
1100
+ for (const group of groups) {
1101
+ console.log(colors.muted.bold(`
1102
+ ${group.heading}`));
1103
+ for (const id of group.ids) {
1104
+ const comp = COMPONENTS.find((c) => c.id === id);
1105
+ if (!comp) continue;
1106
+ const installed = await entityStateManager.isComponentInstalled(id);
1107
+ const tag = installed ? colors.success(" [installed]") : comp.deprecated ? colors.muted(" [deprecated]") : "";
1108
+ console.log(` ${comp.emoji} ${colors.primary.bold(comp.label)}${tag}`);
1109
+ console.log(` ${colors.muted(comp.description.split("\n")[0])}`);
1110
+ }
1111
+ }
1112
+ console.log();
1113
+ printInfo("Tip: `eve add builder` shows a picker for OpenCode / OpenClaude / Dokploy");
1114
+ printInfo("Examples:");
1115
+ printInfo(" eve add hermes # AI agent with sovereign memory");
1116
+ printInfo(" eve add ollama # Local AI inference");
1117
+ printInfo(" eve add builder # Pick a code-execution builder");
1118
+ console.log();
1119
+ return;
1120
+ }
1121
+ await runAdd(component, opts);
1122
+ });
1123
+ }
1124
+
1125
+ export {
1126
+ colors,
1127
+ emojis,
1128
+ createSpinner,
1129
+ printHeader,
1130
+ printSuccess,
1131
+ printError,
1132
+ printWarning,
1133
+ printInfo,
1134
+ printKeyValue,
1135
+ formatOrgan,
1136
+ printBox,
1137
+ runAdd,
1138
+ addCommand
1139
+ };
1140
+ //# sourceMappingURL=chunk-MUEQNBE6.js.map