@intranefr/superbackend 1.5.1 ā 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +5 -2
- package/src/controllers/admin.controller.js +79 -6
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +243 -74
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware.js +195 -34
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/CacheEntry.js +1 -1
- package/src/models/ConsoleLog.js +1 -1
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/Markdown.js +75 -0
- package/src/models/RateLimitCounter.js +1 -1
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminConsoleManager.routes.js +1 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/blogCronsBootstrap.service.js +7 -6
- package/src/services/consoleManager.service.js +56 -18
- package/src/services/consoleOverride.service.js +1 -0
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/jsonConfigs.service.js +24 -12
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +514 -23
- package/src/services/telegram.service.js +130 -0
- package/src/utils/rbac/rightsRegistry.js +4 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +63 -12
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +817 -6
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +4 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/src/middleware/internalCronAuth.js +0 -29
package/manage.js
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const readline = require("readline");
|
|
7
|
+
|
|
8
|
+
async function selectEnvFile() {
|
|
9
|
+
if (process.env.ENV_FILE) {
|
|
10
|
+
return process.env.ENV_FILE;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const files = fs.readdirSync(process.cwd());
|
|
14
|
+
const envFiles = files.filter(
|
|
15
|
+
(f) => f.startsWith(".env") && f !== ".env.example",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (envFiles.length <= 1) {
|
|
19
|
+
return envFiles[0] || ".env";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log("Multiple environment files detected:");
|
|
23
|
+
envFiles.forEach((file, idx) => {
|
|
24
|
+
console.log(`${idx + 1}. ${file}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const choice = await new Promise((resolve) => {
|
|
33
|
+
rl.question(
|
|
34
|
+
`Select an environment file (1-${envFiles.length}, default 1): `,
|
|
35
|
+
(answer) => {
|
|
36
|
+
resolve(answer.trim());
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
rl.close();
|
|
42
|
+
|
|
43
|
+
const index = parseInt(choice) - 1;
|
|
44
|
+
if (isNaN(index) || index < 0 || index >= envFiles.length) {
|
|
45
|
+
if (choice !== "") {
|
|
46
|
+
console.log(`Invalid selection, using ${envFiles[0]}`);
|
|
47
|
+
}
|
|
48
|
+
return envFiles[0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return envFiles[index];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const envFile = await selectEnvFile();
|
|
56
|
+
process.env.ENV_FILE = envFile;
|
|
57
|
+
|
|
58
|
+
// Load environment variables
|
|
59
|
+
require("dotenv").config({
|
|
60
|
+
path: envFile,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Default values for environment variables
|
|
64
|
+
const config = {
|
|
65
|
+
REMOTE_USER: process.env.REMOTE_HOST_USER || "ubuntu",
|
|
66
|
+
REMOTE_HOST: process.env.REMOTE_HOST,
|
|
67
|
+
REMOTE_PORT: process.env.REMOTE_HOST_PORT || "22",
|
|
68
|
+
REMOTE_PATH: process.env.REMOTE_HOST_PATH || "~/docker/mufc-booking-v2",
|
|
69
|
+
LOCAL_PATH: process.cwd(),
|
|
70
|
+
REMOTE_SYNC_EXCLUDES:
|
|
71
|
+
process.env.REMOTE_SYNC_EXCLUDES || "frontend/node_modules",
|
|
72
|
+
APP_NAME: process.env.APP_NAME,
|
|
73
|
+
MODE: process.env.MODE || "staging",
|
|
74
|
+
REMOTE_DOMAIN_CONFIG_FILENAME: process.env.REMOTE_DOMAIN_CONFIG_FILENAME,
|
|
75
|
+
DOMAIN_REMOTE_USER: "root",
|
|
76
|
+
DOMAIN_REMOTE_HOST: process.env.REMOTE_DOMAIN_HOST,
|
|
77
|
+
DOMAIN_REMOTE_PORT: process.env.REMOTE_DOMAIN_PORT || "22",
|
|
78
|
+
DOMAIN_REMOTE_TRAEFIK_PATH: "/data/coolify/proxy/dynamic",
|
|
79
|
+
PROXY_FILE: `.manage-proxy-file-${process.env.MODE || "staging"}.yml`,
|
|
80
|
+
REMOTE_SERVICE_IP: process.env.REMOTE_SERVICE_IP,
|
|
81
|
+
PUBLISHED_DOMAIN: process.env.PUBLISHED_DOMAIN,
|
|
82
|
+
COMPOSE_FILE: process.env.COMPOSE_FILE,
|
|
83
|
+
DOCKER_PLATFORM: process.env.DOCKER_PLATFORM,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
console.log("Env file: ", process.env.ENV_FILE);
|
|
87
|
+
console.log("Mode: ", process.env.MODE || "staging");
|
|
88
|
+
console.log("Will use compose file: ", config.COMPOSE_FILE);
|
|
89
|
+
|
|
90
|
+
const rl = readline.createInterface({
|
|
91
|
+
input: process.stdin,
|
|
92
|
+
output: process.stdout,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function prompt(question) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
rl.question(question, (answer) => {
|
|
98
|
+
resolve(answer);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findComposeFiles() {
|
|
104
|
+
const files = [
|
|
105
|
+
"compose.prod.yml",
|
|
106
|
+
"docker-compose.yml",
|
|
107
|
+
"docker-compose.yaml",
|
|
108
|
+
"compose.yml",
|
|
109
|
+
"compose.yaml",
|
|
110
|
+
"compose.image.yml",
|
|
111
|
+
];
|
|
112
|
+
const found = [];
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
if (fs.existsSync(path.join(config.LOCAL_PATH, file))) {
|
|
115
|
+
found.push(file);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function detectComposeFile() {
|
|
122
|
+
if (config.COMPOSE_FILE) return config.COMPOSE_FILE;
|
|
123
|
+
|
|
124
|
+
const files = findComposeFiles();
|
|
125
|
+
return files.length > 0 ? files[0] : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function detectRemoteDockerComposeCommand() {
|
|
129
|
+
try {
|
|
130
|
+
// Try docker compose first (preferred) on remote server
|
|
131
|
+
execSync(`ssh -p ${config.REMOTE_PORT} ${config.REMOTE_USER}@${config.REMOTE_HOST} "docker compose version"`, { stdio: 'ignore' });
|
|
132
|
+
return 'docker compose';
|
|
133
|
+
} catch (error) {
|
|
134
|
+
try {
|
|
135
|
+
// Fallback to docker-compose on remote server
|
|
136
|
+
execSync(`ssh -p ${config.REMOTE_PORT} ${config.REMOTE_USER}@${config.REMOTE_HOST} "docker-compose version"`, { stdio: 'ignore' });
|
|
137
|
+
return 'docker-compose';
|
|
138
|
+
} catch (error2) {
|
|
139
|
+
console.error('ā Error: Neither docker compose nor docker-compose is available on remote server');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function detectLocalDockerComposeCommand() {
|
|
146
|
+
try {
|
|
147
|
+
// Try docker compose first (preferred) locally
|
|
148
|
+
execSync('docker compose version', { stdio: 'ignore' });
|
|
149
|
+
return 'docker compose';
|
|
150
|
+
} catch (error) {
|
|
151
|
+
try {
|
|
152
|
+
// Fallback to docker-compose locally
|
|
153
|
+
execSync('docker-compose version', { stdio: 'ignore' });
|
|
154
|
+
return 'docker-compose';
|
|
155
|
+
} catch (error2) {
|
|
156
|
+
console.error('ā Error: Neither docker compose nor docker-compose is available locally');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validateEnvVars() {
|
|
163
|
+
const missing = [];
|
|
164
|
+
|
|
165
|
+
if (!config.REMOTE_HOST) missing.push("REMOTE_HOST");
|
|
166
|
+
if (!config.REMOTE_USER) missing.push("REMOTE_USER");
|
|
167
|
+
|
|
168
|
+
if (missing.length > 0) {
|
|
169
|
+
console.error(
|
|
170
|
+
`ā Error: Missing environment variables: ${missing.join(", ")}`,
|
|
171
|
+
);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateDomainEnvVars() {
|
|
179
|
+
const missing = [];
|
|
180
|
+
|
|
181
|
+
if (!config.DOMAIN_REMOTE_HOST) missing.push("REMOTE_DOMAIN_HOST");
|
|
182
|
+
if (!config.REMOTE_SERVICE_IP) missing.push("REMOTE_SERVICE_IP");
|
|
183
|
+
if (!config.PUBLISHED_DOMAIN) missing.push("PUBLISHED_DOMAIN");
|
|
184
|
+
|
|
185
|
+
if (missing.length > 0) {
|
|
186
|
+
console.error(
|
|
187
|
+
`ā Error: Missing environment variables: ${missing.join(", ")}`,
|
|
188
|
+
);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const proxyFile = path.join(config.LOCAL_PATH, config.PROXY_FILE);
|
|
193
|
+
if (!fs.existsSync(proxyFile)) {
|
|
194
|
+
console.error(
|
|
195
|
+
`ā Error: ${config.PROXY_FILE} not found in ${config.LOCAL_PATH}`,
|
|
196
|
+
);
|
|
197
|
+
console.error(
|
|
198
|
+
"Please create the proxy file first using 'node manage.js proxy'",
|
|
199
|
+
);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function followLogs() {
|
|
207
|
+
console.log("š Following logs on remote server...");
|
|
208
|
+
|
|
209
|
+
const composeFile = detectComposeFile();
|
|
210
|
+
if (!composeFile) {
|
|
211
|
+
console.error(
|
|
212
|
+
"ā Error: No compose file found locally. Please provide one via COMPOSE_FILE env var.",
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`Using compose file: ${composeFile}`);
|
|
218
|
+
|
|
219
|
+
const dockerCmd = detectRemoteDockerComposeCommand();
|
|
220
|
+
if (!dockerCmd) {
|
|
221
|
+
console.error('ā Error: Docker compose command not available on remote server');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const sshCmd = `ssh -t -p ${config.REMOTE_PORT} ${config.REMOTE_USER}@${config.REMOTE_HOST} "cd ${config.REMOTE_PATH} && ${dockerCmd} -f ${composeFile} logs -f app automation-api"`;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
execSync(sshCmd, { stdio: "inherit" });
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error("ā Error following logs");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function deployApp() {
|
|
235
|
+
console.log("Starting deployment to remote server...");
|
|
236
|
+
|
|
237
|
+
if (!validateEnvVars()) return;
|
|
238
|
+
|
|
239
|
+
const composeFile = detectComposeFile();
|
|
240
|
+
if (!composeFile) {
|
|
241
|
+
console.error(
|
|
242
|
+
"ā Error: No compose file found locally. Please provide one via COMPOSE_FILE env var.",
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(
|
|
248
|
+
`š§ Ensuring remote directory exists at ${config.REMOTE_PORT} ${config.REMOTE_USER}:${config.REMOTE_HOST}:${config.REMOTE_PATH}...`,
|
|
249
|
+
);
|
|
250
|
+
execSync(
|
|
251
|
+
`ssh -p ${config.REMOTE_PORT} ${config.REMOTE_USER}@${config.REMOTE_HOST} "mkdir -p ${config.REMOTE_PATH}"`,
|
|
252
|
+
{ stdio: "inherit" },
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
console.log(`š¦ Syncing local files from ${config.LOCAL_PATH} to remote...`);
|
|
256
|
+
|
|
257
|
+
const excludes = config.REMOTE_SYNC_EXCLUDES.split(/[,\s]+/).filter((e) =>
|
|
258
|
+
e.trim(),
|
|
259
|
+
);
|
|
260
|
+
let rsyncCmd = `rsync -avz --exclude=.git`;
|
|
261
|
+
excludes.forEach((ex) => {
|
|
262
|
+
if (ex) rsyncCmd += ` --exclude=${ex}`;
|
|
263
|
+
});
|
|
264
|
+
rsyncCmd += ` --progress -e "ssh -p ${config.REMOTE_PORT}" ${config.LOCAL_PATH}/ ${config.REMOTE_USER}@${config.REMOTE_HOST}:${config.REMOTE_PATH}/`;
|
|
265
|
+
|
|
266
|
+
console.log(`Using rsync excludes: ${excludes.join(", ")}`);
|
|
267
|
+
execSync(rsyncCmd, { stdio: "inherit" });
|
|
268
|
+
|
|
269
|
+
const dockerCmd = detectRemoteDockerComposeCommand();
|
|
270
|
+
if (!dockerCmd) {
|
|
271
|
+
console.error('ā Error: Docker compose command not available on remote server');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log("š³ Running docker compose on remote host...");
|
|
276
|
+
const platformFlag = config.DOCKER_PLATFORM && dockerCmd === 'docker compose' ? `--platform ${config.DOCKER_PLATFORM}` : "";
|
|
277
|
+
const remoteCmd =
|
|
278
|
+
`cd ${config.REMOTE_PATH} && ` +
|
|
279
|
+
`echo "Using compose file: ${composeFile}" && ` +
|
|
280
|
+
`echo "š„ Pulling latest images..." && ` +
|
|
281
|
+
`${dockerCmd} -f "${composeFile}" pull && ` +
|
|
282
|
+
`echo "š Stopping containers..." && ` +
|
|
283
|
+
`${dockerCmd} -f "${composeFile}" down && ` +
|
|
284
|
+
`echo "š Creating required networks..." && ` +
|
|
285
|
+
`docker network create coolify-shared 2>/dev/null || echo "Network already exists" && ` +
|
|
286
|
+
`echo "š Starting containers..." && ` +
|
|
287
|
+
`${dockerCmd} -f "${composeFile}" up -d ${platformFlag} && ` +
|
|
288
|
+
`echo "ā³ Waiting 5 seconds for containers to start..." && ` +
|
|
289
|
+
`sleep 5 && ` +
|
|
290
|
+
`echo "š Tailing last 100 lines of logs from all services..." && ` +
|
|
291
|
+
`${dockerCmd} -f "${composeFile}" logs --tail=100`;
|
|
292
|
+
|
|
293
|
+
execSync(
|
|
294
|
+
`ssh -p ${config.REMOTE_PORT} ${config.REMOTE_USER}@${config.REMOTE_HOST} "${remoteCmd}"`,
|
|
295
|
+
{ stdio: "inherit" },
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
console.log("ā
Deployment complete.");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function deployDomain() {
|
|
302
|
+
console.log("Starting domain deployment to Traefik gateway...");
|
|
303
|
+
|
|
304
|
+
if (!validateDomainEnvVars()) return;
|
|
305
|
+
|
|
306
|
+
const proxyFile = path.join(config.LOCAL_PATH, config.PROXY_FILE);
|
|
307
|
+
|
|
308
|
+
console.log("Preview of the proxy file to be deployed:");
|
|
309
|
+
console.log("------------------------");
|
|
310
|
+
console.log(fs.readFileSync(proxyFile, "utf8"));
|
|
311
|
+
console.log("------------------------");
|
|
312
|
+
|
|
313
|
+
const remotePath = `${config.DOMAIN_REMOTE_TRAEFIK_PATH}/${config.REMOTE_DOMAIN_CONFIG_FILENAME || config.PROXY_FILE}`;
|
|
314
|
+
console.log("Preview of the remote path to copy into:");
|
|
315
|
+
console.log("------------------------");
|
|
316
|
+
console.log(remotePath);
|
|
317
|
+
console.log("------------------------");
|
|
318
|
+
|
|
319
|
+
const confirm = await prompt(
|
|
320
|
+
"Do you want to continue with the deployment? (y/n) ",
|
|
321
|
+
);
|
|
322
|
+
if (!/^[Yy]$/.test(confirm)) {
|
|
323
|
+
console.log("Deployment cancelled.");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log(
|
|
328
|
+
`š Deploying domain configuration to ${config.DOMAIN_REMOTE_HOST}...`,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
console.log("š§ Checking if remote Traefik directory exists...");
|
|
332
|
+
execSync(
|
|
333
|
+
`ssh -p ${config.DOMAIN_REMOTE_PORT} ${config.DOMAIN_REMOTE_USER}@${config.DOMAIN_REMOTE_HOST} "mkdir -p ${config.DOMAIN_REMOTE_TRAEFIK_PATH}"`,
|
|
334
|
+
{ stdio: "inherit" },
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
console.log("š¦ Copying Traefik configuration to remote server...");
|
|
338
|
+
execSync(
|
|
339
|
+
`scp -P ${config.DOMAIN_REMOTE_PORT} "${proxyFile}" ${config.DOMAIN_REMOTE_USER}@${config.DOMAIN_REMOTE_HOST}:${remotePath}`,
|
|
340
|
+
{ stdio: "inherit" },
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
console.log("š Verifying file was copied successfully...");
|
|
344
|
+
execSync(
|
|
345
|
+
`ssh -p ${config.DOMAIN_REMOTE_PORT} ${config.DOMAIN_REMOTE_USER}@${config.DOMAIN_REMOTE_HOST} "ls -la ${remotePath}"`,
|
|
346
|
+
{ stdio: "inherit" },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
console.log("ā
Domain configuration deployment complete.");
|
|
350
|
+
const domains = String(config.PUBLISHED_DOMAIN || "")
|
|
351
|
+
.split(",")
|
|
352
|
+
.map((d) => d.trim())
|
|
353
|
+
.filter(Boolean);
|
|
354
|
+
const primaryDomain = domains[0] || config.PUBLISHED_DOMAIN;
|
|
355
|
+
|
|
356
|
+
console.log(
|
|
357
|
+
`š Your API should now be accessible at: https://${primaryDomain}`,
|
|
358
|
+
);
|
|
359
|
+
console.log(
|
|
360
|
+
`\ncURL to test the API:\ncurl https://${primaryDomain}/health`,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
let attempts = 0;
|
|
364
|
+
while (true) {
|
|
365
|
+
console.log(`Waiting for API to be accessible... (${attempts} times)`);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const result = execSync(`curl -s https://${primaryDomain}/health`, {
|
|
369
|
+
encoding: "utf8",
|
|
370
|
+
});
|
|
371
|
+
if (result.includes("ok")) {
|
|
372
|
+
console.log(`ā
API is now accessible at: https://${primaryDomain}`);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
} catch (e) {
|
|
376
|
+
// Try checking for 20x status code
|
|
377
|
+
try {
|
|
378
|
+
const statusCode = execSync(
|
|
379
|
+
`curl -s -o /dev/null -w "%{http_code}" https://${primaryDomain}`,
|
|
380
|
+
{ encoding: "utf8" },
|
|
381
|
+
);
|
|
382
|
+
if (statusCode.startsWith("20")) {
|
|
383
|
+
console.log(
|
|
384
|
+
`ā
API is now accessible at: https://${primaryDomain}`,
|
|
385
|
+
);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
} catch (e2) {}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
392
|
+
attempts++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function createProxyFile() {
|
|
397
|
+
console.log("Creating proxy file...");
|
|
398
|
+
|
|
399
|
+
if (!config.REMOTE_SERVICE_IP) {
|
|
400
|
+
console.error("ā Error: REMOTE_SERVICE_IP is not set in .env");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!config.PUBLISHED_DOMAIN) {
|
|
405
|
+
console.error("ā Error: PUBLISHED_DOMAIN is not set in .env");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const exampleServiceName = (config.APP_NAME || "service")
|
|
410
|
+
.replace(/\s+/g, "-")
|
|
411
|
+
.toLowerCase();
|
|
412
|
+
|
|
413
|
+
const serviceName =
|
|
414
|
+
(await prompt(
|
|
415
|
+
`Enter the Traefik service name (default: ${exampleServiceName}): `,
|
|
416
|
+
)) || exampleServiceName;
|
|
417
|
+
|
|
418
|
+
const traefikServiceName = serviceName.endsWith(`-${config.MODE}`)
|
|
419
|
+
? serviceName
|
|
420
|
+
: `${serviceName}-${config.MODE}`;
|
|
421
|
+
|
|
422
|
+
console.log("š§ Creating proxy file...");
|
|
423
|
+
console.log(`Service name: ${traefikServiceName}`);
|
|
424
|
+
console.log(`Published domain: ${config.PUBLISHED_DOMAIN}`);
|
|
425
|
+
console.log(`Remote service IP: ${config.REMOTE_SERVICE_IP}`);
|
|
426
|
+
|
|
427
|
+
const domains = String(config.PUBLISHED_DOMAIN || "")
|
|
428
|
+
.split(",")
|
|
429
|
+
.map((d) => d.trim())
|
|
430
|
+
.filter(Boolean);
|
|
431
|
+
|
|
432
|
+
//e.g rule: microexits.coolify.intrane.fr || dev.microexits.com
|
|
433
|
+
const ruleHostPart =
|
|
434
|
+
domains.length === 1
|
|
435
|
+
? `Host(\`${domains[0]}\`)`
|
|
436
|
+
: domains.length > 1
|
|
437
|
+
? `${domains.map((d) => `Host(\`${d}\`)`).join(" || ")}`
|
|
438
|
+
: `Host(\`${config.PUBLISHED_DOMAIN}\`)`;
|
|
439
|
+
|
|
440
|
+
const template = `http:
|
|
441
|
+
routers:
|
|
442
|
+
${traefikServiceName}:
|
|
443
|
+
entryPoints:
|
|
444
|
+
- https
|
|
445
|
+
service: ${traefikServiceName}
|
|
446
|
+
rule: ${ruleHostPart}
|
|
447
|
+
tls:
|
|
448
|
+
certresolver: letsencrypt
|
|
449
|
+
services:
|
|
450
|
+
${traefikServiceName}:
|
|
451
|
+
loadBalancer:
|
|
452
|
+
servers:
|
|
453
|
+
-
|
|
454
|
+
url: '${config.REMOTE_SERVICE_IP}'`;
|
|
455
|
+
|
|
456
|
+
const proxyFile = path.join(config.LOCAL_PATH, config.PROXY_FILE);
|
|
457
|
+
fs.writeFileSync(proxyFile, template, "utf8");
|
|
458
|
+
|
|
459
|
+
console.log(`ā
Proxy file created successfully at ${proxyFile}`);
|
|
460
|
+
console.log("Preview of the proxy file:");
|
|
461
|
+
console.log("------------------------");
|
|
462
|
+
console.log(template);
|
|
463
|
+
console.log("------------------------");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function parseComposeFile(composeFile) {
|
|
467
|
+
const content = fs.readFileSync(composeFile, "utf8");
|
|
468
|
+
const services = [];
|
|
469
|
+
const images = [];
|
|
470
|
+
|
|
471
|
+
const lines = content.split("\n");
|
|
472
|
+
let currentService = null;
|
|
473
|
+
let inServices = false;
|
|
474
|
+
|
|
475
|
+
for (let line of lines) {
|
|
476
|
+
const trimmed = line.trim();
|
|
477
|
+
|
|
478
|
+
if (trimmed === "services:") {
|
|
479
|
+
inServices = true;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (inServices && line.match(/^\w/) && !line.startsWith(" ")) {
|
|
484
|
+
inServices = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (inServices && line.match(/^ \w+:/)) {
|
|
488
|
+
currentService = line.match(/^ (\w+):/)[1];
|
|
489
|
+
services.push(currentService);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (currentService && trimmed.startsWith("image:")) {
|
|
493
|
+
const image = trimmed.replace("image:", "").trim();
|
|
494
|
+
images.push({ service: currentService, image });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (currentService && trimmed.startsWith("build:")) {
|
|
498
|
+
images.push({ service: currentService, image: null, hasBuild: true });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { services, images };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function buildImage() {
|
|
506
|
+
console.log("šØ Starting build process...");
|
|
507
|
+
|
|
508
|
+
const composeFiles = findComposeFiles();
|
|
509
|
+
if (composeFiles.length === 0) {
|
|
510
|
+
console.error(
|
|
511
|
+
"ā Error: No compose files found in the current directory.",
|
|
512
|
+
);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let selectedComposeFile;
|
|
517
|
+
|
|
518
|
+
if (composeFiles.length === 1) {
|
|
519
|
+
selectedComposeFile = composeFiles[0];
|
|
520
|
+
console.log(`Using compose file: ${selectedComposeFile}`);
|
|
521
|
+
} else {
|
|
522
|
+
console.log("Available compose files:");
|
|
523
|
+
composeFiles.forEach((file, idx) => {
|
|
524
|
+
console.log(` ${idx + 1}. ${file}`);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const choice = await prompt("Select a compose file (enter number): ");
|
|
528
|
+
const choiceNum = parseInt(choice);
|
|
529
|
+
|
|
530
|
+
if (isNaN(choiceNum) || choiceNum < 1 || choiceNum > composeFiles.length) {
|
|
531
|
+
console.error("ā Invalid selection");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
selectedComposeFile = composeFiles[choiceNum - 1];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const { images } = parseComposeFile(
|
|
539
|
+
path.join(config.LOCAL_PATH, selectedComposeFile),
|
|
540
|
+
);
|
|
541
|
+
const buildableImages = images.filter((img) => img.image || img.hasBuild);
|
|
542
|
+
|
|
543
|
+
if (buildableImages.length === 0) {
|
|
544
|
+
console.error("ā Error: No images found in the selected compose file.");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
console.log(`\nš§ Building images from ${selectedComposeFile}...`);
|
|
549
|
+
|
|
550
|
+
const dockerCmd = detectLocalDockerComposeCommand();
|
|
551
|
+
if (!dockerCmd) {
|
|
552
|
+
console.error('ā Error: Docker compose command not available locally');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
execSync(`${dockerCmd} -f ${selectedComposeFile} build`, {
|
|
558
|
+
stdio: "inherit",
|
|
559
|
+
cwd: config.LOCAL_PATH,
|
|
560
|
+
});
|
|
561
|
+
console.log("ā
Build complete!");
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error("ā Build failed");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
console.log("\nAvailable images to push:");
|
|
568
|
+
buildableImages.forEach((img, idx) => {
|
|
569
|
+
const displayName = img.image || `${img.service} (built locally)`;
|
|
570
|
+
console.log(` ${idx + 1}. ${displayName}`);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const pushConfirm = await prompt("\nDo you want to push an image? (y/n) ");
|
|
574
|
+
if (!/^[Yy]$/.test(pushConfirm)) {
|
|
575
|
+
console.log("Push cancelled.");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const imageChoice = await prompt(
|
|
580
|
+
"Select an image to push (enter number): ",
|
|
581
|
+
);
|
|
582
|
+
const imageChoiceNum = parseInt(imageChoice);
|
|
583
|
+
|
|
584
|
+
if (
|
|
585
|
+
isNaN(imageChoiceNum) ||
|
|
586
|
+
imageChoiceNum < 1 ||
|
|
587
|
+
imageChoiceNum > buildableImages.length
|
|
588
|
+
) {
|
|
589
|
+
console.error("ā Invalid selection");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const selectedImage = buildableImages[imageChoiceNum - 1];
|
|
594
|
+
const imageName = selectedImage.image;
|
|
595
|
+
|
|
596
|
+
if (!imageName) {
|
|
597
|
+
console.error(
|
|
598
|
+
'ā Error: No image name specified for this service. Please add an "image:" field in the compose file.',
|
|
599
|
+
);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log(`\nš¤ Pushing image: ${imageName}...`);
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
execSync(`docker push ${imageName}`, {
|
|
607
|
+
stdio: "inherit",
|
|
608
|
+
cwd: config.LOCAL_PATH,
|
|
609
|
+
});
|
|
610
|
+
console.log("ā
Push complete!");
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.error("ā Push failed");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function showEnvVars() {
|
|
617
|
+
console.log("===== Current Environment Variables =====");
|
|
618
|
+
console.log(`REMOTE_HOST: ${config.REMOTE_HOST}`);
|
|
619
|
+
console.log(`REMOTE_USER: ${config.REMOTE_USER}`);
|
|
620
|
+
console.log(`REMOTE_PORT: ${config.REMOTE_PORT}`);
|
|
621
|
+
console.log(`REMOTE_PATH: ${config.REMOTE_PATH}`);
|
|
622
|
+
console.log(`COMPOSE_FILE: ${config.COMPOSE_FILE}`);
|
|
623
|
+
console.log(`DOMAIN_REMOTE_HOST: ${config.DOMAIN_REMOTE_HOST}`);
|
|
624
|
+
console.log(`DOMAIN_REMOTE_USER: ${config.DOMAIN_REMOTE_USER}`);
|
|
625
|
+
console.log(`DOMAIN_REMOTE_PORT: ${config.DOMAIN_REMOTE_PORT}`);
|
|
626
|
+
console.log(`REMOTE_SERVICE_IP: ${config.REMOTE_SERVICE_IP}`);
|
|
627
|
+
console.log(`PUBLISHED_DOMAIN: ${config.PUBLISHED_DOMAIN}`);
|
|
628
|
+
console.log(`REMOTE_SYNC_EXCLUDES: ${config.REMOTE_SYNC_EXCLUDES}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function showHelp() {
|
|
632
|
+
console.log(
|
|
633
|
+
`===== ${config.APP_NAME || "Application"} Management Script =====`,
|
|
634
|
+
);
|
|
635
|
+
console.log("Usage: node manage.js [OPTION]");
|
|
636
|
+
console.log("");
|
|
637
|
+
console.log("Options:");
|
|
638
|
+
console.log(" logs - Follow logs in remote server");
|
|
639
|
+
console.log(" deploy - Deploy application to remote server");
|
|
640
|
+
console.log(" proxy - Create proxy configuration file");
|
|
641
|
+
console.log(" domain - Deploy domain to remote (Traefik gateway)");
|
|
642
|
+
console.log(" build - Build and optionally push Docker images");
|
|
643
|
+
console.log(" env - Show environment variables");
|
|
644
|
+
console.log(" help - Show this help message");
|
|
645
|
+
console.log("");
|
|
646
|
+
console.log(
|
|
647
|
+
"If no option is provided, an interactive menu will be displayed.",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function showMenu() {
|
|
652
|
+
console.log(`\n===== ${config.APP_NAME || "Application"} Management =====`);
|
|
653
|
+
console.log("1. Follow logs in remote");
|
|
654
|
+
console.log("2. Deploy to remote");
|
|
655
|
+
console.log("3. Create proxy configuration file");
|
|
656
|
+
console.log("4. Deploy domain to remote (Traefik gateway)");
|
|
657
|
+
console.log("5. Build and push Docker images");
|
|
658
|
+
console.log("6. Show environment variables");
|
|
659
|
+
console.log("7. Exit");
|
|
660
|
+
|
|
661
|
+
const choice = await prompt("\nPlease select an option (1-7): ");
|
|
662
|
+
|
|
663
|
+
switch (choice.trim()) {
|
|
664
|
+
case "1":
|
|
665
|
+
await followLogs();
|
|
666
|
+
break;
|
|
667
|
+
case "2":
|
|
668
|
+
await deployApp();
|
|
669
|
+
break;
|
|
670
|
+
case "3":
|
|
671
|
+
await createProxyFile();
|
|
672
|
+
break;
|
|
673
|
+
case "4":
|
|
674
|
+
await deployDomain();
|
|
675
|
+
break;
|
|
676
|
+
case "5":
|
|
677
|
+
await buildImage();
|
|
678
|
+
break;
|
|
679
|
+
case "6":
|
|
680
|
+
showEnvVars();
|
|
681
|
+
break;
|
|
682
|
+
case "7":
|
|
683
|
+
console.log("Exiting...");
|
|
684
|
+
rl.close();
|
|
685
|
+
process.exit(0);
|
|
686
|
+
default:
|
|
687
|
+
console.log("ā Invalid option. Please try again.");
|
|
688
|
+
await showMenu();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
rl.close();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const args = process.argv.slice(2);
|
|
695
|
+
|
|
696
|
+
if (args.length === 0) {
|
|
697
|
+
await showMenu();
|
|
698
|
+
} else {
|
|
699
|
+
const command = args[0];
|
|
700
|
+
|
|
701
|
+
switch (command) {
|
|
702
|
+
case "logs":
|
|
703
|
+
await followLogs();
|
|
704
|
+
rl.close();
|
|
705
|
+
break;
|
|
706
|
+
case "deploy":
|
|
707
|
+
await deployApp();
|
|
708
|
+
rl.close();
|
|
709
|
+
break;
|
|
710
|
+
case "proxy":
|
|
711
|
+
await createProxyFile();
|
|
712
|
+
rl.close();
|
|
713
|
+
break;
|
|
714
|
+
case "domain":
|
|
715
|
+
await deployDomain();
|
|
716
|
+
rl.close();
|
|
717
|
+
break;
|
|
718
|
+
case "build":
|
|
719
|
+
await buildImage();
|
|
720
|
+
rl.close();
|
|
721
|
+
break;
|
|
722
|
+
case "env":
|
|
723
|
+
showEnvVars();
|
|
724
|
+
rl.close();
|
|
725
|
+
break;
|
|
726
|
+
case "help":
|
|
727
|
+
showHelp();
|
|
728
|
+
rl.close();
|
|
729
|
+
break;
|
|
730
|
+
default:
|
|
731
|
+
console.log(`Unknown option: ${command}`);
|
|
732
|
+
showHelp();
|
|
733
|
+
rl.close();
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
main().catch((error) => {
|
|
740
|
+
console.error("Error:", error);
|
|
741
|
+
if (typeof rl !== 'undefined') {
|
|
742
|
+
rl.close();
|
|
743
|
+
}
|
|
744
|
+
process.exit(1);
|
|
745
|
+
});
|