NepsenX commited on
Commit
cb3d548
·
verified ·
1 Parent(s): 447dc0d

Create src/index.ts

Browse files
Files changed (1) hide show
  1. src/index.ts +262 -0
src/index.ts ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as http from 'node:http';
2
+ import { exec, spawn, ChildProcess } from 'node:child_process';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import axios from 'axios';
7
+ import nodemailer from 'nodemailer';
8
+
9
+ // ================== কনফিগারেশন ==================
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ interface Config {
14
+ RES1_REPO: string;
15
+ RES3_REPO: string;
16
+ GITHUB_PAT: string;
17
+ VERSIONS_DIR: string;
18
+ HEALTH_PORT: number;
19
+ PROD_PORT: number;
20
+ TEST_PORT: number;
21
+ ADMIN_EMAIL: string;
22
+ SMTP_USER: string;
23
+ SMTP_PASS: string;
24
+ }
25
+
26
+ const CONFIG: Config = {
27
+ RES1_REPO: 'https://github.com/NepsenX/Oracus-AI.git',
28
+ RES3_REPO: 'https://github.com/oracusai-nepsenx/Recovery-Temp.git',
29
+ GITHUB_PAT: process.env.GITHUB_PAT || '',
30
+ VERSIONS_DIR: '/app/versions',
31
+ HEALTH_PORT: 7860,
32
+ PROD_PORT: 7000,
33
+ TEST_PORT: 2000,
34
+ ADMIN_EMAIL: 'oracusai.nepsenx@gmail.com',
35
+ SMTP_USER: 'oracusai.nepsenx@gmail.com',
36
+ SMTP_PASS: process.env.SMTP_PASS || 'your-app-password'
37
+ };
38
+
39
+ // ================== স্টেট ম্যানেজমেন্ট (TypeScript টাইপসহ) ==================
40
+ let prodProcess: ChildProcess | null = null;
41
+ let prodVersionPath: string | null = null;
42
+ let isUpdating: boolean = false;
43
+
44
+ // ================== ইমেইল ফাংশন ==================
45
+ async function sendEmail(subject: string, text: string): Promise<void> {
46
+ try {
47
+ const transporter = nodemailer.createTransport({
48
+ service: 'gmail',
49
+ auth: { user: CONFIG.SMTP_USER, pass: CONFIG.SMTP_PASS }
50
+ });
51
+ await transporter.sendMail({
52
+ from: CONFIG.SMTP_USER,
53
+ to: CONFIG.ADMIN_EMAIL,
54
+ subject: `[Oracus Runner] ${subject}`,
55
+ text: text
56
+ });
57
+ console.log(`📧 Email sent: ${subject}`);
58
+ } catch (err: any) {
59
+ console.error('❌ Failed to send email:', err.message);
60
+ }
61
+ }
62
+
63
+ // ================== গিট ইউটিলিটি (Promise র্যাপার) ==================
64
+ function runGitCommand(cmd: string, cwd: string): Promise<string> {
65
+ return new Promise((resolve, reject) => {
66
+ exec(cmd, { cwd }, (error, stdout, stderr) => {
67
+ if (error) reject(stderr || error.message);
68
+ else resolve(stdout.trim());
69
+ });
70
+ });
71
+ }
72
+
73
+ async function getCurrentCommitSha(repoUrl: string): Promise<string | null> {
74
+ try {
75
+ const output = await runGitCommand(`git ls-remote ${repoUrl} main`, process.cwd());
76
+ return output.split(/\s/)[0];
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ async function cloneOrPull(repoUrl: string, targetDir: string): Promise<void> {
83
+ if (!fs.existsSync(targetDir)) {
84
+ console.log(`📥 Cloning ${repoUrl} into ${targetDir}...`);
85
+ await runGitCommand(`git clone ${repoUrl} ${targetDir}`, process.cwd());
86
+ } else {
87
+ console.log(`🔄 Pulling latest in ${targetDir}...`);
88
+ await runGitCommand(`git pull`, targetDir);
89
+ }
90
+ }
91
+
92
+ // ================== এজেন্ট ডিপ্লয় (পোর্ট প্যারামিটার সহ) ==================
93
+ function deployAgent(versionDir: string, port: number): Promise<ChildProcess> {
94
+ return new Promise((resolve, reject) => {
95
+ console.log(`🚀 Starting agent on port ${port} from ${versionDir}...`);
96
+
97
+ // ১. ডিপেন্ডেন্সি ও বিল্ড
98
+ exec(`npm install && npm run build`, { cwd: versionDir }, (err) => {
99
+ if (err) return reject(`Build failed: ${err.message}`);
100
+
101
+ // ২. প্রসেস স্পন (পোর্ট এনভ ভেরিয়েবল হিসেবে)
102
+ const proc = spawn('npm', ['start'], {
103
+ cwd: versionDir,
104
+ env: { ...process.env, PORT: String(port) },
105
+ stdio: 'pipe'
106
+ });
107
+
108
+ proc.stdout.on('data', (data) => console.log(`[AGENT:${port}] ${data}`));
109
+ proc.stderr.on('data', (data) => console.error(`[AGENT ERR:${port}] ${data}`));
110
+
111
+ // ৩. হেলথ চেক (৫ সেকেন্ড অপেক্ষা)
112
+ setTimeout(async () => {
113
+ try {
114
+ const res = await axios.get(`http://localhost:${port}/health`, { timeout: 2000 });
115
+ if (res.status === 200) {
116
+ console.log(`✅ Health check passed on port ${port}!`);
117
+ resolve(proc);
118
+ } else {
119
+ reject(`Health check failed (non-200) on port ${port}`);
120
+ }
121
+ } catch (err: any) {
122
+ reject(`Health check error on port ${port}: ${err.message}`);
123
+ }
124
+ }, 5000);
125
+ });
126
+ });
127
+ }
128
+
129
+ // ================== মেমোরি সিঙ্ক (Res3) ==================
130
+ async function syncMemory(versionPath: string): Promise<void> {
131
+ if (!versionPath) return;
132
+ try {
133
+ const memoryBackupDir = '/app/memory-backup';
134
+ if (!fs.existsSync(memoryBackupDir)) {
135
+ await runGitCommand(`git clone ${CONFIG.RES3_REPO} ${memoryBackupDir}`, process.cwd());
136
+ }
137
+
138
+ const srcMemory = path.join(versionPath, 'memory');
139
+ const dstMemory = path.join(memoryBackupDir, 'memory');
140
+
141
+ if (fs.existsSync(srcMemory)) {
142
+ fs.rmSync(dstMemory, { recursive: true, force: true });
143
+ fs.cpSync(srcMemory, dstMemory, { recursive: true });
144
+ }
145
+
146
+ await runGitCommand(`git add . && git commit -m "Memory sync from runner [skip ci]" || echo "No changes"`, memoryBackupDir);
147
+ await runGitCommand(`git push`, memoryBackupDir);
148
+ console.log('💾 Memory synced to Res3.');
149
+ } catch (err: any) {
150
+ console.error('⚠️ Memory sync failed:', err.message);
151
+ }
152
+ }
153
+
154
+ // ================== মূল আপডেট লজিক (ব্লু-গ্রিন) ==================
155
+ async function checkAndUpdate(): Promise<void> {
156
+ if (isUpdating) {
157
+ console.log('⏳ Update already in progress...');
158
+ return;
159
+ }
160
+
161
+ isUpdating = true;
162
+ try {
163
+ console.log('🔍 Checking for updates...');
164
+
165
+ // ১. সর্বশেষ SHA বের করা
166
+ const latestSha = await getCurrentCommitSha(CONFIG.RES1_REPO);
167
+ if (!latestSha) throw new Error('Could not fetch latest SHA');
168
+
169
+ // ২. বর্তমান প্রোডাকশন SHA বের করা
170
+ let currentSha: string | null = null;
171
+ if (prodVersionPath && fs.existsSync(path.join(prodVersionPath, '.git'))) {
172
+ try {
173
+ currentSha = await runGitCommand('git rev-parse HEAD', prodVersionPath);
174
+ } catch {}
175
+ }
176
+
177
+ if (latestSha === currentSha) {
178
+ console.log('✅ Already up to date.');
179
+ isUpdating = false;
180
+ return;
181
+ }
182
+
183
+ console.log(`🆕 New version detected: ${latestSha}. Deploying V2 on port ${CONFIG.TEST_PORT}...`);
184
+
185
+ // ৩. V2 (টেস্ট) ডাউনলোড
186
+ const testVersionDir = path.join(CONFIG.VERSIONS_DIR, `v2_${Date.now()}`);
187
+ await cloneOrPull(CONFIG.RES1_REPO, testVersionDir);
188
+
189
+ // ৪. V2-কে পোর্ট ২০০০-এ চালান
190
+ const testProc = await deployAgent(testVersionDir, CONFIG.TEST_PORT);
191
+
192
+ console.log(`✅ V2 passed health check! Promoting V2 to port ${CONFIG.PROD_PORT}...`);
193
+
194
+ // ৫. প্রমোশন: পুরনো V1 (পোর্ট ৭০০০) বন্ধ
195
+ if (prodProcess) {
196
+ console.log('🛑 Stopping old V1 (Production)...');
197
+ prodProcess.kill('SIGTERM');
198
+ setTimeout(() => { if (!prodProcess?.killed) prodProcess?.kill('SIGKILL'); }, 3000);
199
+ }
200
+
201
+ // ৬. V2-কে পোর্ট ২০০০ থেকে তুলে ৭০০০-এ চালু
202
+ testProc.kill('SIGTERM');
203
+ await new Promise(resolve => setTimeout(resolve, 1000));
204
+ const newProdProc = await deployAgent(testVersionDir, CONFIG.PROD_PORT);
205
+
206
+ // ৭. স্টেট আপডেট
207
+ prodProcess = newProdProc;
208
+ prodVersionPath = testVersionDir;
209
+
210
+ // ৮. মেমোরি সিঙ্ক
211
+ await syncMemory(testVersionDir);
212
+
213
+ console.log(`✅ Successfully deployed V2 (${latestSha}) on port ${CONFIG.PROD_PORT}`);
214
+ await sendEmail('Deployment Successful', `Version ${latestSha} is now LIVE on port 7000.`);
215
+
216
+ } catch (error: any) {
217
+ console.error('❌ Deployment failed:', error);
218
+ if (prodProcess) {
219
+ console.log('🔄 Auto-rollback: Keeping old V1 running on port 7000.');
220
+ } else {
221
+ console.log('⚠️ No previous version available to rollback to.');
222
+ }
223
+ await sendEmail('Deployment FAILED - Rollback', `Error: ${error.message}. Previous version is still running.`);
224
+ } finally {
225
+ isUpdating = false;
226
+ }
227
+ }
228
+
229
+ // ================== HTTP সার্ভার (পোর্ট ৭৮৬০ - UptimeRobot) ==================
230
+ const server = http.createServer(async (req, res) => {
231
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
232
+
233
+ if (url.pathname === '/') {
234
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
235
+ res.end('Oracus Runner is Alive! (Port 7860)');
236
+ setImmediate(() => checkAndUpdate());
237
+ } else if (url.pathname === '/force-update') {
238
+ res.writeHead(200);
239
+ res.end('Force update triggered.');
240
+ setImmediate(() => checkAndUpdate());
241
+ } else {
242
+ res.writeHead(404);
243
+ res.end('Not Found');
244
+ }
245
+ });
246
+
247
+ server.listen(CONFIG.HEALTH_PORT, '0.0.0.0', () => {
248
+ console.log(`✅ Health server running on port ${CONFIG.HEALTH_PORT}`);
249
+ console.log('⏳ Initial setup: Deploying V1 on port 7000...');
250
+ checkAndUpdate();
251
+ });
252
+
253
+ // ================== প্রসেস হ্যান্ডলিং (ক্র্যাশ হলে রিস্টার্ট) ==================
254
+ process.on('uncaughtException', (err) => {
255
+ console.error('💥 Uncaught Exception:', err);
256
+ sendEmail('CRASH - Uncaught Exception', err.stack || err.message);
257
+ });
258
+
259
+ process.on('unhandledRejection', (reason) => {
260
+ console.error('💥 Unhandled Rejection:', reason);
261
+ sendEmail('CRASH - Unhandled Rejection', String(reason));
262
+ });