@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,1773 @@
1
+ import { createRequire as __eveCreateRequire } from 'module'; const require = __eveCreateRequire(import.meta.url);
2
+ import {
3
+ entityStateManager
4
+ } from "./chunk-5TBV2VHZ.js";
5
+ import {
6
+ COMPONENTS,
7
+ resolveComponent,
8
+ validateBaseDomain
9
+ } from "./chunk-O5WTCVMZ.js";
10
+ import {
11
+ readEveSecrets,
12
+ writeEveSecrets
13
+ } from "./chunk-T7GCAJK5.js";
14
+
15
+ // ../@eve/legs/dist/index.js
16
+ import { execSync, spawnSync } from "child_process";
17
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, chmodSync, unlinkSync } from "fs";
18
+ import { join } from "path";
19
+ import { execSync as execSync2 } from "child_process";
20
+ import { writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "fs";
21
+ import { join as join2 } from "path";
22
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
23
+ import { join as join3 } from "path";
24
+ import { execSync as execSync3 } from "child_process";
25
+ import { spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
26
+ import { existsSync as existsSync4 } from "fs";
27
+ import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
28
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
29
+ import { execSync as execSync6, spawnSync as spawnSync4 } from "child_process";
30
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
31
+ import { join as join4 } from "path";
32
+ import { randomBytes } from "crypto";
33
+ import { execFile } from "child_process";
34
+ import { promisify } from "util";
35
+ import { execSync as execSync7, spawnSync as spawnSync5 } from "child_process";
36
+ import { existsSync as existsSync6 } from "fs";
37
+ import { homedir } from "os";
38
+ import { resolve } from "path";
39
+ var HOST_CONFIG_DIR = "/opt/traefik";
40
+ var CONTAINER_DYNAMIC_DIR = "/etc/traefik/dynamic";
41
+ var CONTAINER_ACME_FILE = "/etc/traefik/acme.json";
42
+ function getSynapBackendContainer() {
43
+ return findComposeContainer("backend");
44
+ }
45
+ function getSynapPodAdminContainer() {
46
+ return findComposeContainer("pod-admin");
47
+ }
48
+ function findComposeContainer(service) {
49
+ try {
50
+ const out = execSync(
51
+ `docker ps --filter "label=com.docker.compose.project=synap-backend" --filter "label=com.docker.compose.service=${service}" --format "{{.Names}}"`,
52
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
53
+ ).trim();
54
+ return out.split("\n")[0]?.trim() || null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function connectToEveNetwork(containerName, alias) {
60
+ if (alias) {
61
+ try {
62
+ const inspect = execSync(
63
+ `docker inspect --format "{{json .NetworkSettings.Networks}}" ${containerName}`,
64
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
65
+ ).trim();
66
+ const networks = JSON.parse(inspect);
67
+ const eve = networks["eve-network"];
68
+ if (eve) {
69
+ if ((eve.Aliases ?? []).includes(alias)) return;
70
+ try {
71
+ execSync(`docker network disconnect eve-network ${containerName}`, {
72
+ stdio: ["pipe", "pipe", "ignore"]
73
+ });
74
+ } catch {
75
+ }
76
+ }
77
+ } catch {
78
+ }
79
+ }
80
+ const aliasArg = alias ? ` --alias ${alias}` : "";
81
+ try {
82
+ execSync(`docker network connect${aliasArg} eve-network ${containerName}`, {
83
+ stdio: ["pipe", "pipe", "ignore"]
84
+ });
85
+ } catch {
86
+ }
87
+ }
88
+ var TRUSTED_PROXY_IPS = [
89
+ "10.0.0.0/8",
90
+ "172.16.0.0/12",
91
+ "192.168.0.0/16",
92
+ "127.0.0.1/32"
93
+ ];
94
+ function buildStaticConfig(ssl, email) {
95
+ const acmeSection = ssl && email ? `
96
+ certificatesResolvers:
97
+ letsencrypt:
98
+ acme:
99
+ email: ${email}
100
+ storage: ${CONTAINER_ACME_FILE}
101
+ tlsChallenge: {}
102
+ ` : "";
103
+ return `# Traefik static config \u2014 generated by Eve
104
+ entryPoints:
105
+ web:
106
+ address: ":80"
107
+ websecure:
108
+ address: ":443"
109
+
110
+ providers:
111
+ file:
112
+ directory: ${CONTAINER_DYNAMIC_DIR}
113
+ watch: true
114
+ ${acmeSection}
115
+ api:
116
+ dashboard: true
117
+ insecure: true
118
+
119
+ log:
120
+ level: INFO
121
+
122
+ accessLog: {}
123
+ `.trimStart();
124
+ }
125
+ function buildProxyModeStaticConfig() {
126
+ const trustedIps = TRUSTED_PROXY_IPS.map((ip) => ` - "${ip}"`).join("\n");
127
+ return `# Traefik static config \u2014 generated by Eve (behind-proxy mode)
128
+ # External proxy handles SSL termination. Traefik listens on HTTP only.
129
+ entryPoints:
130
+ web:
131
+ address: ":80"
132
+ forwardedHeaders:
133
+ trustedIPs:
134
+ ${trustedIps}
135
+
136
+ providers:
137
+ file:
138
+ directory: ${CONTAINER_DYNAMIC_DIR}
139
+ watch: true
140
+
141
+ api:
142
+ dashboard: true
143
+ insecure: true
144
+
145
+ log:
146
+ level: INFO
147
+
148
+ accessLog: {}
149
+ `.trimStart();
150
+ }
151
+ function getDockerHostGateway() {
152
+ try {
153
+ const gw = execSync(
154
+ "docker network inspect eve-network --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null",
155
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
156
+ ).trim();
157
+ if (gw) return gw;
158
+ } catch {
159
+ }
160
+ try {
161
+ const gw = execSync(
162
+ "docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null",
163
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
164
+ ).trim();
165
+ if (gw) return gw;
166
+ } catch {
167
+ }
168
+ return "172.17.0.1";
169
+ }
170
+ function allowDockerToHostPorts(ports) {
171
+ try {
172
+ const networks = execSync(
173
+ "docker network ls --format '{{.Name}}' | grep -E 'eve-network|bridge' | while read net; do docker network inspect $net --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 2>/dev/null; done",
174
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
175
+ ).trim().split("\n").filter(Boolean);
176
+ for (const cidr of networks) {
177
+ for (const port of ports) {
178
+ try {
179
+ execSync(`ufw allow from ${cidr} to any port ${port} 2>/dev/null`, {
180
+ stdio: ["pipe", "pipe", "ignore"]
181
+ });
182
+ } catch {
183
+ }
184
+ }
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ var TraefikService = class {
190
+ configDir;
191
+ traefikConfigPath;
192
+ dynamicConfigDir;
193
+ constructor(configDir = HOST_CONFIG_DIR) {
194
+ this.configDir = configDir;
195
+ this.traefikConfigPath = join(configDir, "traefik.yml");
196
+ this.dynamicConfigDir = join(configDir, "dynamic");
197
+ }
198
+ async install() {
199
+ console.log("Installing Traefik...");
200
+ if (!existsSync(this.configDir)) mkdirSync(this.configDir, { recursive: true });
201
+ if (!existsSync(this.dynamicConfigDir)) mkdirSync(this.dynamicConfigDir, { recursive: true });
202
+ const isDokploy = existsSync("/opt/dokploy");
203
+ if (isDokploy) {
204
+ console.log("Detected Dokploy \u2014 using existing Traefik installation");
205
+ await this.configureDokployTraefik();
206
+ } else {
207
+ await this.installStandalone();
208
+ }
209
+ console.log("Traefik installation complete");
210
+ }
211
+ async installStandalone() {
212
+ writeFileSync(this.traefikConfigPath, buildStaticConfig(false));
213
+ console.log("Created Traefik static config");
214
+ try {
215
+ execSync("docker network create eve-network", { stdio: "ignore" });
216
+ } catch {
217
+ }
218
+ const acmePath = join(this.configDir, "acme.json");
219
+ if (!existsSync(acmePath)) {
220
+ writeFileSync(acmePath, "", { mode: 384 });
221
+ } else {
222
+ try {
223
+ chmodSync(acmePath, 384);
224
+ } catch {
225
+ }
226
+ }
227
+ spawnSync("docker", ["rm", "-f", "eve-legs-traefik"], { stdio: "ignore" });
228
+ const result = spawnSync("docker", [
229
+ "run",
230
+ "-d",
231
+ "--name",
232
+ "eve-legs-traefik",
233
+ "--restart",
234
+ "unless-stopped",
235
+ "-p",
236
+ "80:80",
237
+ "-p",
238
+ "443:443",
239
+ "-p",
240
+ "8080:8080",
241
+ "-v",
242
+ "/var/run/docker.sock:/var/run/docker.sock:ro",
243
+ "-v",
244
+ `${this.configDir}/traefik.yml:/etc/traefik/traefik.yml:ro`,
245
+ "-v",
246
+ `${this.dynamicConfigDir}:/etc/traefik/dynamic:ro`,
247
+ "-v",
248
+ `${acmePath}:/etc/traefik/acme.json`,
249
+ "--network",
250
+ "eve-network",
251
+ "traefik:v3.0"
252
+ ], { stdio: "inherit" });
253
+ if (result.status !== 0) throw new Error(`docker run exited with code ${result.status}`);
254
+ console.log("Traefik container started");
255
+ }
256
+ async configureDokployTraefik() {
257
+ console.log("Using Dokploy-managed Traefik");
258
+ }
259
+ async configureSubdomains(domain, ssl, email, installedComponents, behindProxy = false, customRoutes) {
260
+ const synapContainer = getSynapBackendContainer();
261
+ if (synapContainer) {
262
+ connectToEveNetwork(synapContainer, "eve-brain-synap");
263
+ console.log(` Connected ${synapContainer} \u2192 eve-network (alias: eve-brain-synap)`);
264
+ } else if (!installedComponents || installedComponents.includes("synap")) {
265
+ console.warn(" Warning: Synap backend container not found \u2014 pod.domain routing will 502 until `eve brain init` is run");
266
+ }
267
+ const podAdminContainer = getSynapPodAdminContainer();
268
+ if (podAdminContainer) {
269
+ connectToEveNetwork(podAdminContainer, "eve-brain-pod-admin");
270
+ console.log(` Connected ${podAdminContainer} \u2192 eve-network (alias: eve-brain-pod-admin)`);
271
+ }
272
+ let effectiveInstalled = installedComponents;
273
+ if (effectiveInstalled && effectiveInstalled.includes("synap") && !effectiveInstalled.includes("pod-admin")) {
274
+ effectiveInstalled = [...effectiveInstalled, "pod-admin"];
275
+ }
276
+ const services = [];
277
+ const pathRoutes = [];
278
+ for (const comp of COMPONENTS) {
279
+ if (!comp.service || !comp.service.subdomain) continue;
280
+ if (effectiveInstalled && !effectiveInstalled.includes(comp.id)) continue;
281
+ let containerName = comp.service.containerName;
282
+ if (comp.id === "synap" && synapContainer) containerName = synapContainer;
283
+ if (comp.id === "pod-admin" && podAdminContainer) containerName = podAdminContainer;
284
+ services.push({
285
+ id: comp.id,
286
+ subdomain: comp.service.subdomain,
287
+ upstream: `http://${containerName}:${comp.service.internalPort}`,
288
+ requires: comp.id
289
+ });
290
+ for (const extra of comp.service.additionalSubdomains ?? []) {
291
+ services.push({
292
+ id: `${comp.id}-${extra.subdomain}`,
293
+ subdomain: extra.subdomain,
294
+ upstream: `http://${containerName}:${extra.internalPort}`,
295
+ requires: comp.id,
296
+ priority: 1
297
+ });
298
+ for (const proxy of extra.pathProxies ?? []) {
299
+ pathRoutes.push({
300
+ id: `${comp.id}-${extra.subdomain}-${proxy.path.replace(/\//g, "")}`,
301
+ subdomain: extra.subdomain,
302
+ path: proxy.path,
303
+ upstream: `http://${containerName}:${proxy.internalPort}`,
304
+ requires: comp.id
305
+ });
306
+ }
307
+ }
308
+ }
309
+ if (customRoutes && customRoutes.length > 0) {
310
+ allowDockerToHostPorts(customRoutes.map((r) => r.port));
311
+ for (const route of customRoutes) {
312
+ const routeDomain = route.domain || domain;
313
+ const subdomain = route.subdomain;
314
+ const hostGateway = getDockerHostGateway();
315
+ const upstream = `http://${hostGateway}:${route.port}`;
316
+ services.push({
317
+ id: `custom-${subdomain}`,
318
+ subdomain,
319
+ upstream,
320
+ requires: null,
321
+ domain: routeDomain
322
+ });
323
+ console.log(` Added custom route: ${subdomain}.${routeDomain} \u2192 ${upstream}`);
324
+ }
325
+ }
326
+ const routersYaml = services.map((s) => {
327
+ const routeDomain = s.domain || domain;
328
+ const host = `${s.subdomain}.${routeDomain}`;
329
+ if (behindProxy) {
330
+ return ` ${s.id}:
331
+ rule: "Host(\`${host}\`)"
332
+ ` + (s.priority !== void 0 ? ` priority: ${s.priority}
333
+ ` : "") + ` entryPoints:
334
+ - web
335
+ middlewares:
336
+ - forward-https
337
+ service: ${s.id}-svc`;
338
+ }
339
+ if (ssl) {
340
+ return (
341
+ // HTTP → HTTPS redirect router
342
+ ` ${s.id}-http:
343
+ rule: "Host(\`${host}\`)"
344
+ entryPoints:
345
+ - web
346
+ middlewares:
347
+ - redirect-to-https
348
+ service: ${s.id}-svc
349
+ ${s.id}:
350
+ rule: "Host(\`${host}\`)"
351
+ entryPoints:
352
+ - websecure
353
+ tls:
354
+ certResolver: letsencrypt
355
+ service: ${s.id}-svc`
356
+ );
357
+ }
358
+ return ` ${s.id}-http:
359
+ rule: "Host(\`${host}\`)"
360
+ entryPoints:
361
+ - web
362
+ service: ${s.id}-svc
363
+ ${s.id}-https:
364
+ rule: "Host(\`${host}\`)"
365
+ entryPoints:
366
+ - websecure
367
+ tls: {}
368
+ service: ${s.id}-svc`;
369
+ }).join("\n");
370
+ const servicesYaml = services.map(
371
+ (s) => ` ${s.id}-svc:
372
+ loadBalancer:
373
+ ` + (behindProxy ? ` passHostHeader: true
374
+ ` : "") + ` servers:
375
+ - url: "${s.upstream}"`
376
+ ).join("\n");
377
+ const synapRealtimeContainer = findComposeContainer("realtime");
378
+ const synapService = services.find((s) => s.id === "synap");
379
+ let realtimeRouterYaml = "";
380
+ let realtimeServiceYaml = "";
381
+ if (synapService && synapRealtimeContainer) {
382
+ connectToEveNetwork(synapRealtimeContainer);
383
+ const rHost = `${synapService.subdomain}.${domain}`;
384
+ const rRule = `Host(\`${rHost}\`) && (PathPrefix(\`/socket.io/\`) || PathPrefix(\`/realtime/\`))`;
385
+ const rUpstream = `http://${synapRealtimeContainer}:4001`;
386
+ if (behindProxy) {
387
+ realtimeRouterYaml = `
388
+ synap-realtime:
389
+ rule: "${rRule}"
390
+ priority: 100
391
+ entryPoints:
392
+ - web
393
+ middlewares:
394
+ - forward-https
395
+ service: synap-realtime-svc`;
396
+ } else if (ssl) {
397
+ realtimeRouterYaml = `
398
+ synap-realtime-http:
399
+ rule: "${rRule}"
400
+ priority: 100
401
+ entryPoints:
402
+ - web
403
+ middlewares:
404
+ - redirect-to-https
405
+ service: synap-realtime-svc
406
+ synap-realtime:
407
+ rule: "${rRule}"
408
+ priority: 100
409
+ entryPoints:
410
+ - websecure
411
+ tls:
412
+ certResolver: letsencrypt
413
+ service: synap-realtime-svc`;
414
+ } else {
415
+ realtimeRouterYaml = `
416
+ synap-realtime-http:
417
+ rule: "${rRule}"
418
+ priority: 100
419
+ entryPoints:
420
+ - web
421
+ service: synap-realtime-svc
422
+ synap-realtime-https:
423
+ rule: "${rRule}"
424
+ priority: 100
425
+ entryPoints:
426
+ - websecure
427
+ tls: {}
428
+ service: synap-realtime-svc`;
429
+ }
430
+ realtimeServiceYaml = `
431
+ synap-realtime-svc:
432
+ loadBalancer:
433
+ ` + (behindProxy ? ` passHostHeader: true
434
+ ` : "") + ` servers:
435
+ - url: "${rUpstream}"`;
436
+ }
437
+ const middlewaresYaml = behindProxy ? ` middlewares:
438
+ forward-https:
439
+ headers:
440
+ customRequestHeaders:
441
+ X-Forwarded-Proto: "https"
442
+ ` : !ssl ? "" : ` middlewares:
443
+ redirect-to-https:
444
+ redirectScheme:
445
+ scheme: https
446
+ permanent: true
447
+ `;
448
+ const pathRoutesYaml = pathRoutes.length === 0 ? "" : "\n" + pathRoutes.map((p) => {
449
+ const host = `${p.subdomain}.${domain}`;
450
+ if (behindProxy) {
451
+ return ` ${p.id}:
452
+ rule: "Host(\`${host}\`) && Path(\`${p.path}\`)"
453
+ priority: 10
454
+ entryPoints:
455
+ - web
456
+ middlewares:
457
+ - forward-https
458
+ service: ${p.id}-svc`;
459
+ }
460
+ if (ssl) {
461
+ return ` ${p.id}:
462
+ rule: "Host(\`${host}\`) && Path(\`${p.path}\`)"
463
+ priority: 10
464
+ entryPoints:
465
+ - websecure
466
+ tls:
467
+ certResolver: letsencrypt
468
+ service: ${p.id}-svc`;
469
+ }
470
+ return ` ${p.id}:
471
+ rule: "Host(\`${host}\`) && Path(\`${p.path}\`)"
472
+ priority: 10
473
+ entryPoints:
474
+ - web
475
+ service: ${p.id}-svc`;
476
+ }).join("\n");
477
+ const pathServicesYaml = pathRoutes.length === 0 ? "" : "\n" + pathRoutes.map(
478
+ (p) => ` ${p.id}-svc:
479
+ loadBalancer:
480
+ ` + (behindProxy ? ` passHostHeader: true
481
+ ` : "") + ` servers:
482
+ - url: "${p.upstream}"`
483
+ ).join("\n");
484
+ const modeComment = behindProxy ? "behind-proxy" : ssl ? "ssl" : "plain";
485
+ const dynamicConfig = `# Eve subdomain routes \u2014 generated by \`eve domain set\`
486
+ # Domain: ${domain} | Mode: ${modeComment}
487
+ http:
488
+ ${middlewaresYaml} routers:
489
+ ${routersYaml}${realtimeRouterYaml}${pathRoutesYaml}
490
+ services:
491
+ ${servicesYaml}${realtimeServiceYaml}${pathServicesYaml}
492
+ `;
493
+ if (!existsSync(this.configDir)) mkdirSync(this.configDir, { recursive: true });
494
+ writeFileSync(this.traefikConfigPath, behindProxy ? buildProxyModeStaticConfig() : buildStaticConfig(ssl, email));
495
+ if (!existsSync(this.dynamicConfigDir)) mkdirSync(this.dynamicConfigDir, { recursive: true });
496
+ writeFileSync(join(this.dynamicConfigDir, "eve-routes.yml"), dynamicConfig, "utf-8");
497
+ try {
498
+ execSync("docker restart eve-legs-traefik", { stdio: "ignore" });
499
+ console.log(" Traefik restarted");
500
+ } catch {
501
+ console.warn(" Could not restart Traefik automatically \u2014 run: docker restart eve-legs-traefik");
502
+ }
503
+ }
504
+ // ---------------------------------------------------------------------------
505
+ // Proxy mode
506
+ // ---------------------------------------------------------------------------
507
+ /**
508
+ * Detect whether Traefik is currently running in behind-proxy mode by
509
+ * checking whether the static config has no `websecure` entrypoint.
510
+ */
511
+ getProxyModeStatus() {
512
+ try {
513
+ const config = readFileSync(this.traefikConfigPath, "utf-8");
514
+ const enabled = !config.includes("websecure");
515
+ const ssl = config.includes("certificatesResolvers");
516
+ return {
517
+ enabled,
518
+ ssl: !enabled && ssl,
519
+ entrypoints: enabled ? ["web"] : ["web", "websecure"]
520
+ };
521
+ } catch {
522
+ return { enabled: false, ssl: false, entrypoints: [] };
523
+ }
524
+ }
525
+ /**
526
+ * Switch Traefik to behind-proxy mode:
527
+ * - HTTP-only static config (no websecure, no cert resolver, trusted IPs)
528
+ * - Dynamic routes without redirects or TLS
529
+ * - Recreate container without port 443 binding
530
+ */
531
+ async enableProxyMode(domain, installedComponents) {
532
+ if (!existsSync(this.configDir)) mkdirSync(this.configDir, { recursive: true });
533
+ if (!existsSync(this.dynamicConfigDir)) mkdirSync(this.dynamicConfigDir, { recursive: true });
534
+ writeFileSync(this.traefikConfigPath, buildProxyModeStaticConfig());
535
+ const synapContainer = getSynapBackendContainer();
536
+ if (synapContainer) connectToEveNetwork(synapContainer, "eve-brain-synap");
537
+ await this.configureSubdomains(domain, false, void 0, installedComponents, true);
538
+ const acmePath = join(this.configDir, "acme.json");
539
+ if (!existsSync(acmePath)) {
540
+ writeFileSync(acmePath, "", { mode: 384 });
541
+ } else {
542
+ try {
543
+ chmodSync(acmePath, 384);
544
+ } catch {
545
+ }
546
+ }
547
+ this.recreateTraefikContainer(["80:80", "8080:8080"], acmePath);
548
+ }
549
+ /**
550
+ * Restore standalone mode (Traefik handles its own SSL).
551
+ */
552
+ async disableProxyMode(domain, ssl, email, installedComponents) {
553
+ if (!existsSync(this.configDir)) mkdirSync(this.configDir, { recursive: true });
554
+ await this.configureSubdomains(domain, ssl, email, installedComponents, false);
555
+ const acmePath = join(this.configDir, "acme.json");
556
+ if (!existsSync(acmePath)) {
557
+ writeFileSync(acmePath, "", { mode: 384 });
558
+ } else {
559
+ try {
560
+ chmodSync(acmePath, 384);
561
+ } catch {
562
+ }
563
+ }
564
+ this.recreateTraefikContainer(["80:80", "443:443", "8080:8080"], acmePath);
565
+ }
566
+ /** Stop + remove + relaunch the Traefik container with the given port bindings. */
567
+ recreateTraefikContainer(ports, acmePath) {
568
+ spawnSync("docker", ["rm", "-f", "eve-legs-traefik"], { stdio: "ignore" });
569
+ const result = spawnSync("docker", [
570
+ "run",
571
+ "-d",
572
+ "--name",
573
+ "eve-legs-traefik",
574
+ "--restart",
575
+ "unless-stopped",
576
+ ...ports.flatMap((p) => ["-p", p]),
577
+ "-v",
578
+ "/var/run/docker.sock:/var/run/docker.sock:ro",
579
+ "-v",
580
+ `${this.traefikConfigPath}:/etc/traefik/traefik.yml:ro`,
581
+ "-v",
582
+ `${this.dynamicConfigDir}:/etc/traefik/dynamic:ro`,
583
+ "-v",
584
+ `${acmePath}:/etc/traefik/acme.json`,
585
+ "--network",
586
+ "eve-network",
587
+ "traefik:v3.0"
588
+ ], { stdio: "inherit" });
589
+ if (result.status !== 0) throw new Error(`docker run exited with code ${result.status}`);
590
+ }
591
+ // ---------------------------------------------------------------------------
592
+ // Routes
593
+ // ---------------------------------------------------------------------------
594
+ async addRoute(route) {
595
+ const configFile = join(this.dynamicConfigDir, `${route.path.replace(/\//g, "_")}.yml`);
596
+ const routeConfig = `http:
597
+ routers:
598
+ ${route.path.replace(/[^a-zA-Z0-9]/g, "_")}:
599
+ rule: "Host(\`${route.domain || "localhost"}\`) && PathPrefix(\`${route.path}\`)"
600
+ service: ${route.path.replace(/[^a-zA-Z0-9]/g, "_")}
601
+ ` + (route.ssl ? ` tls: {}
602
+ ` : "") + ` services:
603
+ ${route.path.replace(/[^a-zA-Z0-9]/g, "_")}:
604
+ loadBalancer:
605
+ servers:
606
+ - url: "${route.target}"
607
+ `;
608
+ writeFileSync(configFile, routeConfig);
609
+ }
610
+ async removeRoute(path) {
611
+ const configFile = join(this.dynamicConfigDir, `${path.replace(/\//g, "_")}.yml`);
612
+ if (existsSync(configFile)) writeFileSync(configFile, "");
613
+ }
614
+ async configureDomain(domain) {
615
+ const routes = this.getRoutes();
616
+ for (const route of routes) await this.addRoute({ ...route, domain });
617
+ }
618
+ async enableSSL() {
619
+ const routes = this.getRoutes();
620
+ for (const route of routes) {
621
+ if (route.domain) await this.addRoute({ ...route, ssl: true });
622
+ }
623
+ }
624
+ getRoutes() {
625
+ try {
626
+ if (!existsSync(this.dynamicConfigDir)) return [];
627
+ const files = readdirSafe(this.dynamicConfigDir).filter((f) => f.endsWith(".yml") && f !== "eve-routes.yml");
628
+ return files.flatMap((file) => {
629
+ const content = readFileSync(join(this.dynamicConfigDir, file), "utf-8");
630
+ const pathMatch = content.match(/PathPrefix\(`(.+)`\)/);
631
+ const urlMatch = content.match(/url: "(.+)"/);
632
+ if (!pathMatch || !urlMatch) return [];
633
+ return [{
634
+ path: pathMatch[1],
635
+ target: urlMatch[1],
636
+ domain: content.match(/Host\(`(.+?)`\)/)?.[1],
637
+ ssl: content.includes("tls:")
638
+ }];
639
+ });
640
+ } catch {
641
+ return [];
642
+ }
643
+ }
644
+ getStatus() {
645
+ const installed = existsSync(this.traefikConfigPath);
646
+ let running = false;
647
+ try {
648
+ const out = execSync('docker ps --filter "name=eve-legs-traefik" --format "{{.Names}}"', { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
649
+ running = out.length > 0;
650
+ } catch {
651
+ }
652
+ const routes = this.getRoutes();
653
+ const proxyStatus = this.getProxyModeStatus();
654
+ let domain = routes.find((r) => r.domain)?.domain ?? null;
655
+ let ssl = routes.some((r) => r.ssl);
656
+ const eveRoutesPath = join(this.dynamicConfigDir, "eve-routes.yml");
657
+ if (existsSync(eveRoutesPath)) {
658
+ try {
659
+ const content = readFileSync(eveRoutesPath, "utf-8");
660
+ const hostMatch = content.match(/Host\(`([^`]+)`\)/);
661
+ if (hostMatch) {
662
+ const host = hostMatch[1];
663
+ const parts = host.split(".");
664
+ domain = parts.length > 2 ? parts.slice(1).join(".") : host;
665
+ }
666
+ ssl = content.includes("certResolver:");
667
+ } catch {
668
+ }
669
+ }
670
+ return {
671
+ installed,
672
+ running,
673
+ domain,
674
+ ssl,
675
+ behindProxy: proxyStatus.enabled,
676
+ routes
677
+ };
678
+ }
679
+ cleanConfig() {
680
+ if (existsSync(this.dynamicConfigDir)) {
681
+ for (const file of readdirSafe(this.dynamicConfigDir)) {
682
+ if (file.endsWith(".yml") || file.endsWith(".yaml")) {
683
+ try {
684
+ unlinkSync(join(this.dynamicConfigDir, file));
685
+ } catch {
686
+ }
687
+ }
688
+ }
689
+ }
690
+ if (existsSync(this.traefikConfigPath)) {
691
+ try {
692
+ unlinkSync(this.traefikConfigPath);
693
+ } catch {
694
+ }
695
+ }
696
+ }
697
+ };
698
+ function readdirSafe(dir) {
699
+ try {
700
+ return readdirSync(dir);
701
+ } catch {
702
+ return [];
703
+ }
704
+ }
705
+ var TunnelService = class {
706
+ configDir;
707
+ constructor(configDir = "/opt/hestia/tunnels") {
708
+ this.configDir = configDir;
709
+ if (!existsSync2(this.configDir)) {
710
+ mkdirSync2(this.configDir, { recursive: true });
711
+ }
712
+ }
713
+ async setupPangolin(config) {
714
+ console.log("Setting up Pangolin tunnel...");
715
+ try {
716
+ execSync2("which pangolin", { stdio: "ignore" });
717
+ console.log("Pangolin CLI already installed");
718
+ } catch {
719
+ console.log("Installing Pangolin CLI...");
720
+ execSync2("curl -fsSL https://get.pangolin.cloud | sh", { stdio: "inherit" });
721
+ }
722
+ const pangolinConfig = {
723
+ server: config?.server || "pangolin.to",
724
+ domain: config?.domain,
725
+ autoUpdate: true
726
+ };
727
+ writeFileSync2(
728
+ join2(this.configDir, "pangolin.json"),
729
+ JSON.stringify(pangolinConfig, null, 2)
730
+ );
731
+ console.log("Pangolin configured successfully");
732
+ }
733
+ async setupCloudflare(config) {
734
+ console.log("Setting up Cloudflare tunnel...");
735
+ try {
736
+ execSync2("which cloudflared", { stdio: "ignore" });
737
+ console.log("cloudflared already installed");
738
+ } catch {
739
+ console.log("Installing cloudflared...");
740
+ execSync2("npm install -g cloudflared", { stdio: "inherit" });
741
+ }
742
+ const cfConfig = {
743
+ tunnel: null,
744
+ "credentials-file": join2(this.configDir, "cloudflare-credentials.json"),
745
+ ingress: [
746
+ {
747
+ hostname: config?.domain,
748
+ service: "http://localhost:3000"
749
+ },
750
+ {
751
+ service: "http_status:404"
752
+ }
753
+ ]
754
+ };
755
+ writeFileSync2(
756
+ join2(this.configDir, "cloudflare.yml"),
757
+ JSON.stringify(cfConfig, null, 2)
758
+ );
759
+ console.log("Cloudflare tunnel configured");
760
+ }
761
+ startTunnel(provider) {
762
+ console.log(`Starting ${provider} tunnel...`);
763
+ if (provider === "pangolin") {
764
+ execSync2("pangolin start", { stdio: "inherit" });
765
+ } else {
766
+ execSync2("cloudflared tunnel run", { stdio: "inherit" });
767
+ }
768
+ }
769
+ stopTunnel(provider) {
770
+ console.log(`Stopping ${provider} tunnel...`);
771
+ try {
772
+ if (provider === "pangolin") {
773
+ execSync2("pkill pangolin", { stdio: "ignore" });
774
+ } else {
775
+ execSync2("pkill cloudflared", { stdio: "ignore" });
776
+ }
777
+ console.log(`${provider} tunnel stopped`);
778
+ } catch {
779
+ console.log(`${provider} tunnel was not running`);
780
+ }
781
+ }
782
+ getConfig() {
783
+ try {
784
+ const configPath = join2(this.configDir, "config.json");
785
+ if (!existsSync2(configPath)) {
786
+ return null;
787
+ }
788
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
789
+ } catch {
790
+ return null;
791
+ }
792
+ }
793
+ };
794
+ async function runLegsProxySetup(options) {
795
+ console.log("\u{1F9B5} Setting up Legs (Traefik reverse proxy)...\n");
796
+ const traefik = options.standalone ? new TraefikService("/opt/eve/traefik") : new TraefikService();
797
+ console.log("Step 1: Installing Traefik...");
798
+ await traefik.install();
799
+ console.log("\nStep 2: Configuring routes...");
800
+ const organs = [
801
+ { name: "brain", path: "/brain", port: 3e3 },
802
+ { name: "heart", path: "/heart", port: 4e3 },
803
+ { name: "memory", path: "/memory", port: 5432 },
804
+ { name: "nerves", path: "/nerves", port: 6379 },
805
+ { name: "eyes", path: "/eyes", port: 8080 },
806
+ { name: "dna", path: "/dna", port: 9e3 }
807
+ ];
808
+ for (const organ of organs) {
809
+ const target = `http://localhost:${organ.port}`;
810
+ await traefik.addRoute({
811
+ path: organ.path,
812
+ target,
813
+ domain: options.domain,
814
+ ssl: false
815
+ });
816
+ console.log(` \u2713 Route: ${organ.path} -> ${target}`);
817
+ }
818
+ if (options.tunnel) {
819
+ const tunnelMode = options.tunnel === "cloudflare_tunnel" ? "cloudflare" : options.tunnel === "pangolin_tunnel" ? "pangolin" : options.tunnel;
820
+ console.log(`
821
+ Step 3: Setting up ${tunnelMode} tunnel...`);
822
+ const tunnel = new TunnelService();
823
+ if (tunnelMode === "pangolin") {
824
+ await tunnel.setupPangolin({ domain: options.tunnelDomain });
825
+ console.log(" \u2713 Pangolin tunnel configured");
826
+ } else if (tunnelMode === "cloudflare") {
827
+ await tunnel.setupCloudflare({ domain: options.tunnelDomain });
828
+ console.log(" \u2713 Cloudflare tunnel configured");
829
+ } else {
830
+ console.warn(` \u26A0 Unknown tunnel provider: ${String(options.tunnel)}`);
831
+ }
832
+ }
833
+ if (options.domain) {
834
+ console.log(`
835
+ Step 4: Configuring domain ${options.domain}...`);
836
+ await traefik.configureDomain(options.domain);
837
+ console.log(" \u2713 Domain configured");
838
+ if (options.ssl) {
839
+ console.log("\nStep 5: Enabling SSL...");
840
+ await traefik.enableSSL();
841
+ console.log(" \u2713 SSL enabled with Let's Encrypt");
842
+ }
843
+ }
844
+ console.log("\n\u2705 Legs setup complete!");
845
+ console.log("\nYour organs are now accessible at:");
846
+ const baseUrl = options.domain || "localhost";
847
+ const protocol = options.ssl ? "https" : "http";
848
+ for (const organ of organs) {
849
+ console.log(` ${protocol}://${baseUrl}${organ.path}`);
850
+ }
851
+ if (options.tunnel) {
852
+ console.log(`
853
+ Tunnel configured with ${options.tunnel}`);
854
+ if (options.tunnelDomain) {
855
+ console.log(`External domain: ${options.tunnelDomain}`);
856
+ }
857
+ }
858
+ }
859
+ function setupCommand(program) {
860
+ program.command("setup").description("Setup Traefik reverse proxy for Eve").option("--domain <domain>", "Custom domain for external access").option(
861
+ "--tunnel <provider>",
862
+ "Tunnel provider (pangolin, cloudflare, pangolin_tunnel, cloudflare_tunnel)"
863
+ ).option("--tunnel-domain <domain>", "Domain for tunnel (if using tunnel)").option("--ssl", "Enable SSL/TLS (requires --domain)").option("--standalone", "Install standalone Traefik (not using Dokploy)").action(async (options) => {
864
+ try {
865
+ await runLegsProxySetup({
866
+ domain: options.domain,
867
+ tunnel: options.tunnel,
868
+ tunnelDomain: options.tunnelDomain,
869
+ ssl: Boolean(options.ssl),
870
+ standalone: Boolean(options.standalone)
871
+ });
872
+ } catch (error) {
873
+ console.error("\n\u274C Setup failed:", error);
874
+ process.exit(1);
875
+ }
876
+ });
877
+ }
878
+ function domainCommand(program) {
879
+ const domain = program.command("domain").description("Configure Traefik domain and SSL");
880
+ domain.command("set <domain>").description("Set primary domain for Traefik routes").option("--ssl", "Enable SSL with Let's Encrypt").option("--email <email>", "ACME email for SSL certificate").option("--behind-proxy", "HTTP-only mode: external proxy handles SSL (no port 443, no redirects)").action(async (domainName, options) => {
881
+ try {
882
+ const traefik = new TraefikService();
883
+ if (options.behindProxy && options.ssl) {
884
+ console.error("--behind-proxy and --ssl are mutually exclusive. The external proxy handles SSL.");
885
+ process.exit(1);
886
+ }
887
+ const domainError = validateBaseDomain(domainName);
888
+ if (domainError) {
889
+ console.error(domainError);
890
+ process.exit(1);
891
+ }
892
+ const prior = await readEveSecrets(process.cwd());
893
+ const explicitMode = options.behindProxy === true || options.ssl === true;
894
+ const behindProxy = options.behindProxy === true ? true : options.ssl === true ? false : !!prior?.domain?.behindProxy;
895
+ const ssl = behindProxy ? false : options.ssl === true || !explicitMode && !!prior?.domain?.ssl;
896
+ const email = options.email ?? prior?.domain?.email;
897
+ let installedComponents = [];
898
+ try {
899
+ installedComponents = await entityStateManager.getInstalledComponents();
900
+ } catch {
901
+ }
902
+ const inheritedNote = !explicitMode ? ` (inherited mode: ${behindProxy ? "behind-proxy" : ssl ? "ssl" : "plain"})` : "";
903
+ console.log(`Configuring domain: ${domainName}${inheritedNote}`);
904
+ if (behindProxy) {
905
+ await traefik.enableProxyMode(domainName, installedComponents);
906
+ await writeEveSecrets(
907
+ { domain: { primary: domainName, ssl: false, behindProxy: true, email } },
908
+ process.cwd()
909
+ );
910
+ console.log(`Domain configured: ${domainName} (behind-proxy, HTTP only)`);
911
+ console.log("External proxy must forward port 80 and handle SSL termination.");
912
+ } else {
913
+ await traefik.configureSubdomains(domainName, ssl, email, installedComponents, false);
914
+ await writeEveSecrets(
915
+ { domain: { primary: domainName, ssl, email, behindProxy: false } },
916
+ process.cwd()
917
+ );
918
+ const protocol = ssl ? "https" : "http";
919
+ console.log(`Domain configured: ${domainName}${ssl ? " (SSL)" : ""}`);
920
+ console.log("\nExample endpoints:");
921
+ for (const sub of ["pod", "pod-admin", "eve"]) {
922
+ console.log(` ${protocol}://${sub}.${domainName}`);
923
+ }
924
+ }
925
+ } catch (error) {
926
+ console.error("Domain configuration failed:", error instanceof Error ? error.message : error);
927
+ process.exit(1);
928
+ }
929
+ });
930
+ domain.command("unset").description("Revert domain to localhost").action(async () => {
931
+ try {
932
+ const traefik = new TraefikService();
933
+ await traefik.configureDomain("localhost");
934
+ console.log("Domain configuration removed (localhost)");
935
+ } catch (error) {
936
+ console.error("Failed to unset domain:", error instanceof Error ? error.message : error);
937
+ process.exit(1);
938
+ }
939
+ });
940
+ domain.command("status").description("Show current domain and route configuration").action(async () => {
941
+ try {
942
+ const traefik = new TraefikService();
943
+ const status = await traefik.getStatus();
944
+ console.log("Legs (Traefik) configuration:\n");
945
+ const state = status.installed ? status.running ? "running" : "installed (not running)" : "not installed";
946
+ console.log(`Status: ${state}`);
947
+ console.log(`Domain: ${status.domain || "localhost"}`);
948
+ console.log(`SSL: ${status.ssl ? "enabled" : "disabled"}`);
949
+ console.log(`
950
+ Routes (${status.routes.length}):`);
951
+ for (const route of status.routes) {
952
+ console.log(` ${route.path} -> ${route.target}`);
953
+ }
954
+ } catch (error) {
955
+ console.error("Failed to get status:", error instanceof Error ? error.message : error);
956
+ process.exit(1);
957
+ }
958
+ });
959
+ }
960
+ var COMPOSE = `services:
961
+ newt:
962
+ image: fosrl/newt:latest
963
+ container_name: eve-legs-newt
964
+ restart: unless-stopped
965
+ env_file:
966
+ - \${NEWT_ENV_FILE}
967
+ networks:
968
+ - eve-network
969
+
970
+ networks:
971
+ eve-network:
972
+ external: true
973
+ `.trim();
974
+ function composePath(cwd) {
975
+ return join3(cwd, ".eve", "legs", "newt-compose.yml");
976
+ }
977
+ function envPath(cwd) {
978
+ return join3(cwd, ".eve", "legs", "newt.env");
979
+ }
980
+ function writeNewtEnvTemplate(cwd) {
981
+ const dir = join3(cwd, ".eve", "legs");
982
+ mkdirSync3(dir, { recursive: true });
983
+ const p = envPath(cwd);
984
+ if (!existsSync3(p)) {
985
+ writeFileSync3(
986
+ p,
987
+ [
988
+ "# Pangolin Newt site connector \u2014 https://docs.pangolin.net/manage/sites/install-site",
989
+ "PANGOLIN_ENDPOINT=https://your-pangolin-host.example",
990
+ "NEWT_ID=",
991
+ "NEWT_SECRET=",
992
+ "LOG_LEVEL=INFO",
993
+ "# Optional: Docker discovery (read-only socket)",
994
+ "# DOCKER_SOCKET=unix:///var/run/docker.sock",
995
+ ""
996
+ ].join("\n"),
997
+ "utf-8"
998
+ );
999
+ }
1000
+ return p;
1001
+ }
1002
+ function newtCommand(program) {
1003
+ const n = program.command("newt").description("Pangolin Newt (site connector) on eve-network \u2014 fosrl/newt");
1004
+ n.command("init").description("Write .eve/legs/newt.env template if missing").action(() => {
1005
+ const cwd = process.cwd();
1006
+ const p = writeNewtEnvTemplate(cwd);
1007
+ console.log(`Newt env template: ${p}
1008
+ Edit PANGOLIN_ENDPOINT, NEWT_ID, NEWT_SECRET then run: eve legs newt up`);
1009
+ });
1010
+ n.command("up").description("Start Newt container (requires filled newt.env + eve-network)").action(() => {
1011
+ const cwd = process.cwd();
1012
+ writeNewtEnvTemplate(cwd);
1013
+ const envFile = envPath(cwd);
1014
+ mkdirSync3(join3(cwd, ".eve", "legs"), { recursive: true });
1015
+ writeFileSync3(composePath(cwd), COMPOSE, "utf-8");
1016
+ try {
1017
+ execSync3("docker network create eve-network", { stdio: "ignore" });
1018
+ } catch {
1019
+ }
1020
+ const env = { ...process.env, NEWT_ENV_FILE: envFile };
1021
+ execSync3(`docker compose -f "${composePath(cwd)}" up -d`, { stdio: "inherit", cwd, env });
1022
+ console.log("\nNewt running. See Pangolin dashboard for site status.\n");
1023
+ });
1024
+ n.command("down").description("Stop Newt container").action(() => {
1025
+ const cwd = process.cwd();
1026
+ if (!existsSync3(composePath(cwd))) {
1027
+ console.log("No newt compose. Run: eve legs newt up");
1028
+ return;
1029
+ }
1030
+ execSync3(`docker compose -f "${composePath(cwd)}" down`, { stdio: "inherit", cwd, env: { ...process.env, NEWT_ENV_FILE: envPath(cwd) } });
1031
+ });
1032
+ }
1033
+ var CONTAINER = "eve-legs-traefik";
1034
+ var CONFIG_DIR = "/opt/traefik";
1035
+ function reconnectSynapToEveNetwork() {
1036
+ try {
1037
+ const name = execSync4(
1038
+ `docker ps --filter "label=com.docker.compose.project=synap-backend" --filter "label=com.docker.compose.service=backend" --format "{{.Names}}"`,
1039
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
1040
+ ).trim().split("\n")[0]?.trim();
1041
+ if (!name) return;
1042
+ try {
1043
+ const inspect = execSync4(
1044
+ `docker inspect --format "{{json .NetworkSettings.Networks}}" ${name}`,
1045
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
1046
+ ).trim();
1047
+ const networks = JSON.parse(inspect);
1048
+ const eve = networks["eve-network"];
1049
+ if (eve && (eve.Aliases ?? []).includes("eve-brain-synap")) return;
1050
+ if (eve) {
1051
+ try {
1052
+ execSync4(`docker network disconnect eve-network ${name}`, { stdio: ["pipe", "pipe", "ignore"] });
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ } catch {
1057
+ }
1058
+ try {
1059
+ execSync4(`docker network connect --alias eve-brain-synap eve-network ${name}`, { stdio: ["pipe", "pipe", "ignore"] });
1060
+ console.log(` Reconnected ${name} \u2192 eve-network (alias: eve-brain-synap)`);
1061
+ } catch {
1062
+ }
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ function freePort(port) {
1067
+ try {
1068
+ const out = execSync4(
1069
+ `docker ps --format '{{.Names}}\\t{{.Ports}}' | grep ':${port}->'`,
1070
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
1071
+ ).trim();
1072
+ for (const line of out.split("\n").filter(Boolean)) {
1073
+ const name = line.split(" ")[0]?.trim();
1074
+ if (name && name !== CONTAINER) {
1075
+ console.log(` Stopping container holding port ${port}: ${name}`);
1076
+ spawnSync2("docker", ["stop", name], { stdio: "inherit" });
1077
+ }
1078
+ }
1079
+ } catch {
1080
+ }
1081
+ }
1082
+ function restartCommand(program) {
1083
+ program.command("restart").description("Recreate the Traefik container with correct volume mounts").action(() => {
1084
+ if (!existsSync4(`${CONFIG_DIR}/traefik.yml`)) {
1085
+ console.error(`No Traefik config found at ${CONFIG_DIR}/traefik.yml`);
1086
+ console.error("Run: eve domain set <domain> --ssl --email <email> first");
1087
+ process.exit(1);
1088
+ }
1089
+ console.log("Freeing ports 80 / 443...");
1090
+ freePort(80);
1091
+ freePort(443);
1092
+ const acmePath = `${CONFIG_DIR}/acme.json`;
1093
+ if (!existsSync4(acmePath)) {
1094
+ execSync4(`touch ${acmePath} && chmod 600 ${acmePath}`);
1095
+ console.log(`Created ${acmePath} (600)`);
1096
+ } else {
1097
+ execSync4(`chmod 600 ${acmePath}`);
1098
+ console.log(`Fixed permissions on ${acmePath} \u2192 600`);
1099
+ }
1100
+ console.log(`Removing existing ${CONTAINER} container (if any)...`);
1101
+ spawnSync2("docker", ["rm", "-f", CONTAINER], { stdio: "inherit" });
1102
+ console.log("Starting Traefik with correct volume mounts...");
1103
+ const result = spawnSync2("docker", [
1104
+ "run",
1105
+ "-d",
1106
+ "--name",
1107
+ CONTAINER,
1108
+ "--restart",
1109
+ "unless-stopped",
1110
+ "-p",
1111
+ "80:80",
1112
+ "-p",
1113
+ "443:443",
1114
+ "-p",
1115
+ "8080:8080",
1116
+ "-v",
1117
+ "/var/run/docker.sock:/var/run/docker.sock:ro",
1118
+ "-v",
1119
+ `${CONFIG_DIR}/traefik.yml:/etc/traefik/traefik.yml:ro`,
1120
+ "-v",
1121
+ `${CONFIG_DIR}/dynamic:/etc/traefik/dynamic:ro`,
1122
+ "-v",
1123
+ `${acmePath}:/etc/traefik/acme.json`,
1124
+ "--network",
1125
+ "eve-network",
1126
+ "traefik:v3.0"
1127
+ ], { stdio: "inherit" });
1128
+ if (result.status !== 0) {
1129
+ console.error("\nFailed to start Traefik. Containers currently using port 80/443:");
1130
+ spawnSync2("docker", ["ps", "--format", "{{.Names}} {{.Ports}}"], { stdio: "inherit" });
1131
+ process.exit(1);
1132
+ }
1133
+ reconnectSynapToEveNetwork();
1134
+ console.log("\u2713 Traefik running \u2014 routes are live");
1135
+ });
1136
+ }
1137
+ var CONTAINER2 = "eve-legs-traefik";
1138
+ var CONFIG_DIR2 = "/opt/traefik";
1139
+ var API = "http://localhost:8080/api";
1140
+ function tryFetch(url) {
1141
+ try {
1142
+ const out = execSync5(`curl -sf "${url}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
1143
+ return JSON.parse(out);
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+ function statusCommand(program) {
1149
+ program.command("status").description("Show Traefik health, routes, and config").option("--logs", "Show last 30 lines of Traefik logs").action((opts) => {
1150
+ console.log("\n\u{1F9BF} Eve Legs \u2014 Traefik Status\n");
1151
+ let running = false;
1152
+ try {
1153
+ const out = execSync5(
1154
+ `docker ps --filter "name=${CONTAINER2}" --format "{{.Names}}\\t{{.Status}}"`,
1155
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
1156
+ ).trim();
1157
+ running = out.includes(CONTAINER2);
1158
+ if (running) {
1159
+ console.log(` Container: \u2713 running (${out.split(" ")[1] ?? ""})`);
1160
+ } else {
1161
+ console.log(` Container: \u2717 NOT running`);
1162
+ }
1163
+ } catch {
1164
+ console.log(` Container: \u2717 docker error`);
1165
+ }
1166
+ const staticExists = existsSync5(`${CONFIG_DIR2}/traefik.yml`);
1167
+ const dynamicExists = existsSync5(`${CONFIG_DIR2}/dynamic/eve-routes.yml`);
1168
+ console.log(` Static cfg: ${staticExists ? "\u2713" : "\u2717 MISSING"} ${CONFIG_DIR2}/traefik.yml`);
1169
+ console.log(` Routes cfg: ${dynamicExists ? "\u2713" : "\u2717 MISSING"} ${CONFIG_DIR2}/dynamic/eve-routes.yml`);
1170
+ if (staticExists) {
1171
+ console.log("\n \u2500\u2500 traefik.yml \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1172
+ console.log(readFileSync3(`${CONFIG_DIR2}/traefik.yml`, "utf-8").split("\n").map((l) => " " + l).join("\n"));
1173
+ }
1174
+ if (dynamicExists) {
1175
+ console.log("\n \u2500\u2500 eve-routes.yml \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1176
+ console.log(readFileSync3(`${CONFIG_DIR2}/dynamic/eve-routes.yml`, "utf-8").split("\n").map((l) => " " + l).join("\n"));
1177
+ }
1178
+ if (running) {
1179
+ console.log("\n \u2500\u2500 Active routers (Traefik API) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1180
+ const routers = tryFetch(`${API}/http/routers`);
1181
+ if (!routers) {
1182
+ console.log(" Could not reach Traefik API at port 8080 \u2014 is insecure API enabled?");
1183
+ } else if (routers.length === 0) {
1184
+ console.log(" No routers registered yet");
1185
+ } else {
1186
+ for (const r of routers) {
1187
+ const status = r.status === "enabled" ? "\u2713" : "\u2717";
1188
+ console.log(` ${status} ${r.name}`);
1189
+ console.log(` Rule: ${r.rule}`);
1190
+ console.log(` Entry: ${(r.entryPoints ?? []).join(", ")}`);
1191
+ console.log(` Status: ${r.status}`);
1192
+ }
1193
+ }
1194
+ const certs = tryFetch(`${API}/tls/certificates`);
1195
+ if (certs && certs.length > 0) {
1196
+ console.log("\n \u2500\u2500 TLS Certificates \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1197
+ for (const c of certs) {
1198
+ console.log(` \u2713 ${c.subject?.commonName} (expires ${c.notAfter})`);
1199
+ }
1200
+ } else if (certs !== null) {
1201
+ console.log("\n \u2500\u2500 TLS Certificates \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1202
+ console.log(" No certificates yet \u2014 DNS must propagate before Let's Encrypt can provision");
1203
+ }
1204
+ }
1205
+ if (opts.logs || !running) {
1206
+ console.log("\n \u2500\u2500 Traefik logs (last 30 lines) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1207
+ spawnSync3("docker", ["logs", "--tail", "30", CONTAINER2], { stdio: "inherit" });
1208
+ }
1209
+ console.log();
1210
+ });
1211
+ }
1212
+ function proxyModeCommand(program) {
1213
+ const cmd = program.command("proxy-mode").description("Run Traefik behind an external SSL-terminating proxy (Caddy, Nginx, etc.)");
1214
+ cmd.command("enable").description("Switch to behind-proxy mode: HTTP only on port 80, no SSL/redirects").action(async () => {
1215
+ try {
1216
+ const secrets = await readEveSecrets(process.cwd());
1217
+ const domain = secrets?.domain?.primary;
1218
+ if (!domain) {
1219
+ console.error("No domain configured. Run `eve legs domain set <domain>` first.");
1220
+ process.exit(1);
1221
+ }
1222
+ const status = new TraefikService().getProxyModeStatus();
1223
+ if (status.enabled) {
1224
+ console.log("Already in behind-proxy mode.");
1225
+ printProxyInstructions(domain);
1226
+ return;
1227
+ }
1228
+ let installedComponents = [];
1229
+ try {
1230
+ installedComponents = await entityStateManager.getInstalledComponents();
1231
+ } catch {
1232
+ }
1233
+ console.log(`Enabling behind-proxy mode for domain: ${domain}`);
1234
+ const traefik = new TraefikService();
1235
+ await traefik.enableProxyMode(domain, installedComponents);
1236
+ await writeEveSecrets({ domain: { ...secrets?.domain, behindProxy: true } }, process.cwd());
1237
+ console.log("\nBehind-proxy mode enabled.");
1238
+ console.log("Traefik now listens on port 80 only (no port 443, no SSL).\n");
1239
+ printProxyInstructions(domain);
1240
+ } catch (err) {
1241
+ console.error("Failed to enable proxy mode:", err instanceof Error ? err.message : err);
1242
+ process.exit(1);
1243
+ }
1244
+ });
1245
+ cmd.command("disable").description("Restore standalone mode (Traefik handles SSL directly)").option("--ssl", "Re-enable SSL with Let's Encrypt").option("--email <email>", "ACME email address for SSL certificate").action(async (options) => {
1246
+ try {
1247
+ const secrets = await readEveSecrets(process.cwd());
1248
+ const domain = secrets?.domain?.primary;
1249
+ if (!domain) {
1250
+ console.error("No domain configured. Run `eve legs domain set <domain>` first.");
1251
+ process.exit(1);
1252
+ }
1253
+ const status = new TraefikService().getProxyModeStatus();
1254
+ if (!status.enabled) {
1255
+ console.log("Already in standalone mode (not behind a proxy).");
1256
+ return;
1257
+ }
1258
+ const ssl = options.ssl ?? false;
1259
+ const email = options.email ?? secrets?.domain?.email;
1260
+ if (ssl && !email) {
1261
+ console.error("--email <email> is required when using --ssl.");
1262
+ process.exit(1);
1263
+ }
1264
+ let installedComponents = [];
1265
+ try {
1266
+ installedComponents = await entityStateManager.getInstalledComponents();
1267
+ } catch {
1268
+ }
1269
+ console.log(`Restoring standalone mode for domain: ${domain}`);
1270
+ const traefik = new TraefikService();
1271
+ await traefik.disableProxyMode(domain, ssl, email, installedComponents);
1272
+ await writeEveSecrets(
1273
+ { domain: { ...secrets?.domain, ssl, email, behindProxy: false } },
1274
+ process.cwd()
1275
+ );
1276
+ console.log("\nStandalone mode restored.");
1277
+ console.log(`Traefik now listens on ports 80 and 443.`);
1278
+ if (ssl) console.log(`SSL certificates will be obtained via Let's Encrypt for ${domain}.`);
1279
+ } catch (err) {
1280
+ console.error("Failed to disable proxy mode:", err instanceof Error ? err.message : err);
1281
+ process.exit(1);
1282
+ }
1283
+ });
1284
+ cmd.command("status").description("Show current proxy mode configuration").action(async () => {
1285
+ try {
1286
+ const traefik = new TraefikService();
1287
+ const status = traefik.getProxyModeStatus();
1288
+ const secrets = await readEveSecrets(process.cwd());
1289
+ const domain = secrets?.domain?.primary ?? "(not set)";
1290
+ console.log(`
1291
+ Traefik mode: ${status.enabled ? "behind-proxy (HTTP only)" : "standalone (handles SSL)"}`);
1292
+ console.log(`Domain: ${domain}`);
1293
+ console.log(`SSL: ${status.ssl ? "enabled (Let's Encrypt)" : "disabled"}`);
1294
+ console.log(`Entrypoints: ${status.entrypoints.join(", ")}`);
1295
+ console.log(`Port 443: ${status.enabled ? "not bound (proxy handles it)" : "bound"}`);
1296
+ if (status.enabled) {
1297
+ console.log("\nYour external proxy must:");
1298
+ console.log(` - Terminate SSL for ${domain} and its subdomains`);
1299
+ console.log(" - Forward requests to http://<this-server>:80");
1300
+ console.log(" - Set X-Forwarded-Proto: https and X-Forwarded-Host headers");
1301
+ }
1302
+ console.log();
1303
+ } catch (err) {
1304
+ console.error("Failed to get proxy mode status:", err instanceof Error ? err.message : err);
1305
+ process.exit(1);
1306
+ }
1307
+ });
1308
+ }
1309
+ function printProxyInstructions(domain) {
1310
+ console.log("Configure your external proxy to:");
1311
+ console.log(` 1. Terminate SSL for ${domain} and its subdomains (pod.${domain}, etc.)`);
1312
+ console.log(" 2. Forward requests to http://<this-server-ip>:80");
1313
+ console.log(" 3. Pass these headers:");
1314
+ console.log(" X-Forwarded-For: <client-ip>");
1315
+ console.log(" X-Forwarded-Proto: https");
1316
+ console.log(` X-Forwarded-Host: <subdomain>.${domain}`);
1317
+ console.log();
1318
+ console.log("Example Caddy config:");
1319
+ console.log(` ${domain}, *.${domain} {`);
1320
+ console.log(` reverse_proxy <this-server-ip>:80 {`);
1321
+ console.log(` header_up X-Forwarded-Host {host}`);
1322
+ console.log(` header_up X-Forwarded-Proto https`);
1323
+ console.log(` }`);
1324
+ console.log(` }`);
1325
+ console.log();
1326
+ }
1327
+ var GATEWAY_CONTAINER = "eve-inference-gateway";
1328
+ var DEFAULT_HOST_PORT = "11435";
1329
+ var InferenceGateway = class {
1330
+ baseDir;
1331
+ hostPort;
1332
+ cwd;
1333
+ constructor(cwd = process.cwd(), hostPort = DEFAULT_HOST_PORT) {
1334
+ this.cwd = cwd;
1335
+ this.baseDir = join4(cwd, ".eve", "inference-gateway");
1336
+ this.hostPort = process.env.EVE_INFERENCE_GATEWAY_PORT?.trim() || hostPort;
1337
+ }
1338
+ /**
1339
+ * APR1 hash line for Traefik usersFile (user:hash).
1340
+ *
1341
+ * Pipe the password via stdin instead of passing it as an arg — base64url
1342
+ * passwords starting with `-` were being interpreted by openssl as a flag
1343
+ * ("Unknown option: -LglNqOAY..."). The `-stdin` form is unambiguous.
1344
+ */
1345
+ htpasswdLine(username, plainPassword) {
1346
+ const r = spawnSync4("openssl", ["passwd", "-apr1", "-stdin"], {
1347
+ input: plainPassword + "\n",
1348
+ encoding: "utf-8"
1349
+ });
1350
+ if (r.error || r.status !== 0) {
1351
+ throw new Error(`openssl passwd -apr1 failed: ${r.stderr || r.error?.message || "unknown"}`);
1352
+ }
1353
+ const hash = (r.stdout ?? "").trim();
1354
+ return `${username}:${hash}`;
1355
+ }
1356
+ async ensure(ollamaHost = "http://eve-brain-ollama:11434") {
1357
+ mkdirSync4(join4(this.baseDir, "dynamic"), { recursive: true });
1358
+ const username = "eve";
1359
+ const password = randomBytes(18).toString("base64url").slice(0, 24);
1360
+ const userLine = this.htpasswdLine(username, password);
1361
+ const staticYaml = `
1362
+ entryPoints:
1363
+ web:
1364
+ address: ":80"
1365
+
1366
+ providers:
1367
+ file:
1368
+ directory: /etc/traefik/dynamic
1369
+ watch: true
1370
+
1371
+ log:
1372
+ level: INFO
1373
+ `.trim();
1374
+ writeFileSync4(join4(this.baseDir, "dynamic", "ollama-users"), `${userLine}
1375
+ `, { mode: 384 });
1376
+ const dynamicYaml = [
1377
+ "http:",
1378
+ " routers:",
1379
+ " ollama_api:",
1380
+ " rule: PathPrefix(`/`)",
1381
+ " entryPoints:",
1382
+ " - web",
1383
+ " service: ollama_svc",
1384
+ " middlewares:",
1385
+ " - ollama_auth",
1386
+ " middlewares:",
1387
+ " ollama_auth:",
1388
+ " basicAuth:",
1389
+ " usersFile: /etc/traefik/dynamic/ollama-users",
1390
+ " services:",
1391
+ " ollama_svc:",
1392
+ " loadBalancer:",
1393
+ " servers:",
1394
+ ` - url: "${ollamaHost}"`
1395
+ ].join("\n");
1396
+ writeFileSync4(join4(this.baseDir, "traefik.yml"), staticYaml);
1397
+ writeFileSync4(join4(this.baseDir, "dynamic", "ollama.yml"), dynamicYaml);
1398
+ const secretsFile = join4(this.baseDir, "..", "secrets", "ollama-gateway.txt");
1399
+ mkdirSync4(join4(this.baseDir, "..", "secrets"), { recursive: true });
1400
+ const secretBody = `# Eve inference gateway (Basic auth for Ollama)
1401
+ URL=http://127.0.0.1:${this.hostPort}
1402
+ USER=${username}
1403
+ PASS=${password}
1404
+
1405
+ Example:
1406
+ curl -u '${username}:${password}' http://127.0.0.1:${this.hostPort}/api/tags
1407
+ `;
1408
+ writeFileSync4(secretsFile, secretBody, { mode: 384 });
1409
+ const prevSecrets = await readEveSecrets(this.cwd);
1410
+ await writeEveSecrets(
1411
+ {
1412
+ inference: {
1413
+ ...prevSecrets?.inference ?? {},
1414
+ gatewayUrl: `http://127.0.0.1:${this.hostPort}`,
1415
+ gatewayUser: username,
1416
+ gatewayPass: password,
1417
+ ollamaUrl: prevSecrets?.inference?.ollamaUrl ?? ollamaHost
1418
+ }
1419
+ },
1420
+ this.cwd
1421
+ );
1422
+ try {
1423
+ execSync6("docker network create eve-network", { stdio: "ignore" });
1424
+ } catch {
1425
+ }
1426
+ const running = this.isGatewayRunning();
1427
+ if (!running) {
1428
+ if (this.gatewayContainerExists()) {
1429
+ execSync6(`docker start ${GATEWAY_CONTAINER}`, { stdio: "inherit" });
1430
+ } else {
1431
+ execSync6(
1432
+ [
1433
+ "docker",
1434
+ "run",
1435
+ "-d",
1436
+ "--name",
1437
+ GATEWAY_CONTAINER,
1438
+ "--restart",
1439
+ "unless-stopped",
1440
+ "--network",
1441
+ "eve-network",
1442
+ "-p",
1443
+ `${this.hostPort}:80`,
1444
+ "-v",
1445
+ `${this.baseDir}/traefik.yml:/etc/traefik/traefik.yml:ro`,
1446
+ "-v",
1447
+ `${this.baseDir}/dynamic:/etc/traefik/dynamic:ro`,
1448
+ "traefik:v3.0",
1449
+ "--configFile=/etc/traefik/traefik.yml"
1450
+ ].join(" "),
1451
+ { stdio: "inherit" }
1452
+ );
1453
+ }
1454
+ }
1455
+ return {
1456
+ baseDir: this.baseDir,
1457
+ hostPort: this.hostPort,
1458
+ publicUrl: `http://127.0.0.1:${this.hostPort}`,
1459
+ username,
1460
+ password,
1461
+ secretsFile
1462
+ };
1463
+ }
1464
+ gatewayContainerExists() {
1465
+ try {
1466
+ execSync6(`docker container inspect ${GATEWAY_CONTAINER}`, { stdio: "ignore" });
1467
+ return true;
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1472
+ isGatewayRunning() {
1473
+ try {
1474
+ const out = execSync6(`docker inspect -f '{{.State.Running}}' ${GATEWAY_CONTAINER}`, {
1475
+ encoding: "utf-8"
1476
+ }).trim();
1477
+ return out === "true";
1478
+ } catch {
1479
+ return false;
1480
+ }
1481
+ }
1482
+ };
1483
+ async function refreshTraefikRoutes(cwd) {
1484
+ const secrets = await readEveSecrets(cwd ?? process.cwd());
1485
+ const domain = secrets?.domain?.primary;
1486
+ if (!domain) {
1487
+ return { refreshed: false, domain: null, reason: "no domain configured" };
1488
+ }
1489
+ let installedComponents = [];
1490
+ try {
1491
+ installedComponents = await entityStateManager.getInstalledComponents();
1492
+ } catch {
1493
+ }
1494
+ try {
1495
+ const traefik = new TraefikService();
1496
+ await traefik.configureSubdomains(
1497
+ domain,
1498
+ secrets?.domain?.ssl !== false,
1499
+ secrets?.domain?.email,
1500
+ installedComponents,
1501
+ !!secrets?.domain?.behindProxy,
1502
+ secrets?.domain?.customRoutes
1503
+ );
1504
+ return { refreshed: true, domain };
1505
+ } catch (err) {
1506
+ return {
1507
+ refreshed: false,
1508
+ domain,
1509
+ reason: err instanceof Error ? err.message : String(err)
1510
+ };
1511
+ }
1512
+ }
1513
+ var execFileAsync = promisify(execFile);
1514
+ function sleep(ms) {
1515
+ return new Promise((r) => setTimeout(r, ms));
1516
+ }
1517
+ var DOCKER_CALL_TIMEOUT_MS = 4e3;
1518
+ async function isContainerRunning(name) {
1519
+ try {
1520
+ const { stdout } = await execFileAsync(
1521
+ "docker",
1522
+ ["ps", "--filter", `name=^${name}$`, "--format", "{{.Names}}"],
1523
+ { timeout: DOCKER_CALL_TIMEOUT_MS }
1524
+ );
1525
+ if (stdout.trim() === name) return true;
1526
+ } catch {
1527
+ }
1528
+ try {
1529
+ const { stdout: ids } = await execFileAsync(
1530
+ "docker",
1531
+ ["ps", "--filter", `network=eve-network`, "--format", "{{.ID}}"],
1532
+ { timeout: DOCKER_CALL_TIMEOUT_MS }
1533
+ );
1534
+ const containerIds = ids.trim().split("\n").filter(Boolean);
1535
+ for (const id of containerIds) {
1536
+ try {
1537
+ const { stdout: aliasJson } = await execFileAsync(
1538
+ "docker",
1539
+ ["inspect", "--format", "{{json .NetworkSettings.Networks}}", id],
1540
+ { timeout: DOCKER_CALL_TIMEOUT_MS }
1541
+ );
1542
+ const networks = JSON.parse(aliasJson.trim());
1543
+ const aliases = networks["eve-network"]?.Aliases ?? [];
1544
+ if (aliases.includes(name)) return true;
1545
+ } catch {
1546
+ continue;
1547
+ }
1548
+ }
1549
+ } catch {
1550
+ }
1551
+ return false;
1552
+ }
1553
+ async function canReachFromTraefik(host, port, path) {
1554
+ try {
1555
+ const { stdout } = await execFileAsync(
1556
+ "docker",
1557
+ [
1558
+ "exec",
1559
+ "eve-legs-traefik",
1560
+ "sh",
1561
+ "-c",
1562
+ `wget -q -O /dev/null --timeout=3 --server-response http://${host}:${port}${path} 2>&1 | grep "HTTP/" | tail -1 || echo "no-response"`
1563
+ ],
1564
+ { timeout: DOCKER_CALL_TIMEOUT_MS }
1565
+ );
1566
+ const out = stdout.trim();
1567
+ if (out.includes("no-response")) return false;
1568
+ const match = out.match(/HTTP\/\S+\s+(\d{3})/);
1569
+ if (!match) return false;
1570
+ return parseInt(match[1], 10) < 500;
1571
+ } catch {
1572
+ return false;
1573
+ }
1574
+ }
1575
+ async function verifyComponent(componentId, opts = {}) {
1576
+ const checks = [];
1577
+ let comp;
1578
+ try {
1579
+ comp = resolveComponent(componentId);
1580
+ } catch {
1581
+ return {
1582
+ ok: false,
1583
+ checks: [{ name: "registry", ok: false, detail: `unknown component: ${componentId}` }],
1584
+ summary: `unknown component: ${componentId}`
1585
+ };
1586
+ }
1587
+ if (!comp.service) {
1588
+ return {
1589
+ ok: true,
1590
+ checks: [{ name: "service", ok: true, detail: "no network service to verify" }],
1591
+ summary: `${comp.label} install complete (no network service)`
1592
+ };
1593
+ }
1594
+ const containerName = comp.service.containerName;
1595
+ const containerAttempts = opts.quick ? 1 : 5;
1596
+ const reachableAttempts = opts.quick ? 1 : 4;
1597
+ let running = false;
1598
+ for (let i = 0; i < containerAttempts; i++) {
1599
+ if (await isContainerRunning(containerName)) {
1600
+ running = true;
1601
+ break;
1602
+ }
1603
+ if (i < containerAttempts - 1) await sleep(1500);
1604
+ }
1605
+ checks.push({
1606
+ name: "container",
1607
+ ok: running,
1608
+ detail: running ? `${containerName} is running` : opts.quick ? `${containerName} not in docker ps` : `${containerName} not in docker ps after 7.5s`
1609
+ });
1610
+ if (!running) {
1611
+ return { ok: false, checks, summary: `${comp.label}: container not running` };
1612
+ }
1613
+ if (comp.service.healthPath) {
1614
+ let reachable = false;
1615
+ for (let i = 0; i < reachableAttempts; i++) {
1616
+ if (await canReachFromTraefik(containerName, comp.service.internalPort, comp.service.healthPath)) {
1617
+ reachable = true;
1618
+ break;
1619
+ }
1620
+ if (i < reachableAttempts - 1) await sleep(2e3);
1621
+ }
1622
+ checks.push({
1623
+ name: "reachable",
1624
+ ok: reachable,
1625
+ detail: reachable ? `responded on :${comp.service.internalPort}${comp.service.healthPath}` : opts.quick ? `not responding on :${comp.service.internalPort}${comp.service.healthPath}` : `not responding on :${comp.service.internalPort}${comp.service.healthPath} after 10s`
1626
+ });
1627
+ if (!reachable) {
1628
+ return { ok: false, checks, summary: `${comp.label}: container running but not responding on its port` };
1629
+ }
1630
+ }
1631
+ return { ok: true, checks, summary: `${comp.label}: container running and reachable` };
1632
+ }
1633
+ var CONTAINER_NAME = "eve-dashboard";
1634
+ var IMAGE_TAG = "eve-dashboard:local";
1635
+ var INTERNAL_PORT = 3e3;
1636
+ var HOST_PORT = 7979;
1637
+ function ensureNetwork(name) {
1638
+ try {
1639
+ execSync7(`docker network inspect ${name}`, { stdio: "ignore" });
1640
+ } catch {
1641
+ execSync7(`docker network create ${name}`, { stdio: "inherit" });
1642
+ }
1643
+ }
1644
+ function ensureEveNetwork() {
1645
+ ensureNetwork("eve-network");
1646
+ }
1647
+ function imageExists() {
1648
+ try {
1649
+ const out = execSync7(`docker image inspect ${IMAGE_TAG}`, { stdio: ["pipe", "pipe", "ignore"] });
1650
+ return out.length > 0;
1651
+ } catch {
1652
+ return false;
1653
+ }
1654
+ }
1655
+ function buildImage(workspaceRoot) {
1656
+ const dockerfile = resolve(workspaceRoot, "packages/eve-dashboard/Dockerfile");
1657
+ if (!existsSync6(dockerfile)) {
1658
+ throw new Error(`Dockerfile not found at ${dockerfile}. Are you in the eve workspace root?`);
1659
+ }
1660
+ console.log(` Building ${IMAGE_TAG} (this takes a minute on first run)...`);
1661
+ const r = spawnSync5(
1662
+ "docker",
1663
+ ["build", "-f", dockerfile, "-t", IMAGE_TAG, workspaceRoot],
1664
+ { stdio: "inherit" }
1665
+ );
1666
+ if (r.status !== 0) throw new Error(`docker build exited with code ${r.status}`);
1667
+ }
1668
+ function removeContainer() {
1669
+ try {
1670
+ execSync7(`docker rm -f ${CONTAINER_NAME}`, { stdio: "ignore" });
1671
+ } catch {
1672
+ }
1673
+ }
1674
+ function runContainer(workspaceRoot, secret) {
1675
+ const stateDir = `${homedir()}/.local/share/eve`;
1676
+ const args = [
1677
+ "run",
1678
+ "-d",
1679
+ "--name",
1680
+ CONTAINER_NAME,
1681
+ "--restart",
1682
+ "unless-stopped",
1683
+ "--network",
1684
+ "eve-network",
1685
+ "-p",
1686
+ `${HOST_PORT}:${INTERNAL_PORT}`,
1687
+ "-e",
1688
+ `PORT=${INTERNAL_PORT}`,
1689
+ "-e",
1690
+ `EVE_HOME=${workspaceRoot}`,
1691
+ "-e",
1692
+ `EVE_DASHBOARD_SECRET=${secret}`,
1693
+ // Mounts that mirror what the host CLI would touch.
1694
+ "-v",
1695
+ "/opt:/opt",
1696
+ "-v",
1697
+ `${stateDir}:${stateDir}`,
1698
+ "-v",
1699
+ "/var/run/docker.sock:/var/run/docker.sock",
1700
+ IMAGE_TAG
1701
+ ];
1702
+ const r = spawnSync5("docker", args, { stdio: "inherit" });
1703
+ if (r.status !== 0) throw new Error(`docker run exited with code ${r.status}`);
1704
+ }
1705
+ function installDashboardContainer(opts) {
1706
+ const workspaceRoot = resolve(opts.workspaceRoot ?? process.cwd());
1707
+ ensureEveNetwork();
1708
+ if (opts.rebuild || !imageExists()) {
1709
+ buildImage(workspaceRoot);
1710
+ }
1711
+ removeContainer();
1712
+ runContainer(workspaceRoot, opts.secret);
1713
+ }
1714
+ function uninstallDashboardContainer() {
1715
+ removeContainer();
1716
+ }
1717
+ function dashboardContainerName() {
1718
+ return CONTAINER_NAME;
1719
+ }
1720
+ function dashboardImageTag() {
1721
+ return IMAGE_TAG;
1722
+ }
1723
+ function dashboardIsRunning() {
1724
+ try {
1725
+ const out = execSync7(
1726
+ `docker ps --filter "name=^${CONTAINER_NAME}$" --format "{{.Names}}"`,
1727
+ { encoding: "utf-8" }
1728
+ ).trim();
1729
+ return out === CONTAINER_NAME;
1730
+ } catch {
1731
+ return false;
1732
+ }
1733
+ }
1734
+ function registerLegsCommands(legs) {
1735
+ setupCommand(legs);
1736
+ domainCommand(legs);
1737
+ newtCommand(legs);
1738
+ restartCommand(legs);
1739
+ statusCommand(legs);
1740
+ proxyModeCommand(legs);
1741
+ }
1742
+ function registerCommands(program) {
1743
+ const legs = program.command("legs").description("Traefik, domains, and tunnels");
1744
+ registerLegsCommands(legs);
1745
+ }
1746
+ var index_default = {
1747
+ registerLegsCommands,
1748
+ registerCommands
1749
+ };
1750
+
1751
+ export {
1752
+ TraefikService,
1753
+ TunnelService,
1754
+ runLegsProxySetup,
1755
+ setupCommand,
1756
+ domainCommand,
1757
+ newtCommand,
1758
+ restartCommand,
1759
+ statusCommand,
1760
+ proxyModeCommand,
1761
+ InferenceGateway,
1762
+ refreshTraefikRoutes,
1763
+ verifyComponent,
1764
+ installDashboardContainer,
1765
+ uninstallDashboardContainer,
1766
+ dashboardContainerName,
1767
+ dashboardImageTag,
1768
+ dashboardIsRunning,
1769
+ registerLegsCommands,
1770
+ registerCommands,
1771
+ index_default
1772
+ };
1773
+ //# sourceMappingURL=chunk-23XQDB7K.js.map