fa-mcp-sdk 0.2.87 → 0.2.95

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.
@@ -1,642 +1,638 @@
1
- // noinspection UnnecessaryLocalVariableJS
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync, spawn } = require('child_process');
6
- const os = require('os');
7
-
8
- const version = '2025.11.24-0743';
9
- console.log(`Update script version: ${version}`);
10
-
11
- // Имя этой папки
12
- const scriptDirName = require('path').basename(__dirname);
13
- // Смена рабочей директории на директорию скрипта
14
- process.chdir(__dirname);
15
- const CWD = process.cwd();
16
- const VON = path.resolve(path.join(CWD, '..'));
17
-
18
- // Default configuration
19
- const DEFAULT_CONFIG = {
20
- branch: 'master',
21
- email: '',
22
- };
23
-
24
- // Colors for terminal output
25
- const colors = {
26
- reset: '\x1b[0m',
27
- bright: '\x1b[1m',
28
- dim: '\x1b[2m',
29
- red: '\x1b[31m',
30
- green: '\x1b[32m',
31
- yellow: '\x1b[33m',
32
- blue: '\x1b[34m',
33
- magenta: '\x1b[35m',
34
- cyan: '\x1b[36m',
35
- white: '\x1b[37m',
36
- };
37
-
38
- const color = {};
39
- const colorG = {};
40
- ['cyan', 'green', 'magenta', 'red', 'yellow'].forEach((col) => {
41
- const firstLetter = col[0];
42
- color[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.reset}`;
43
- color[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.reset}`;
44
- colorG[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.green}`;
45
- colorG[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.green}`;
46
- });
47
-
48
- // Echo functions with colors
49
- const echo = {
50
- c: (text) => console.log(color.c(text)),
51
- lc: (text) => console.log(color.lc(text)),
52
- g: (text) => console.log(color.g(text)),
53
- lg: (text) => console.log(color.lg(text)),
54
- m: (text) => console.log(color.m(text)),
55
- lm: (text) => console.log(color.lm(text)),
56
- r: (text) => console.log(color.r(text)),
57
- lr: (text) => console.log(color.lr(text)),
58
- y: (text) => console.log(color.y(text)),
59
- ly: (text) => console.log(color.ly(text)),
60
- lg_no_newline: (msg) => process.stdout.write(color.lg(msg)),
61
- };
62
-
63
- let logBuffer = '';
64
-
65
- // Global variable to store NVM environment
66
- let setupScript = '';
67
- let nodeVersion = null;
68
- const DEFAULT_NODE_VERSION = '22.17.1';
69
-
70
- const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0].replace('T', '');
71
- const ytdl = timestamp.slice(2, 14); // YYMMDDHHMMSS format
72
- const runTimeLogFile = path.join(VON, `deploy__${scriptDirName}__processing__${ytdl}.log`);
73
- const lastDeployLogFile = path.join(VON, `deploy__${scriptDirName}__last_deploy.log`);
74
- const cumulativeLogFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
75
-
76
- const clearColors = (text) => text.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
77
- const clearHtmlColors = (text) => text.replace(/<\/?(red|y|g|r|status)>/g, '');
78
-
79
- const logIt = (msg, isTitle) => {
80
- if (isTitle) {
81
- const lng = 60 - (msg.length + 2);
82
- const left = Math.floor(lng / 2);
83
- const right = lng - left;
84
- msg = `${'='.repeat(left)} ${msg} ${'='.repeat(right)}`;
85
- }
86
- const msg4console = clearHtmlColors(msg);
87
- echo.g(msg4console);
88
- logBuffer += `${msg}\n`;
89
- fs.appendFileSync(runTimeLogFile, `${clearColors(msg4console)}\n`);
90
- };
91
-
92
- const logError = (msg) => {
93
- console.error(color.r(msg));
94
- const msg2 = `[ERROR] ${msg}`;
95
- logBuffer += `<red>${msg2}</red>\n`;
96
- fs.appendFileSync(cumulativeLogFile, `${msg2}\n`);
97
- };
98
-
99
- const nowPretty = () => new Date().toISOString().replace('T', ' ').substring(0, 19) + 'Z';
100
-
101
- /**
102
- * Truncate cumulative log file if it exceeds 2MB, keeping last 10KB
103
- */
104
- const truncateCumulativeLogIfNeeded = () => {
105
- const logFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
106
- const maxFileSize = 2 * 1024 * 1024; // 2MB
107
- const keepSize = 10 * 1024; // 10KB
108
-
109
- try {
110
- if (fs.existsSync(logFile)) {
111
- const stats = fs.statSync(logFile);
112
- if (stats.size > maxFileSize) {
113
- // Read last 10KB
114
- const fd = fs.openSync(logFile, 'r');
115
- const buffer = Buffer.alloc(keepSize);
116
-
117
- // Position to last 10KB
118
- fs.readSync(fd, buffer, 0, keepSize, stats.size - keepSize);
119
- fs.closeSync(fd);
120
-
121
- // Write back only the last 10KB
122
- const tailContent = buffer.toString('utf8').replace(/^[\r\n]*/, ''); // Remove leading newlines
123
- fs.writeFileSync(logFile, tailContent);
124
-
125
- log(`Cumulative log truncated to ${Math.round(tailContent.length / 1024)}KB`);
126
- }
127
- }
128
- } catch (error) {
129
- logError(`Failed to truncate cumulative log: ${error.message}`);
130
- }
131
- };
132
-
133
- const logTryUpdate = (updateReason = '') => {
134
- truncateCumulativeLogIfNeeded();
135
- updateReason = updateReason ? `Update reason: ${updateReason}` : '';
136
- const message = updateReason || nowPretty();
137
- fs.appendFileSync(cumulativeLogFile, `${message}\n`);
138
- };
139
-
140
- /**
141
- * Execute command in NVM environment
142
- */
143
- function execCommand (command, options = {}, withSetupScript = false) {
144
- // If we have NVM setup, wrap the command
145
- const fullCommand = (setupScript && withSetupScript) ? `${setupScript} && ${command}` : command;
146
- try {
147
- const result = execSync(fullCommand, {
148
- encoding: 'utf8',
149
- stdio: options.silent ? 'inherit' : 'pipe',
150
- shell: '/bin/bash',
151
- ...options,
152
- });
153
- return result;
154
- } catch (error) {
155
- if (options.throwOnError !== false) {
156
- throw error;
157
- }
158
- return error.stdout || '';
159
- }
160
- }
161
-
162
- function execWithNODE (command, options = {}) {
163
- return execCommand(command, options, true);
164
- }
165
-
166
- /**
167
- * Load NVM environment and get Node.js version
168
- */
169
- function loadNVMEnvironment () {
170
- try {
171
- if (fs.existsSync('.envrc')) {
172
- const envrcContent = fs.readFileSync('.envrc', 'utf8');
173
-
174
- // Extract Node.js version from .envrc for logging
175
- const nodeVersionMatch = envrcContent.match(/nvm use\s+([0-9.]+)/);
176
- const nodeV = nodeVersionMatch ? nodeVersionMatch[1] : null;
177
-
178
- if (nodeV) {
179
- nodeVersion = nodeV;
180
- }
181
- setupScript = 'source .envrc';
182
- }
183
- } catch (error) {
184
- logError('Error loading .envrc file');
185
- }
186
- }
187
-
188
- /**
189
- * Parse command line arguments
190
- */
191
- function parseArgs () {
192
- const pArgs = process.argv.slice(2);
193
- const args = {
194
- expectedBranch: null,
195
- help: false,
196
- force: false,
197
- };
198
-
199
- for (let i = 0; i < pArgs.length; i++) {
200
- const arg = pArgs[i];
201
- switch (arg) {
202
- case '-b':
203
- case '--branch':
204
- args.expectedBranch = pArgs[++i];
205
- break;
206
- case '-f':
207
- case '--force':
208
- args.force = true;
209
- break;
210
- case '-?':
211
- case '--help':
212
- args.help = true;
213
- break;
214
- }
215
- }
216
-
217
- return args;
218
- }
219
-
220
- /**
221
- * Show help information
222
- */
223
- function showHelp () {
224
- console.log(`
225
- ================================================================================
226
- Project update and rebuild
227
-
228
- Usage:
229
- node update.js [Options]
230
-
231
- Options:
232
-
233
- -b|--branch
234
- GIT branch name. Default - master
235
- -l|--log
236
- Switch to log display mode after completion
237
- -?|--help
238
- Display help
239
-
240
- Example: node update.js -b production -l
241
- ================================================================================
242
- `);
243
- }
244
-
245
- /**
246
- * Parse simple YAML content (key: value format)
247
- */
248
- function parseSimpleYAML (content) {
249
- const config = {};
250
- const lines = content.split('\n');
251
-
252
- for (const line of lines) {
253
- const trimmed = line.trim();
254
- // Skip empty lines and comments
255
- if (trimmed && !trimmed.startsWith('#')) {
256
- // Parse key: value pairs
257
- const colonIndex = trimmed.indexOf(':');
258
- if (colonIndex > 0) {
259
- const [, key, valueRaw] = trimmed.match(/^\s*([^:]+?)\s*:\s*(.*)\s*$/) || [];
260
- let value = valueRaw ? valueRaw.replace(/^(['"])(.*)\1$/, '$2') : '';
261
- // Handle empty values
262
- if (value === 'null' || value === '~') {
263
- value = '';
264
- }
265
- config[key] = value;
266
- }
267
- }
268
- }
269
-
270
- return config;
271
- }
272
-
273
- /**
274
- * Load configuration from YAML file
275
- */
276
- function loadConfig () {
277
- // Load NVM environment from .envrc
278
- loadNVMEnvironment();
279
-
280
- const configFile = path.join(process.cwd(), 'deploy', 'config.yml');
281
-
282
- // Get Node.js version from NVM environment if available
283
- if (!fs.existsSync(configFile)) {
284
- return { ...DEFAULT_CONFIG };
285
- }
286
-
287
- try {
288
- const configContent = fs.readFileSync(configFile, 'utf8');
289
- const config = parseSimpleYAML(configContent);
290
-
291
- return {
292
- branch: config.branch || DEFAULT_CONFIG.branch,
293
- nodeVersion: config.nodeVersion,
294
- email: config.email || DEFAULT_CONFIG.email,
295
- };
296
- } catch (error) {
297
- console.warn(`Warning: Could not parse config file ${configFile}:`, error.message);
298
- return { ...DEFAULT_CONFIG };
299
- }
300
- }
301
-
302
- /**
303
- * Get service name from package.json and .env
304
- */
305
- function getServiceName () {
306
- let serviceName = '';
307
- let serviceNameAlt = '';
308
- let serviceInstance = '';
309
- try {
310
- if (fs.existsSync('.env')) {
311
- const envContent = fs.readFileSync('.env', 'utf8');
312
- let match = envContent.match(/^SERVICE_NAME=([^\r\n]+)/m);
313
- if (match) {
314
- serviceName = match[1].trim();
315
- }
316
- match = envContent.match(/^SERVICE_NAME_ALT=([^\r\n]+)/m);
317
- if (match) {
318
- serviceNameAlt = match[1].trim();
319
- }
320
- match = envContent.match(/^SERVICE_INSTANCE=([^\r\n]+)/m);
321
- if (match) {
322
- serviceInstance = `--${match[1].trim()}`;
323
- }
324
- }
325
- if (!serviceName) {
326
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
327
- serviceName = packageJson.name;
328
- }
329
-
330
- return {
331
- serviceNameForPM2: `${serviceName}${serviceInstance}`,
332
- serviceName,
333
- serviceNameForSystemd: serviceNameAlt || serviceName,
334
- };
335
- } catch (error) {
336
- console.error('Error getting service name:', error.message);
337
- process.exit(1);
338
- }
339
- }
340
-
341
- /**
342
- * Check if systemctl service exists
343
- */
344
- function systemctlServiceExists (serviceName) {
345
- try {
346
- const res = execCommand(`systemctl list-unit-files "${serviceName}.service"`);
347
- return true;
348
- } catch {
349
- return false;
350
- }
351
- }
352
-
353
- function pm2ServiceExists (serviceName) {
354
- try {
355
- const res = execCommand(`pm2 id "${serviceName}"`);
356
- return /\[\s*\d\s*]/.test(res);
357
- } catch {
358
- return false;
359
- }
360
- }
361
-
362
- /**
363
- * Get git repository information
364
- */
365
- function getRepoInfo () {
366
- try {
367
- const branch = execCommand('git rev-parse --abbrev-ref HEAD').trim();
368
- const headHash = execCommand('git rev-parse HEAD').trim();
369
- // const headShortHash = execCommand('git rev-parse --short HEAD').trim();
370
- const headCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${headHash}`).trim();
371
- const headDdate = execCommand(`git log -n 1 --format="%at" ${headHash} | xargs -I{} date -d @{} +%d.%m.%Y_%H:%M:%S`).trim();
372
- execCommand(`git fetch origin ${branch} --prune`);
373
-
374
- const upstreamHash = execCommand(`git rev-parse ${branch}@{upstream}`).trim();
375
- // const upstreamShortHash = execCommand(`git rev-parse --short ${branch}@{upstream}`).trim();
376
- // const upstreamCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${upstreamHash}`).trim();
377
-
378
- return {
379
- branch,
380
- headDdate,
381
- headHash,
382
- headCommitMessage,
383
- upstreamHash,
384
- };
385
- } catch (error) {
386
- console.error('Error getting repo info:', error.message);
387
- return null;
388
- }
389
- }
390
-
391
- const colorizeHTML = (text) => text
392
- .replace(/<red>/g, '<span style="color:#ff0000;">')
393
- .replace(/<\/red>/g, '</span>')
394
- .replace(/<y>/g, '<span style="background-color:#ffff00;">')
395
- .replace(/<\/y>/g, '</span>')
396
- .replace(/<g>/g, '<span style="background-color:#00ff00;">')
397
- .replace(/<\/g>/g, '</span>')
398
- .replace(/<r>/g, '<span style="background-color:#ff0000; color:#ffffff;">')
399
- .replace(/<\/r>/g, '</span>')
400
- .replace(/\[ERROR]/g, '<span style="color:#ffffff; background-color: #ff0000">[ERROR]</span>');
401
-
402
- async function sendBuildNotification (emails, status, body, serviceName) {
403
- if (!emails) { return; }
404
- let s = '';
405
- if (status === 'FAIL') {
406
- s = `<r>FAIL</r> `;
407
- } else if (status === 'SUCCESS') {
408
- s = `<g>SUCCESS</g> `;
409
- }
410
- body = body.replace('<status>', s);
411
-
412
- // Create HTML email content
413
- const hostname = os.hostname();
414
- const htmlContent = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
415
- <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
416
- <head>
417
- <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
418
- <meta name="viewport" content="width=device-width, initial-scale=1">
419
- <title>${status} Update ${serviceName} (on ${hostname})</title>
420
- </head>
421
- <body>
422
- <pre>
423
- ${colorizeHTML(clearColors(body))}
424
- </pre></body></html>`;
425
-
426
- // Send to each email address
427
- const emailArray = emails.split(',').map((email) => email.trim()).filter((email) => email);
428
-
429
- for (let i = 0; i < emailArray.length; i++) {
430
- const emailAddress = emailArray[i];
431
- try {
432
- logIt(`Sending update notification to: ${emailAddress}`);
433
- const subject = `${status} Update: ${serviceName} (on ${hostname})`;
434
-
435
- // Подаем тело письма через stdin, задаем Content-Type
436
- const command = `mail -a "Content-Type: text/html; charset=UTF-8" -s "${subject.replace(/"/g, '\\"')}" "${emailAddress}"`;
437
- const child = spawn('/bin/bash', ['-lc', command], { stdio: ['pipe', 'inherit', 'inherit'] });
438
- child.stdin.write(htmlContent);
439
- child.stdin.end();
440
-
441
- await new Promise((resolve, reject) => {
442
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`mail exit code ${code}`))));
443
- child.on('error', reject);
444
- });
445
- } catch (error) {
446
- console.error(`Failed to send email to ${emailAddress}:`, error.message);
447
- }
448
- }
449
- }
450
-
451
- const printCurrenBranch = () => {
452
- const i = getRepoInfo();
453
- logIt(`Current branch: ${colorG.lg(i.branch)}
454
- Last commit: ${colorG.lg(i.headHash)}, date: ${colorG.lg(i.headDdate)}
455
- Commit message: ${colorG.lg(i.headCommitMessage)}`);
456
- return i;
457
- };
458
-
459
- let scriptsDirName = fs.existsSync(path.join(CWD, '_sh/npm/yarn-ci.sh')) ? '_sh' : 'scripts';
460
-
461
- const reinstallDependencies = () => {
462
- logIt('CLEAN INSTALL DEPENDENCIES', true);
463
-
464
- execCommand('rm -rf node_modules/');
465
- execWithNODE('yarn install --frozen-lockfile');
466
- logIt('Dependencies installed');
467
-
468
- // Patch node modules if patch file exists
469
- const patchFile = path.join(scriptsDirName, 'patch_node_modules.js');
470
- if (fs.existsSync(patchFile)) {
471
- logIt('PATCH NODE MODULES', true);
472
- execWithNODE(`node --no-node-snapshot ${patchFile}`);
473
- logIt('Node modules patched');
474
- }
475
- };
476
-
477
- const compile = () => {
478
- logIt('TYPESCRIPT BUILD', true);
479
- execWithNODE('yarn cb', { silent: true });
480
- logIt('TypeScript build completed');
481
- };
482
-
483
- const buildQuasar = () => {
484
- if (!fs.existsSync(path.join(CWD, 'quasar.config.js'))) {
485
- return;
486
- }
487
- logIt('BUILD QUASAR', true);
488
- execWithNODE(`node ./${scriptsDirName}/quasar-prepare-color-vars.mjs`);
489
- const result = execWithNODE('yarn quasar build');
490
- logIt(result);
491
- logIt('Quasar build completed');
492
- };
493
-
494
- const restartService = ({ serviceName, serviceNameForPM2, serviceNameForSystemd }, args) => {
495
- let srvc = '';
496
- if (systemctlServiceExists(serviceNameForSystemd)) {
497
- srvc = 'systemctl';
498
- logIt(`Restarting service ${serviceNameForSystemd} via ${srvc}`, true);
499
- execCommand(`${srvc} restart "${serviceNameForSystemd}"`);
500
- } else if (pm2ServiceExists(serviceNameForPM2)) {
501
- srvc = 'pm2';
502
- logIt(`Restarting service ${serviceNameForPM2} via ${srvc}`, true);
503
- execCommand(`${srvc} restart "${serviceNameForPM2}"`);
504
- } else {
505
- logIt(`Service ${serviceName} not found in systemctl or PM2`);
506
- return;
507
- }
508
- logIt(`Service restarted`);
509
- };
510
-
511
- /**
512
- * Main update function
513
- */
514
- async function main () {
515
- logTryUpdate();
516
- fs.writeFileSync(runTimeLogFile, '');
517
- const args = parseArgs();
518
-
519
- if (args.help) {
520
- showHelp();
521
- return;
522
- }
523
-
524
- // Get service information
525
- const { serviceName, serviceNameForPM2, serviceNameForSystemd } = getServiceName();
526
-
527
- logIt(`<status>Update <y>${colorG.y(serviceName)}</y> ${nowPretty()}`);
528
-
529
- logIt(`Working directory: ${colorG.y(CWD)}`);
530
- // Load configuration
531
- const config = loadConfig();
532
-
533
- let from = ' DEFAULT';
534
- if (nodeVersion) {
535
- from = ' .envrc';
536
- } else if (config.nodeVersion) {
537
- nodeVersion = config.nodeVersion;
538
- from = ' deploy/config.yaml';
539
- }
540
-
541
- logIt(`Using Node.js version: ${nodeVersion || DEFAULT_NODE_VERSION}${from}`);
542
-
543
- // Override branch if specified in arguments
544
- const expectedBranch = args.expectedBranch || config.branch;
545
- let updateDeployedLogFile = false;
546
- try {
547
- // 1) Если есть локальные изменения — откатить
548
- const hasChanges = execCommand('git status --porcelain').trim().length > 0;
549
- if (hasChanges) {
550
- logIt(`Found uncommited changes. Reset to HEAD...`);
551
- execCommand('git reset --hard HEAD');
552
- execCommand(`git clean -fd`);
553
- }
554
-
555
- let needUpdate = false;
556
- let updateReason = args.force ? 'force' : '';
557
- const repoInfo = getRepoInfo();
558
- let { branch, headHash, upstreamHash } = repoInfo;
559
-
560
- // 2) Если ветка не та — жестко переключиться на голову удаленной expectedBranch
561
- const expectedUpstream = `origin/${expectedBranch}`;
562
- if (branch !== expectedBranch) {
563
- needUpdate = true;
564
- updateReason += `${updateReason ? '. ' : ''}branch !== expectedBranch (${branch} != ${expectedBranch})`;
565
- logIt(`Switch to branch ${expectedBranch}...`);
566
- execCommand(`git fetch origin ${expectedBranch} --prune`);
567
- execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
568
- execCommand(`git reset --hard ${expectedUpstream}`);
569
- execCommand(`git clean -fd`);
570
- const i = printCurrenBranch();
571
- branch = i.branch;
572
- headHash = i.headHash;
573
- upstreamHash = i.upstreamHash;
574
- if (branch !== expectedBranch) {
575
- throw new Error(`Failed to switch to branch ${expectedBranch}`);
576
- }
577
- }
578
-
579
- if (headHash !== upstreamHash) {
580
- // 3) Ветка та же, но надо подтянуть изменения
581
- needUpdate = true;
582
- updateReason += `${updateReason ? '. ' : ''}headHash !== upstreamHash (${headHash} != ${upstreamHash})`;
583
- printCurrenBranch();
584
- logIt(`FOUND CHANGES. UPDATE branch ${expectedBranch}...`);
585
- execCommand(`git fetch origin ${expectedBranch} --prune`);
586
- execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
587
- execCommand(`git reset --hard ${expectedUpstream}`);
588
- execCommand(`git clean -fd`);
589
- printCurrenBranch();
590
- }
591
-
592
- if (needUpdate || args.force) {
593
- updateDeployedLogFile = true;
594
- logTryUpdate(updateReason);
595
- //reinstallDependencies();
596
- //compile();
597
- //buildQuasar();
598
- restartService({ serviceName, serviceNameForPM2, serviceNameForSystemd }, args);
599
-
600
- // Add completion info to build log
601
- logIt(`Update completed successfully at ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`);
602
- // Send build notification if email is configured
603
- if (config.email) {
604
- await sendBuildNotification(config.email, 'SUCCESS', logBuffer, serviceName);
605
- } else {
606
- logIt('EMAIL not found');
607
- }
608
- }
609
- } catch (err) {
610
- const message = String(err.message).includes(err.stderr)
611
- ? err.message
612
- : [err.stderr, err.message].join('\n');
613
- logError(message);
614
- if (config.email) {
615
- await sendBuildNotification(config.email, 'FAIL', logBuffer, serviceName);
616
- }
617
- } finally {
618
- logIt('#FINISH#');
619
- if (updateDeployedLogFile) {
620
- fs.copyFileSync(runTimeLogFile, lastDeployLogFile);
621
- }
622
- execCommand(`rm -rf "${runTimeLogFile}"`);
623
- }
624
- }
625
-
626
- // Handle process termination gracefully
627
- process.on('SIGINT', () => {
628
- console.log('\nUpdate process interrupted');
629
- process.exit(1);
630
- });
631
-
632
- process.on('SIGTERM', () => {
633
- console.log('\nUpdate process terminated');
634
- process.exit(1);
635
- });
636
-
637
- main().then(() => {
638
- process.exit(0);
639
- }).catch((error) => {
640
- console.error('Update failed:', error.message);
641
- process.exit(1);
642
- });
1
+ #!/usr/bin/env node
2
+
3
+ // noinspection UnnecessaryLocalVariableJS
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { execSync, spawn } = require('child_process');
8
+ const os = require('os');
9
+
10
+ const version = '2025.11.25-0506';
11
+ console.log(`Update script version: ${version}`);
12
+
13
+ // Name of this folder
14
+ const scriptDirName = require('path').basename(__dirname);
15
+ // Changing the working directory to a script directory
16
+ process.chdir(__dirname);
17
+ const CWD = process.cwd();
18
+ const VON = path.resolve(path.join(CWD, '..'));
19
+
20
+ // Default configuration
21
+ const DEFAULT_CONFIG = {
22
+ branch: 'master',
23
+ email: '',
24
+ };
25
+
26
+ // Colors for terminal output
27
+ const colors = {
28
+ reset: '\x1b[0m',
29
+ bright: '\x1b[1m',
30
+ dim: '\x1b[2m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ blue: '\x1b[34m',
35
+ magenta: '\x1b[35m',
36
+ cyan: '\x1b[36m',
37
+ white: '\x1b[37m',
38
+ };
39
+
40
+ const color = {};
41
+ const colorG = {};
42
+ ['cyan', 'green', 'magenta', 'red', 'yellow'].forEach((col) => {
43
+ const firstLetter = col[0];
44
+ color[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.reset}`;
45
+ color[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.reset}`;
46
+ colorG[firstLetter] = (text) => `${colors.bright}${colors[col]}${text}${colors.green}`;
47
+ colorG[`l${firstLetter}`] = (text) => `${colors[col]}${text}${colors.green}`;
48
+ });
49
+
50
+ // Echo functions with colors
51
+ const echo = {
52
+ c: (text) => console.log(color.c(text)),
53
+ lc: (text) => console.log(color.lc(text)),
54
+ g: (text) => console.log(color.g(text)),
55
+ lg: (text) => console.log(color.lg(text)),
56
+ m: (text) => console.log(color.m(text)),
57
+ lm: (text) => console.log(color.lm(text)),
58
+ r: (text) => console.log(color.r(text)),
59
+ lr: (text) => console.log(color.lr(text)),
60
+ y: (text) => console.log(color.y(text)),
61
+ ly: (text) => console.log(color.ly(text)),
62
+ lg_no_newline: (msg) => process.stdout.write(color.lg(msg)),
63
+ };
64
+
65
+ let logBuffer = '';
66
+
67
+ // Global variable to store NVM environment
68
+ let setupScript = '';
69
+ let nodeVersion = null;
70
+ const DEFAULT_NODE_VERSION = '22.17.1';
71
+
72
+ const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0].replace('T', '');
73
+ const ytdl = timestamp.slice(2, 14); // YYMMDDHHMMSS format
74
+ const runTimeLogFile = path.join(VON, `deploy__${scriptDirName}__processing__${ytdl}.log`);
75
+ const lastDeployLogFile = path.join(VON, `deploy__${scriptDirName}__last_deploy.log`);
76
+ const cumulativeLogFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
77
+
78
+ const clearColors = (text) => text.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
79
+ const clearHtmlColors = (text) => text.replace(/<\/?(red|y|g|r|status)>/g, '');
80
+
81
+ const logIt = (msg, isTitle) => {
82
+ if (isTitle) {
83
+ const lng = 60 - (msg.length + 2);
84
+ const left = Math.floor(lng / 2);
85
+ const right = lng - left;
86
+ msg = `${'='.repeat(left)} ${msg} ${'='.repeat(right)}`;
87
+ }
88
+ const msg4console = clearHtmlColors(msg);
89
+ echo.g(msg4console);
90
+ logBuffer += `${msg}\n`;
91
+ fs.appendFileSync(runTimeLogFile, `${clearColors(msg4console)}\n`);
92
+ };
93
+
94
+ const logError = (msg) => {
95
+ console.error(color.r(msg));
96
+ const msg2 = `[ERROR] ${msg}`;
97
+ logBuffer += `<red>${msg2}</red>\n`;
98
+ fs.appendFileSync(cumulativeLogFile, `${msg2}\n`);
99
+ };
100
+
101
+ const nowPretty = () => new Date().toISOString().replace('T', ' ').substring(0, 19) + 'Z';
102
+
103
+ /**
104
+ * Truncate cumulative log file if it exceeds 2MB, keeping last 10KB
105
+ */
106
+ const truncateCumulativeLogIfNeeded = () => {
107
+ const logFile = path.join(VON, `deploy__${scriptDirName}__cumulative.log`);
108
+ const maxFileSize = 2 * 1024 * 1024; // 2MB
109
+ const keepSize = 10 * 1024; // 10KB
110
+
111
+ try {
112
+ if (fs.existsSync(logFile)) {
113
+ const stats = fs.statSync(logFile);
114
+ if (stats.size > maxFileSize) {
115
+ // Read last 10KB
116
+ const fd = fs.openSync(logFile, 'r');
117
+ const buffer = Buffer.alloc(keepSize);
118
+
119
+ // Position to last 10KB
120
+ fs.readSync(fd, buffer, 0, keepSize, stats.size - keepSize);
121
+ fs.closeSync(fd);
122
+
123
+ // Write back only the last 10KB
124
+ const tailContent = buffer.toString('utf8').replace(/^[\r\n]*/, ''); // Remove leading newlines
125
+ fs.writeFileSync(logFile, tailContent);
126
+
127
+ logIt(`Cumulative log truncated to ${Math.round(tailContent.length / 1024)}KB`);
128
+ }
129
+ }
130
+ } catch (error) {
131
+ logError(`Failed to truncate cumulative log: ${error.message}`);
132
+ }
133
+ };
134
+
135
+ const logTryUpdate = (updateReason = '') => {
136
+ truncateCumulativeLogIfNeeded();
137
+ updateReason = updateReason ? `Update reason: ${updateReason}` : '';
138
+ const message = updateReason || nowPretty();
139
+ fs.appendFileSync(cumulativeLogFile, `${message}\n`);
140
+ };
141
+
142
+ /**
143
+ * Execute command in NVM environment
144
+ */
145
+ function execCommand (command, options = {}, withSetupScript = false) {
146
+ // If we have NVM setup, wrap the command
147
+ const fullCommand = (setupScript && withSetupScript) ? `${setupScript} && ${command}` : command;
148
+ try {
149
+ const result = execSync(fullCommand, {
150
+ encoding: 'utf8',
151
+ stdio: options.silent ? 'inherit' : 'pipe',
152
+ shell: '/bin/bash',
153
+ ...options,
154
+ });
155
+ return result;
156
+ } catch (error) {
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ function execWithNODE (command, options = {}) {
162
+ return execCommand(command, options, true);
163
+ }
164
+
165
+ /**
166
+ * Load NVM environment and get Node.js version
167
+ */
168
+ function loadNVMEnvironment () {
169
+ try {
170
+ if (fs.existsSync('.envrc')) {
171
+ const envrcContent = fs.readFileSync('.envrc', 'utf8');
172
+
173
+ // Extract Node.js version from .envrc for logging
174
+ const nodeVersionMatch = envrcContent.match(/nvm use\s+([0-9.]+)/);
175
+ const nodeV = nodeVersionMatch ? nodeVersionMatch[1] : null;
176
+
177
+ if (nodeV) {
178
+ nodeVersion = nodeV;
179
+ }
180
+ setupScript = 'source .envrc';
181
+ }
182
+ } catch (error) {
183
+ logError('Error loading .envrc file');
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Parse command line arguments
189
+ */
190
+ function parseArgs () {
191
+ const pArgs = process.argv.slice(2);
192
+ const args = {
193
+ expectedBranch: null,
194
+ help: false,
195
+ force: false,
196
+ };
197
+
198
+ for (let i = 0; i < pArgs.length; i++) {
199
+ const arg = pArgs[i];
200
+ switch (arg) {
201
+ case '-b':
202
+ case '--branch':
203
+ args.expectedBranch = pArgs[++i];
204
+ break;
205
+ case '-f':
206
+ case '--force':
207
+ args.force = true;
208
+ break;
209
+ case '-?':
210
+ case '--help':
211
+ args.help = true;
212
+ break;
213
+ }
214
+ }
215
+
216
+ return args;
217
+ }
218
+
219
+ /**
220
+ * Show help information
221
+ */
222
+ function showHelp () {
223
+ console.log(`
224
+ ================================================================================
225
+ Project update and rebuild
226
+
227
+ Usage:
228
+ node update.js [Options]
229
+
230
+ Options:
231
+
232
+ -b|--branch
233
+ GIT branch name. Default - master
234
+ -l|--log
235
+ Switch to log display mode after completion
236
+ -?|--help
237
+ Display help
238
+
239
+ Example: node update.js -b production -l
240
+ ================================================================================
241
+ `);
242
+ }
243
+
244
+ /**
245
+ * Parse simple YAML content (key: value format)
246
+ */
247
+ function parseSimpleYAML (content) {
248
+ const config = {};
249
+ const lines = content.split('\n');
250
+
251
+ for (const line of lines) {
252
+ const trimmed = line.trim();
253
+ // Skip empty lines and comments
254
+ if (trimmed && !trimmed.startsWith('#')) {
255
+ // Parse key: value pairs
256
+ const colonIndex = trimmed.indexOf(':');
257
+ if (colonIndex > 0) {
258
+ const [, key, valueRaw] = trimmed.match(/^\s*([^:]+?)\s*:\s*(.*)\s*$/) || [];
259
+ let value = valueRaw ? valueRaw.replace(/^(['"])(.*)\1$/, '$2') : '';
260
+ // Handle empty values
261
+ if (value === 'null' || value === '~') {
262
+ value = '';
263
+ }
264
+ config[key] = value;
265
+ }
266
+ }
267
+ }
268
+
269
+ return config;
270
+ }
271
+
272
+ /**
273
+ * Load configuration from YAML file
274
+ */
275
+ function loadConfig () {
276
+ // Load NVM environment from .envrc
277
+ loadNVMEnvironment();
278
+
279
+ const configFile = path.join(process.cwd(), 'deploy', 'config.yml');
280
+
281
+ // Get Node.js version from NVM environment if available
282
+ if (!fs.existsSync(configFile)) {
283
+ return { ...DEFAULT_CONFIG };
284
+ }
285
+
286
+ try {
287
+ const configContent = fs.readFileSync(configFile, 'utf8');
288
+ const config = parseSimpleYAML(configContent);
289
+
290
+ return {
291
+ branch: config.branch || DEFAULT_CONFIG.branch,
292
+ nodeVersion: config.nodeVersion,
293
+ email: config.email || DEFAULT_CONFIG.email,
294
+ };
295
+ } catch (error) {
296
+ console.warn(`Warning: Could not parse config file ${configFile}:`, error.message);
297
+ return { ...DEFAULT_CONFIG };
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Get service name from package.json and .env
303
+ */
304
+ function getServiceName () {
305
+ let serviceName = '';
306
+ let serviceInstance = '';
307
+ try {
308
+ if (fs.existsSync('.env')) {
309
+ const envContent = fs.readFileSync('.env', 'utf8');
310
+ let match = envContent.match(/^SERVICE_NAME=([^\r\n]+)/m);
311
+ if (match) {
312
+ serviceName = match[1].trim();
313
+ }
314
+ match = envContent.match(/^SERVICE_INSTANCE=([^\r\n]+)/m);
315
+ if (match) {
316
+ serviceInstance = `--${match[1].trim()}`;
317
+ }
318
+ }
319
+ if (!serviceName) {
320
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
321
+ serviceName = packageJson.name;
322
+ }
323
+
324
+ return {
325
+ serviceName,
326
+ serviceNamePM: `${serviceName}${serviceInstance}`,
327
+ };
328
+ } catch (error) {
329
+ console.error('Error getting service name:', error.message);
330
+ process.exit(1);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Check if systemctl service exists
336
+ */
337
+ function systemctlServiceExists (serviceName) {
338
+ try {
339
+ const res = execCommand(`systemctl list-unit-files "${serviceName}.service"`);
340
+ return true;
341
+ } catch {
342
+ return false;
343
+ }
344
+ }
345
+
346
+ function pm2ServiceExists (serviceName) {
347
+ try {
348
+ const res = execCommand(`pm2 id "${serviceName}"`);
349
+ return /\[\s*\d\s*]/.test(res);
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Get git repository information
357
+ */
358
+ function getRepoInfo () {
359
+ try {
360
+ const branch = execCommand('git rev-parse --abbrev-ref HEAD').trim();
361
+ const headHash = execCommand('git rev-parse HEAD').trim();
362
+ // const headShortHash = execCommand('git rev-parse --short HEAD').trim();
363
+ const headCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${headHash}`).trim();
364
+ const headDdate = execCommand(`git log -n 1 --format="%at" ${headHash} | xargs -I{} date -d @{} +%d.%m.%Y_%H:%M:%S`).trim();
365
+ execCommand(`git fetch origin ${branch} --prune`);
366
+
367
+ const upstreamHash = execCommand(`git rev-parse ${branch}@{upstream}`).trim();
368
+ // const upstreamShortHash = execCommand(`git rev-parse --short ${branch}@{upstream}`).trim();
369
+ // const upstreamCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${upstreamHash}`).trim();
370
+
371
+ return {
372
+ branch,
373
+ headDdate,
374
+ headHash,
375
+ headCommitMessage,
376
+ upstreamHash,
377
+ };
378
+ } catch (error) {
379
+ const message = String(error.message).includes(error.stderr)
380
+ ? error.message
381
+ : [error.stderr, error.message].join('\n');
382
+
383
+ console.error('Error getting repo info:', message);
384
+ return null;
385
+ }
386
+ }
387
+
388
+ const colorizeHTML = (text) => text
389
+ .replace(/<red>/g, '<span style="color:#ff0000;">')
390
+ .replace(/<\/red>/g, '</span>')
391
+ .replace(/<y>/g, '<span style="background-color:#ffff00;">')
392
+ .replace(/<\/y>/g, '</span>')
393
+ .replace(/<g>/g, '<span style="background-color:#00ff00;">')
394
+ .replace(/<\/g>/g, '</span>')
395
+ .replace(/<r>/g, '<span style="background-color:#ff0000; color:#ffffff;">')
396
+ .replace(/<\/r>/g, '</span>')
397
+ .replace(/\[ERROR]/g, '<span style="color:#ffffff; background-color: #ff0000">[ERROR]</span>');
398
+
399
+ async function sendBuildNotification (emails, status, body, serviceName) {
400
+ if (!emails) { return; }
401
+ let s = '';
402
+ if (status === 'FAIL') {
403
+ s = `<r>FAIL</r> `;
404
+ } else if (status === 'SUCCESS') {
405
+ s = `<g>SUCCESS</g> `;
406
+ }
407
+ body = body.replace('<status>', s);
408
+
409
+ // Create HTML email content
410
+ const hostname = os.hostname();
411
+ const htmlContent = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
412
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
413
+ <head>
414
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
415
+ <meta name="viewport" content="width=device-width, initial-scale=1">
416
+ <title>${status} Update ${serviceName} (on ${hostname})</title>
417
+ </head>
418
+ <body>
419
+ <pre>
420
+ ${colorizeHTML(clearColors(body))}
421
+ </pre></body></html>`;
422
+
423
+ // Send to each email address
424
+ const emailArray = emails.split(',').map((email) => email.trim()).filter((email) => email);
425
+
426
+ for (let i = 0; i < emailArray.length; i++) {
427
+ const emailAddress = emailArray[i];
428
+ try {
429
+ logIt(`Sending update notification to: ${emailAddress}`);
430
+ const subject = `${status} Update: ${serviceName} (on ${hostname})`;
431
+
432
+ const command = `mail -a "Content-Type: text/html; charset=UTF-8" -s "${subject.replace(/"/g, '\\"')}" "${emailAddress}"`;
433
+ const child = spawn('/bin/bash', ['-lc', command], { stdio: ['pipe', 'inherit', 'inherit'] });
434
+ child.stdin.write(htmlContent);
435
+ child.stdin.end();
436
+
437
+ await new Promise((resolve, reject) => {
438
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`mail exit code ${code}`))));
439
+ child.on('error', reject);
440
+ });
441
+ } catch (error) {
442
+ console.error(`Failed to send email to ${emailAddress}:`, error.message);
443
+ }
444
+ }
445
+ }
446
+
447
+ const printCurrenBranch = () => {
448
+ const i = getRepoInfo();
449
+ logIt(`Current branch: ${colorG.lg(i.branch)}
450
+ Last commit: ${colorG.lg(i.headHash)}, date: ${colorG.lg(i.headDdate)}
451
+ Commit message: ${colorG.lg(i.headCommitMessage)}`);
452
+ return i;
453
+ };
454
+
455
+ let scriptsDirName = fs.existsSync(path.join(CWD, '_sh/npm/yarn-ci.sh')) ? '_sh' : 'scripts';
456
+
457
+ const reinstallDependencies = () => {
458
+ logIt('CLEAN INSTALL DEPENDENCIES', true);
459
+
460
+ execCommand('rm -rf node_modules/');
461
+ execWithNODE('yarn install --frozen-lockfile');
462
+ logIt('Dependencies installed');
463
+
464
+ // Patch node modules if patch file exists
465
+ const patchFile = path.join(scriptsDirName, 'patch_node_modules.js');
466
+ if (fs.existsSync(patchFile)) {
467
+ logIt('PATCH NODE MODULES', true);
468
+ execWithNODE(`node --no-node-snapshot ${patchFile}`);
469
+ logIt('Node modules patched');
470
+ }
471
+ };
472
+
473
+ const compile = () => {
474
+ logIt('TYPESCRIPT BUILD', true);
475
+ execWithNODE('yarn cb', { silent: true });
476
+ logIt('TypeScript build completed');
477
+ };
478
+
479
+ const buildQuasar = () => {
480
+ if (!fs.existsSync(path.join(CWD, 'quasar.config.js'))) {
481
+ return;
482
+ }
483
+ logIt('BUILD QUASAR', true);
484
+ execWithNODE(`node ./${scriptsDirName}/quasar-prepare-color-vars.mjs`);
485
+ const result = execWithNODE('yarn quasar build');
486
+ logIt(result);
487
+ logIt('Quasar build completed');
488
+ };
489
+
490
+ const restartService = (serviceNamePM) => {
491
+ let srvc = '';
492
+ if (systemctlServiceExists(serviceNamePM)) {
493
+ srvc = 'systemctl';
494
+ } else if (pm2ServiceExists(serviceNamePM)) {
495
+ srvc = 'pm2';
496
+ } else {
497
+ logIt(`Service ${serviceNamePM} not found in systemctl or PM2`);
498
+ return;
499
+ }
500
+ logIt(`Restarting service ${serviceNamePM} via ${srvc}`, true);
501
+ execCommand(`${srvc} restart "${serviceNamePM}"`);
502
+ logIt(`Service restarted`);
503
+ };
504
+
505
+ /**
506
+ * Main update function
507
+ */
508
+ async function main () {
509
+ logTryUpdate();
510
+ fs.writeFileSync(runTimeLogFile, '');
511
+ const args = parseArgs();
512
+
513
+ if (args.help) {
514
+ showHelp();
515
+ return;
516
+ }
517
+
518
+ // Get service information
519
+ const { serviceName, serviceNamePM } = getServiceName();
520
+
521
+ logIt(`<status>Update <y>${colorG.y(serviceName)}</y> ${nowPretty()}`);
522
+
523
+ logIt(`Working directory: ${colorG.y(CWD)}`);
524
+ // Load configuration
525
+ const config = loadConfig();
526
+
527
+ let from = ' DEFAULT';
528
+ if (nodeVersion) {
529
+ from = ' .envrc';
530
+ } else if (config.nodeVersion) {
531
+ nodeVersion = config.nodeVersion;
532
+ from = ' deploy/config.yaml';
533
+ }
534
+
535
+ logIt(`Using Node.js version: ${nodeVersion || DEFAULT_NODE_VERSION}${from}`);
536
+
537
+ // Override branch if specified in arguments
538
+ const expectedBranch = args.expectedBranch || config.branch;
539
+ let updateDeployedLogFile = false;
540
+ try {
541
+ // 1) If there are local changes, roll back
542
+ const hasChanges = execCommand('git status --porcelain').trim().length > 0;
543
+ if (hasChanges) {
544
+ logIt(`Found uncommited changes. Reset to HEAD...`);
545
+ execCommand('git reset --hard HEAD');
546
+ execCommand(`git clean -fd`);
547
+ }
548
+
549
+ let needUpdate = false;
550
+ let updateReason = args.force ? 'force' : '';
551
+ const repoInfo = getRepoInfo();
552
+ let { branch, headHash, upstreamHash } = repoInfo;
553
+
554
+ // 2) If the branch is not the same, hard switch to the head of the deleted expectedBranch
555
+ const expectedUpstream = `origin/${expectedBranch}`;
556
+ if (branch !== expectedBranch) {
557
+ needUpdate = true;
558
+ updateReason += `${updateReason ? '. ' : ''}branch !== expectedBranch (${branch} != ${expectedBranch})`;
559
+ logIt(`Switch to branch ${expectedBranch}...`);
560
+ execCommand(`git fetch origin ${expectedBranch} --prune`);
561
+ execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
562
+ execCommand(`git reset --hard ${expectedUpstream}`);
563
+ execCommand(`git clean -fd`);
564
+ const i = printCurrenBranch();
565
+ branch = i.branch;
566
+ headHash = i.headHash;
567
+ upstreamHash = i.upstreamHash;
568
+ if (branch !== expectedBranch) {
569
+ throw new Error(`Failed to switch to branch ${expectedBranch}`);
570
+ }
571
+ }
572
+
573
+ if (headHash !== upstreamHash) {
574
+ // 3) The branch is the same, but we need to tighten up the changes
575
+ needUpdate = true;
576
+ updateReason += `${updateReason ? '. ' : ''}headHash !== upstreamHash (${headHash} != ${upstreamHash})`;
577
+ printCurrenBranch();
578
+ logIt(`FOUND CHANGES. UPDATE branch ${expectedBranch}...`);
579
+ execCommand(`git fetch origin ${expectedBranch} --prune`);
580
+ execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
581
+ execCommand(`git reset --hard ${expectedUpstream}`);
582
+ execCommand(`git clean -fd`);
583
+ printCurrenBranch();
584
+ }
585
+
586
+ if (needUpdate || args.force) {
587
+ updateDeployedLogFile = true;
588
+ logTryUpdate(updateReason);
589
+ reinstallDependencies();
590
+ compile();
591
+ buildQuasar();
592
+ restartService(serviceNamePM);
593
+
594
+ // Add completion info to build log
595
+ logIt(`Update completed successfully at ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`);
596
+ // Send build notification if email is configured
597
+ if (config.email) {
598
+ await sendBuildNotification(config.email, 'SUCCESS', logBuffer, serviceName);
599
+ } else {
600
+ logIt('EMAIL not found');
601
+ }
602
+ } else {
603
+ logIt('No changes detected. Update skipped.');
604
+ }
605
+ } catch (err) {
606
+ const message = String(err.message).includes(err.stderr)
607
+ ? err.message
608
+ : [err.stderr, err.message].join('\n');
609
+ logError(message);
610
+ if (config.email) {
611
+ await sendBuildNotification(config.email, 'FAIL', logBuffer, serviceName);
612
+ }
613
+ } finally {
614
+ logIt('#FINISH#');
615
+ if (updateDeployedLogFile) {
616
+ fs.copyFileSync(runTimeLogFile, lastDeployLogFile);
617
+ }
618
+ execCommand(`rm -rf "${runTimeLogFile}"`);
619
+ }
620
+ }
621
+
622
+ // Handle process termination gracefully
623
+ process.on('SIGINT', () => {
624
+ console.log('\nUpdate process interrupted');
625
+ process.exit(1);
626
+ });
627
+
628
+ process.on('SIGTERM', () => {
629
+ console.log('\nUpdate process terminated');
630
+ process.exit(1);
631
+ });
632
+
633
+ main().then(() => {
634
+ process.exit(0);
635
+ }).catch((error) => {
636
+ console.error('Update failed:', error.message);
637
+ process.exit(1);
638
+ });