@askexenow/exe-os 0.9.82 → 0.9.84

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.
@@ -0,0 +1,392 @@
1
+ # exe-os VPS stack — full production compose
2
+ #
3
+ # Services: exe-crm + crm-postgres + clickhouse + redis + exe-wiki + exed + exe-gateway
4
+ # Standard for managed customer VPSs: exe-monitor-agent reports fleet health to monitor.askexe.com.
5
+ # All image tags pinned per-client via .env (no :latest). Healthchecks on every service.
6
+ # Named volumes for state; explicit subnets; depends_on with service_healthy gates.
7
+ #
8
+ # Validate without secrets:
9
+ # cp .env.example .env && docker compose -f docker-compose.yml config
10
+ #
11
+ # Boot stack on a Lane-A-1-provisioned host:
12
+ # docker compose -f docker-compose.yml up -d
13
+ #
14
+ # nginx snippets at ../nginx/snippets/*.conf are included by the Ansible
15
+ # nginx-tls role from /etc/nginx/snippets/.
16
+
17
+ name: exe-os
18
+
19
+ services:
20
+ # ------------------------------------------------------------------
21
+ # Data layer
22
+ # ------------------------------------------------------------------
23
+
24
+ crm-postgres:
25
+ image: postgres:16.6-alpine
26
+ container_name: crm-postgres
27
+ restart: unless-stopped
28
+ env_file:
29
+ - path: .env
30
+ required: false
31
+ environment:
32
+ POSTGRES_USER: ${POSTGRES_USER:-exe}
33
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
34
+ POSTGRES_DB: ${POSTGRES_DB:-default}
35
+ PGDATA: /var/lib/postgresql/data/pgdata
36
+ volumes:
37
+ - postgres_data:/var/lib/postgresql/data
38
+ networks:
39
+ backend:
40
+ ipv4_address: 10.42.0.10
41
+ healthcheck:
42
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-exe} -d ${POSTGRES_DB:-default}"]
43
+ interval: 10s
44
+ timeout: 5s
45
+ start_period: 30s
46
+ retries: 5
47
+ logging:
48
+ driver: json-file
49
+ options: { max-size: "10m", max-file: "3" }
50
+
51
+ clickhouse:
52
+ image: clickhouse/clickhouse-server:24.8.4.13-alpine
53
+ container_name: clickhouse
54
+ restart: unless-stopped
55
+ env_file:
56
+ - path: .env
57
+ required: false
58
+ environment:
59
+ CLICKHOUSE_DB: ${CLICKHOUSE_DB:-default}
60
+ CLICKHOUSE_USER: ${CLICKHOUSE_USER:-exe}
61
+ CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is required}
62
+ CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
63
+ volumes:
64
+ - clickhouse_data:/var/lib/clickhouse
65
+ - clickhouse_logs:/var/log/clickhouse-server
66
+ ulimits:
67
+ nofile:
68
+ soft: 262144
69
+ hard: 262144
70
+ networks:
71
+ backend:
72
+ ipv4_address: 10.42.0.11
73
+ healthcheck:
74
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8123/ping"]
75
+ interval: 10s
76
+ timeout: 5s
77
+ start_period: 30s
78
+ retries: 5
79
+ logging:
80
+ driver: json-file
81
+ options: { max-size: "10m", max-file: "3" }
82
+
83
+ redis:
84
+ image: redis:7.4-alpine
85
+ container_name: redis
86
+ restart: unless-stopped
87
+ env_file:
88
+ - path: .env
89
+ required: false
90
+ command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}", "--save", "60", "1", "--appendonly", "yes"]
91
+ volumes:
92
+ - redis_data:/data
93
+ networks:
94
+ backend:
95
+ ipv4_address: 10.42.0.12
96
+ healthcheck:
97
+ test: ["CMD-SHELL", "redis-cli -a $${REDIS_PASSWORD:?REDIS_PASSWORD is required} ping | grep -q PONG"]
98
+ interval: 10s
99
+ timeout: 5s
100
+ start_period: 10s
101
+ retries: 5
102
+ logging:
103
+ driver: json-file
104
+ options: { max-size: "10m", max-file: "3" }
105
+
106
+ # ------------------------------------------------------------------
107
+ # Apps
108
+ # ------------------------------------------------------------------
109
+
110
+ exe-crm:
111
+ image: ${CRM_IMAGE_TAG:-registry.askexe.com/askexe/exe-crm:v0.9.3}
112
+ container_name: exe-crm
113
+ restart: unless-stopped
114
+ depends_on:
115
+ crm-postgres:
116
+ condition: service_healthy
117
+ clickhouse:
118
+ condition: service_healthy
119
+ redis:
120
+ condition: service_healthy
121
+ env_file:
122
+ - path: .env
123
+ required: false
124
+ environment:
125
+ NODE_ENV: production
126
+ NODE_PORT: "3000"
127
+ EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
128
+ SERVER_URL: ${CRM_SERVER_URL:-https://crm.askexe.com}
129
+ APP_SECRET: ${CRM_APP_SECRET:?CRM_APP_SECRET is required}
130
+ PG_DATABASE_URL: postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@crm-postgres:5432/${POSTGRES_DB:-default}
131
+ REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@redis:6379
132
+ CLICKHOUSE_URL: http://${CLICKHOUSE_USER:-exe}:${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is required}@clickhouse:8123/${CLICKHOUSE_DB:-default}
133
+ STORAGE_TYPE: local
134
+ STORAGE_LOCAL_PATH: /app/.local-storage
135
+ BRANDING_CONFIG_PATH: /app/branding.json
136
+ ports:
137
+ - "127.0.0.1:${CRM_HOST_PORT:-3000}:3000"
138
+ volumes:
139
+ - crm_data:/app/.local-storage
140
+ - ${BRANDING_CONFIG:-./branding.json}:/app/branding.json:ro
141
+ networks:
142
+ backend:
143
+ ipv4_address: 10.42.0.20
144
+ frontend:
145
+ ipv4_address: 10.43.0.20
146
+ healthcheck:
147
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/healthz"]
148
+ interval: 30s
149
+ timeout: 5s
150
+ start_period: 60s
151
+ retries: 5
152
+ logging:
153
+ driver: json-file
154
+ options: { max-size: "10m", max-file: "3" }
155
+
156
+ exe-crm-worker:
157
+ image: ${CRM_IMAGE_TAG:-registry.askexe.com/askexe/exe-crm:v0.9.3}
158
+ container_name: exe-crm-worker
159
+ restart: unless-stopped
160
+ command: ["yarn", "worker:prod"]
161
+ depends_on:
162
+ crm-postgres:
163
+ condition: service_healthy
164
+ clickhouse:
165
+ condition: service_healthy
166
+ redis:
167
+ condition: service_healthy
168
+ exe-crm:
169
+ condition: service_healthy
170
+ env_file:
171
+ - path: .env
172
+ required: false
173
+ environment:
174
+ NODE_ENV: production
175
+ EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
176
+ SERVER_URL: ${CRM_SERVER_URL:-https://crm.askexe.com}
177
+ APP_SECRET: ${CRM_APP_SECRET:?CRM_APP_SECRET is required}
178
+ PG_DATABASE_URL: postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@crm-postgres:5432/${POSTGRES_DB:-default}
179
+ REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@redis:6379
180
+ CLICKHOUSE_URL: http://${CLICKHOUSE_USER:-exe}:${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD is required}@clickhouse:8123/${CLICKHOUSE_DB:-default}
181
+ STORAGE_TYPE: local
182
+ STORAGE_LOCAL_PATH: /app/.local-storage
183
+ BRANDING_CONFIG_PATH: /app/branding.json
184
+ DISABLE_DB_MIGRATIONS: "true"
185
+ DISABLE_CRON_JOBS_REGISTRATION: "true"
186
+ volumes:
187
+ - crm_data:/app/.local-storage
188
+ - ${BRANDING_CONFIG:-./branding.json}:/app/branding.json:ro
189
+ networks:
190
+ backend:
191
+ ipv4_address: 10.42.0.24
192
+ logging:
193
+ driver: json-file
194
+ options: { max-size: "10m", max-file: "3" }
195
+
196
+ exe-wiki:
197
+ image: ${WIKI_IMAGE_TAG:-registry.askexe.com/askexe/exe-wiki:v0.9.3}
198
+ container_name: exe-wiki
199
+ restart: unless-stopped
200
+ depends_on:
201
+ crm-postgres:
202
+ condition: service_healthy
203
+ env_file:
204
+ - path: .env
205
+ required: false
206
+ environment:
207
+ NODE_ENV: production
208
+ SERVER_PORT: "3001"
209
+ EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
210
+ STORAGE_DIR: /app/server/storage
211
+ DATABASE_URL: postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@crm-postgres:5432/${POSTGRES_DB:-default}?schema=${WIKI_DB_SCHEMA:-wiki}
212
+ AUTH_TOKEN: ${WIKI_AUTH_TOKEN:?WIKI_AUTH_TOKEN is required}
213
+ JWT_SECRET: ${WIKI_JWT_SECRET:?WIKI_JWT_SECRET is required}
214
+ SIG_KEY: ${WIKI_SIG_KEY:?WIKI_SIG_KEY is required}
215
+ SIG_SALT: ${WIKI_SIG_SALT:?WIKI_SIG_SALT is required}
216
+ VECTOR_DB: ${WIKI_VECTOR_DB:-postgres}
217
+ BRANDING_CONFIG_PATH: /app/branding.json
218
+ ports:
219
+ - "127.0.0.1:${WIKI_HOST_PORT:-3001}:3001"
220
+ volumes:
221
+ - wiki_data:/app/server/storage
222
+ - ${BRANDING_CONFIG:-./branding.json}:/app/branding.json:ro
223
+ networks:
224
+ backend:
225
+ ipv4_address: 10.42.0.21
226
+ frontend:
227
+ ipv4_address: 10.43.0.21
228
+ healthcheck:
229
+ test: ["CMD", "node", "-e", "const http=require('http');const r=http.get('http://127.0.0.1:3001/api/ping',s=>{process.exit(s.statusCode===200?0:1)});r.on('error',()=>process.exit(1));r.setTimeout(4000,()=>process.exit(1))"]
230
+ interval: 30s
231
+ timeout: 5s
232
+ start_period: 60s
233
+ retries: 5
234
+ logging:
235
+ driver: json-file
236
+ options: { max-size: "10m", max-file: "3" }
237
+
238
+ exed:
239
+ image: ${EXED_IMAGE_TAG:-registry.askexe.com/askexe/exed:v0.9.7}
240
+ container_name: exed
241
+ restart: unless-stopped
242
+ env_file:
243
+ - path: .env
244
+ required: false
245
+ environment:
246
+ NODE_ENV: production
247
+ EXED_PORT: "8765"
248
+ EXED_HOST: 0.0.0.0
249
+ EXED_MCP_TOKEN: ${EXED_MCP_TOKEN:?EXED_MCP_TOKEN is required}
250
+ EXED_DEVICE_ID: ${EXED_DEVICE_ID:-vps-default}
251
+ EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
252
+ DATABASE_URL: ${EXED_DATABASE_URL:-postgres://${POSTGRES_USER:-exe}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@crm-postgres:5432/${POSTGRES_DB:-default}}
253
+ EXE_CLOUD_SYNC_TO_POSTGRES: ${EXE_CLOUD_SYNC_TO_POSTGRES:-true}
254
+ EXE_RSS_WARN_MB: ${EXE_RSS_WARN_MB:-6144}
255
+ EXE_RSS_RESTART_MB: ${EXE_RSS_RESTART_MB:-8192}
256
+ EXE_OS_DIR: /home/exed/.exe-os
257
+ volumes:
258
+ - exed_data:/home/exed/.exe-os
259
+ ports:
260
+ - "127.0.0.1:${EXED_HOST_PORT:-8765}:8765"
261
+ networks:
262
+ backend:
263
+ ipv4_address: 10.42.0.22
264
+ healthcheck:
265
+ test: ["CMD", "node", "-e", "const http=require('http');const r=http.get('http://127.0.0.1:8765/health',s=>{process.exit(s.statusCode===200?0:1)});r.on('error',()=>process.exit(1));r.setTimeout(4000,()=>process.exit(1))"]
266
+ interval: 30s
267
+ timeout: 5s
268
+ start_period: 30s
269
+ retries: 5
270
+ logging:
271
+ driver: json-file
272
+ options: { max-size: "10m", max-file: "3" }
273
+
274
+ exe-gateway:
275
+ image: ${GATEWAY_IMAGE_TAG:-registry.askexe.com/askexe/exe-gateway:v0.9.3}
276
+ container_name: exe-gateway
277
+ restart: unless-stopped
278
+ depends_on:
279
+ exed:
280
+ condition: service_healthy
281
+ env_file:
282
+ - path: .env
283
+ required: false
284
+ environment:
285
+ NODE_ENV: production
286
+ EXE_GATEWAY_HOME: /data
287
+ EXE_GATEWAY_CONFIG: /data/gateway.json
288
+ EXE_GATEWAY_PORT: "3100"
289
+ EXE_GATEWAY_HOST: 0.0.0.0
290
+ EXE_GATEWAY_AUTH_TOKEN: ${EXE_GATEWAY_AUTH_TOKEN:?EXE_GATEWAY_AUTH_TOKEN is required}
291
+ EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN: ${EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN:?EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN is required}
292
+ EXE_GATEWAY_WS_RELAY_ENABLED: "true"
293
+ EXE_GATEWAY_WS_RELAY_HOST: 0.0.0.0
294
+ EXE_GATEWAY_WS_RELAY_PORT: "3101"
295
+ EXE_GATEWAY_WS_RELAY_AUTH_TOKEN: ${EXE_GATEWAY_WS_RELAY_AUTH_TOKEN:?EXE_GATEWAY_WS_RELAY_AUTH_TOKEN is required}
296
+ WHATSAPP_ACCESS_TOKEN: ${WHATSAPP_ACCESS_TOKEN:-}
297
+ API_ROUTER_URL: ${API_ROUTER_URL:-https://gateway.askexe.com}
298
+ API_ROUTER_KEY: ${API_ROUTER_KEY:-}
299
+ BYOK_ENABLED: ${BYOK_ENABLED:-false}
300
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
301
+ EXE_LICENSE_KEY: ${EXE_LICENSE_KEY:?EXE_LICENSE_KEY is required — purchase at https://askexe.com}
302
+ ports:
303
+ - "127.0.0.1:${GATEWAY_HTTP_HOST_PORT:-3100}:3100"
304
+ - "127.0.0.1:${GATEWAY_WS_HOST_PORT:-3101}:3101"
305
+ volumes:
306
+ - gateway_data:/data
307
+ - ./gateway.json:/data/gateway.json:ro
308
+ networks:
309
+ backend:
310
+ ipv4_address: 10.42.0.23
311
+ frontend:
312
+ ipv4_address: 10.43.0.23
313
+ healthcheck:
314
+ test: ["CMD", "node", "-e", "const http=require('http');const r=http.get('http://localhost:3100/health',s=>{process.exit(s.statusCode===200?0:1)});r.on('error',()=>process.exit(1));r.setTimeout(4000,()=>process.exit(1))"]
315
+ interval: 30s
316
+ timeout: 5s
317
+ start_period: 30s
318
+ retries: 5
319
+ logging:
320
+ driver: json-file
321
+ options: { max-size: "10m", max-file: "3" }
322
+
323
+ exe-monitor-agent:
324
+ image: ${MONITOR_AGENT_IMAGE_TAG:-registry.askexe.com/askexe/exe-monitor-agent:v0.9.3}
325
+ container_name: exe-monitor-agent
326
+ restart: unless-stopped
327
+ environment:
328
+ HUB_URL: ${MONITOR_HUB_URL:-https://monitor.askexe.com}
329
+ TOKEN: ${MONITOR_AGENT_TOKEN:?MONITOR_AGENT_TOKEN is required — create Hygo system in monitor.askexe.com}
330
+ KEY: ${MONITOR_AGENT_KEY:?MONITOR_AGENT_KEY is required — copy public key from monitor.askexe.com}
331
+ LISTEN: ${MONITOR_AGENT_LISTEN:-:45876}
332
+ volumes:
333
+ - /var/run/docker.sock:/var/run/docker.sock:ro
334
+ - /proc:/host/proc:ro
335
+ - /sys:/host/sys:ro
336
+ - /etc/os-release:/host/etc/os-release:ro
337
+ - monitor_agent_data:/var/lib/beszel-agent
338
+ networks:
339
+ backend:
340
+ ipv4_address: 10.42.0.30
341
+ healthcheck:
342
+ test: ["CMD", "/agent", "health"]
343
+ interval: 30s
344
+ timeout: 5s
345
+ start_period: 30s
346
+ retries: 5
347
+ logging:
348
+ driver: json-file
349
+ options: { max-size: "10m", max-file: "3" }
350
+
351
+ # ------------------------------------------------------------------
352
+ # Volumes
353
+ # ------------------------------------------------------------------
354
+ volumes:
355
+ postgres_data:
356
+ driver: local
357
+ clickhouse_data:
358
+ driver: local
359
+ clickhouse_logs:
360
+ driver: local
361
+ redis_data:
362
+ driver: local
363
+ crm_data:
364
+ driver: local
365
+ wiki_data:
366
+ driver: local
367
+ exed_data:
368
+ driver: local
369
+ gateway_data:
370
+ driver: local
371
+ monitor_agent_data:
372
+ driver: local
373
+
374
+ # ------------------------------------------------------------------
375
+ # Networks
376
+ # ------------------------------------------------------------------
377
+ # backend — internal service-to-service traffic (DBs + apps)
378
+ # frontend — exposed to host nginx via published ports on apps that
379
+ # terminate user traffic (CRM, wiki, gateway). DBs never touch this net.
380
+ networks:
381
+ backend:
382
+ driver: bridge
383
+ ipam:
384
+ driver: default
385
+ config:
386
+ - subnet: 10.42.0.0/24
387
+ frontend:
388
+ driver: bridge
389
+ ipam:
390
+ driver: default
391
+ config:
392
+ - subnet: 10.43.0.0/24
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,252 @@
1
+ import { randomBytes } from "node:crypto";
2
+ const REGISTRY = "ghcr.io/askexe";
3
+
4
+ // v0.9.x is the private/customer pilot stack line. Component repos keep their
5
+ // own package semver internally, but customer deployments use the tested stack
6
+ // image alias so one stack manifest can pin the whole release. exe-os/exed is
7
+ // the exception because the runtime package version is the orchestrator release.
8
+ const STACK_IMAGE_TAG = "v0.9.2";
9
+ const EXED_RELEASE_TAG = "v0.9.2";
10
+
11
+ const POSTGRES_USER = "exe";
12
+ const POSTGRES_DB = "default";
13
+ const CLICKHOUSE_DB = "default";
14
+ const CLICKHOUSE_USER = "exe";
15
+ export const CRM_IMAGE_TAG = `${REGISTRY}/exe-crm:${STACK_IMAGE_TAG}`;
16
+ const CRM_HOST_PORT = "3000";
17
+ export const WIKI_IMAGE_TAG = `${REGISTRY}/exe-wiki:${STACK_IMAGE_TAG}`;
18
+ const WIKI_DB_SCHEMA = "wiki";
19
+ const WIKI_VECTOR_DB = "postgres";
20
+ const WIKI_HOST_PORT = "3001";
21
+ export const EXED_IMAGE_TAG = `${REGISTRY}/exed:${EXED_RELEASE_TAG}`;
22
+ export const GATEWAY_IMAGE_TAG = `${REGISTRY}/exe-gateway:${STACK_IMAGE_TAG}`;
23
+ export const MONITOR_AGENT_IMAGE_TAG = `${REGISTRY}/exe-monitor-agent:${STACK_IMAGE_TAG}`;
24
+ const GATEWAY_HTTP_HOST_PORT = "3100";
25
+ const GATEWAY_WS_HOST_PORT = "3101";
26
+ const RANDOM_SECRET_16 = 16;
27
+ const RANDOM_SECRET_32 = 32;
28
+ const RANDOM_SECRET_48 = 48;
29
+ const LICENSE_KEY_COMMENT = "# injected by deploy_client";
30
+ const MANUAL_VALUE_COMMENT = "# SET_MANUALLY";
31
+ const EXAMPLE_LICENSE_KEY = "CHANGEME_EXE_LICENSE_KEY";
32
+ const DEFAULT_API_ROUTER_URL = "https://gateway.askexe.com";
33
+ const DEFAULT_MONITOR_HUB_URL = "https://monitor.askexe.com";
34
+ const DEFAULT_MONITOR_AGENT_LISTEN = ":45876";
35
+
36
+ export interface GenerateEnvOptions {
37
+ clientName: string;
38
+ domain: string;
39
+ licenseKey?: string;
40
+ }
41
+
42
+ export function generateEnv(options: GenerateEnvOptions): string {
43
+ const normalizedDomain = normalizeDomain(options.domain);
44
+ const normalizedClientName = normalizeClientName(options.clientName);
45
+ const gatewayWsAuthToken = randomSecret(RANDOM_SECRET_48);
46
+
47
+ return joinEnvLines([
48
+ "# --- Data Layer ---",
49
+ `POSTGRES_USER=${POSTGRES_USER}`,
50
+ `POSTGRES_PASSWORD=${randomSecret(RANDOM_SECRET_32)}`,
51
+ `POSTGRES_DB=${POSTGRES_DB}`,
52
+ "",
53
+ `CLICKHOUSE_DB=${CLICKHOUSE_DB}`,
54
+ `CLICKHOUSE_USER=${CLICKHOUSE_USER}`,
55
+ `CLICKHOUSE_PASSWORD=${randomSecret(RANDOM_SECRET_32)}`,
56
+ "",
57
+ `REDIS_PASSWORD=${randomSecret(RANDOM_SECRET_32)}`,
58
+ "",
59
+ "# --- CRM ---",
60
+ `CRM_IMAGE_TAG=${CRM_IMAGE_TAG}`,
61
+ `CRM_SERVER_URL=https://${normalizedDomain}`,
62
+ `CRM_APP_SECRET=${randomSecret(RANDOM_SECRET_48)}`,
63
+ `CRM_HOST_PORT=${CRM_HOST_PORT}`,
64
+ "",
65
+ "# --- Wiki ---",
66
+ `WIKI_IMAGE_TAG=${WIKI_IMAGE_TAG}`,
67
+ `WIKI_DB_SCHEMA=${WIKI_DB_SCHEMA}`,
68
+ `WIKI_VECTOR_DB=${WIKI_VECTOR_DB}`,
69
+ `WIKI_AUTH_TOKEN=${randomSecret(RANDOM_SECRET_48)}`,
70
+ `WIKI_JWT_SECRET=${randomSecret(RANDOM_SECRET_48)}`,
71
+ `WIKI_SIG_KEY=${randomSecret(RANDOM_SECRET_48)}`,
72
+ `WIKI_SIG_SALT=${randomSecret(RANDOM_SECRET_16)}`,
73
+ `WIKI_HOST_PORT=${WIKI_HOST_PORT}`,
74
+ "",
75
+ "# --- exed ---",
76
+ `EXED_IMAGE_TAG=${EXED_IMAGE_TAG}`,
77
+ `EXED_MCP_TOKEN=${randomSecret(RANDOM_SECRET_48)}`,
78
+ `EXED_DEVICE_ID=vps-${normalizedClientName}`,
79
+ "# VPS-only: enables cloud/local SQLite -> exe-db Postgres projection.",
80
+ "EXE_CLOUD_SYNC_TO_POSTGRES=true",
81
+ "",
82
+ "# --- Gateway ---",
83
+ `GATEWAY_IMAGE_TAG=${GATEWAY_IMAGE_TAG}`,
84
+ `EXE_GATEWAY_AUTH_TOKEN=${randomSecret(RANDOM_SECRET_48)}`,
85
+ `EXE_GATEWAY_WS_RELAY_AUTH_TOKEN=${gatewayWsAuthToken}`,
86
+ `EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN=${randomSecret(RANDOM_SECRET_32)}`,
87
+ MANUAL_VALUE_COMMENT,
88
+ "WHATSAPP_ACCESS_TOKEN=",
89
+ `API_ROUTER_URL=${DEFAULT_API_ROUTER_URL}`,
90
+ `API_ROUTER_KEY=exe_rk_${randomSecret(48)}`,
91
+ "# BYOK: to use your own API keys instead of the Exe API Router,",
92
+ "# set BYOK_ENABLED=true and provide ANTHROPIC_API_KEY below.",
93
+ "# BYOK_ENABLED=false",
94
+ "# ANTHROPIC_API_KEY=",
95
+ `GATEWAY_HTTP_HOST_PORT=${GATEWAY_HTTP_HOST_PORT}`,
96
+ `GATEWAY_WS_HOST_PORT=${GATEWAY_WS_HOST_PORT}`,
97
+ "",
98
+ "# --- Monitoring agent (standard for managed customer VPSs) ---",
99
+ `MONITOR_AGENT_IMAGE_TAG=${MONITOR_AGENT_IMAGE_TAG}`,
100
+ `MONITOR_HUB_URL=${DEFAULT_MONITOR_HUB_URL}`,
101
+ "MONITOR_AGENT_TOKEN=CHANGEME_MONITOR_AGENT_TOKEN_FROM_MONITOR_HUB",
102
+ "MONITOR_AGENT_KEY=CHANGEME_MONITOR_AGENT_PUBLIC_KEY_FROM_MONITOR_HUB",
103
+ `MONITOR_AGENT_LISTEN=${DEFAULT_MONITOR_AGENT_LISTEN}`,
104
+ "",
105
+ "# --- License ---",
106
+ LICENSE_KEY_COMMENT,
107
+ `EXE_LICENSE_KEY=${options.licenseKey ?? EXAMPLE_LICENSE_KEY}`,
108
+ ]);
109
+ }
110
+
111
+ export function generateExampleEnv(): string {
112
+ return joinEnvLines([
113
+ "# exe-os VPS stack — example environment variables",
114
+ "# Copy to .env before deployment and replace every CHANGEME_* value.",
115
+ "# Values under # SET_MANUALLY must be provided by the operator.",
116
+ "",
117
+ "# --- Data Layer ---",
118
+ `POSTGRES_USER=${POSTGRES_USER}`,
119
+ "POSTGRES_PASSWORD=CHANGEME_POSTGRES_PASSWORD",
120
+ `POSTGRES_DB=${POSTGRES_DB}`,
121
+ "",
122
+ `CLICKHOUSE_DB=${CLICKHOUSE_DB}`,
123
+ `CLICKHOUSE_USER=${CLICKHOUSE_USER}`,
124
+ "CLICKHOUSE_PASSWORD=CHANGEME_CLICKHOUSE_PASSWORD",
125
+ "",
126
+ "REDIS_PASSWORD=CHANGEME_REDIS_PASSWORD",
127
+ "",
128
+ "# --- CRM ---",
129
+ `CRM_IMAGE_TAG=${CRM_IMAGE_TAG}`,
130
+ "CRM_SERVER_URL=https://CHANGEME_DOMAIN",
131
+ "CRM_APP_SECRET=CHANGEME_CRM_APP_SECRET",
132
+ `CRM_HOST_PORT=${CRM_HOST_PORT}`,
133
+ "",
134
+ "# --- Wiki ---",
135
+ `WIKI_IMAGE_TAG=${WIKI_IMAGE_TAG}`,
136
+ `WIKI_DB_SCHEMA=${WIKI_DB_SCHEMA}`,
137
+ `WIKI_VECTOR_DB=${WIKI_VECTOR_DB}`,
138
+ "WIKI_AUTH_TOKEN=CHANGEME_WIKI_AUTH_TOKEN",
139
+ "WIKI_JWT_SECRET=CHANGEME_WIKI_JWT_SECRET",
140
+ "WIKI_SIG_KEY=CHANGEME_WIKI_SIG_KEY",
141
+ "WIKI_SIG_SALT=CHANGEME_WIKI_SIG_SALT",
142
+ `WIKI_HOST_PORT=${WIKI_HOST_PORT}`,
143
+ "",
144
+ "# --- exed ---",
145
+ `EXED_IMAGE_TAG=${EXED_IMAGE_TAG}`,
146
+ "EXED_MCP_TOKEN=CHANGEME_EXED_MCP_TOKEN",
147
+ "EXED_DEVICE_ID=vps-default",
148
+ "# VPS-only: enables cloud/local SQLite -> exe-db Postgres projection.",
149
+ "# Keep false on laptops/dev boxes.",
150
+ "EXE_CLOUD_SYNC_TO_POSTGRES=true",
151
+ "",
152
+ "# --- Gateway ---",
153
+ `GATEWAY_IMAGE_TAG=${GATEWAY_IMAGE_TAG}`,
154
+ "EXE_GATEWAY_AUTH_TOKEN=CHANGEME_EXE_GATEWAY_AUTH_TOKEN",
155
+ "EXE_GATEWAY_WS_RELAY_AUTH_TOKEN=CHANGEME_EXE_GATEWAY_WS_RELAY_AUTH_TOKEN",
156
+ "EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN=CHANGEME_EXE_GATEWAY_WHATSAPP_VERIFY_TOKEN",
157
+ MANUAL_VALUE_COMMENT,
158
+ "WHATSAPP_ACCESS_TOKEN=",
159
+ `API_ROUTER_URL=${DEFAULT_API_ROUTER_URL}`,
160
+ "API_ROUTER_KEY=exe_rk_CHANGEME_API_ROUTER_KEY",
161
+ "# BYOK: to use your own API keys instead of the Exe API Router,",
162
+ "# set BYOK_ENABLED=true and provide ANTHROPIC_API_KEY below.",
163
+ "# BYOK_ENABLED=false",
164
+ "# ANTHROPIC_API_KEY=CHANGEME_ANTHROPIC_API_KEY",
165
+ `GATEWAY_HTTP_HOST_PORT=${GATEWAY_HTTP_HOST_PORT}`,
166
+ `GATEWAY_WS_HOST_PORT=${GATEWAY_WS_HOST_PORT}`,
167
+ "",
168
+ "# --- Monitoring agent (standard for managed customer VPSs) ---",
169
+ `MONITOR_AGENT_IMAGE_TAG=${MONITOR_AGENT_IMAGE_TAG}`,
170
+ `MONITOR_HUB_URL=${DEFAULT_MONITOR_HUB_URL}`,
171
+ "# Values copied from monitor.askexe.com when adding a new system.",
172
+ "MONITOR_AGENT_TOKEN=CHANGEME_MONITOR_AGENT_TOKEN_FROM_MONITOR_HUB",
173
+ "MONITOR_AGENT_KEY=CHANGEME_MONITOR_AGENT_PUBLIC_KEY_FROM_MONITOR_HUB",
174
+ `MONITOR_AGENT_LISTEN=${DEFAULT_MONITOR_AGENT_LISTEN}`,
175
+ "",
176
+ "# --- AskExe central monitoring hub auth (AskExe-owned infra only) ---",
177
+ "# Customer VPSs normally run only MONITOR_AGENT_* above. These hub values are",
178
+ "# for monitor.askexe.com on exe-db-jkt.",
179
+ "MONITOR_HUB_PUBLIC_URL=https://monitor.askexe.com",
180
+ "MONITOR_HUB_SOURCE_DIR=/opt/exe-monitor",
181
+ "MONITOR_HUB_HOST_PORT=8090",
182
+ "MONITOR_HUB_DATA_DIR=/opt/exe-monitor-data",
183
+ "MONITOR_TRUSTED_AUTH_HEADER=X-AskExe-User-Email",
184
+ "# Keep false during bootstrap; flip true after the GoTrue auth proxy is live.",
185
+ "MONITOR_DISABLE_PASSWORD_AUTH=false",
186
+ "MONITOR_USER_CREATION=true",
187
+ "MONITOR_SHARE_ALL_SYSTEMS=false",
188
+ "",
189
+ "# --- License ---",
190
+ LICENSE_KEY_COMMENT,
191
+ `EXE_LICENSE_KEY=${EXAMPLE_LICENSE_KEY}`,
192
+ ]);
193
+ }
194
+
195
+ export function randomSecret(length: number): string {
196
+ return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
197
+ }
198
+
199
+ function normalizeDomain(domain: string): string {
200
+ return domain.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
201
+ }
202
+
203
+ function normalizeClientName(clientName: string): string {
204
+ return clientName
205
+ .trim()
206
+ .toLowerCase()
207
+ .replace(/[^a-z0-9-]/g, "-")
208
+ .replace(/-+/g, "-")
209
+ .replace(/^-|-$/g, "") || "client";
210
+ }
211
+
212
+ function joinEnvLines(lines: string[]): string {
213
+ return `${lines.join("\n")}\n`;
214
+ }
215
+
216
+ function printUsage(): void {
217
+ process.stderr.write(
218
+ "Usage:\n" +
219
+ " npx tsx deploy/compose/generate-env.ts <client-name> <domain> [license-key]\n" +
220
+ " npx tsx deploy/compose/generate-env.ts --example\n",
221
+ );
222
+ }
223
+
224
+ function isCliInvocation(): boolean {
225
+ return process.argv[1]?.endsWith("generate-env.ts") === true || process.argv[1]?.endsWith("generate-env.js") === true;
226
+ }
227
+
228
+ function main(): void {
229
+ const args = process.argv.slice(2);
230
+
231
+ if (args.includes("--help") || args.includes("-h")) {
232
+ printUsage();
233
+ process.exit(0);
234
+ }
235
+
236
+ if (args.includes("--example")) {
237
+ process.stdout.write(generateExampleEnv());
238
+ return;
239
+ }
240
+
241
+ const [clientName, domain, licenseKey] = args;
242
+ if (!clientName || !domain) {
243
+ printUsage();
244
+ process.exit(1);
245
+ }
246
+
247
+ process.stdout.write(generateEnv({ clientName, domain, licenseKey }));
248
+ }
249
+
250
+ if (isCliInvocation()) {
251
+ main();
252
+ }