fa-mcp-sdk 0.2.84 → 0.2.87

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,631 +1,642 @@
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.22-1800';
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 serviceInstance = '';
308
- try {
309
- if (fs.existsSync('.env')) {
310
- const envContent = fs.readFileSync('.env', 'utf8');
311
- let match = envContent.match(/^SERVICE_NAME=([^\r\n]+)/m);
312
- if (match) {
313
- serviceName = match[1].trim();
314
- }
315
- match = envContent.match(/^SERVICE_INSTANCE=([^\r\n]+)/m);
316
- if (match) {
317
- serviceInstance = `--${match[1].trim()}`;
318
- }
319
- }
320
- if (!serviceName) {
321
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
322
- serviceName = packageJson.name;
323
- }
324
-
325
- return `${serviceName}${serviceInstance}`;
326
- } catch (error) {
327
- console.error('Error getting service name:', error.message);
328
- process.exit(1);
329
- }
330
- }
331
-
332
- /**
333
- * Check if systemctl service exists
334
- */
335
- function systemctlServiceExists (serviceName) {
336
- try {
337
- execCommand(`systemctl list-unit-files "${serviceName}.service"`);
338
- return true;
339
- } catch {
340
- return false;
341
- }
342
- }
343
-
344
- function pm2ServiceExists (serviceName) {
345
- try {
346
- execCommand(`pm2 id "${serviceName}"`);
347
- return true;
348
- } catch {
349
- return false;
350
- }
351
- }
352
-
353
- /**
354
- * Get git repository information
355
- */
356
- function getRepoInfo () {
357
- try {
358
- const branch = execCommand('git rev-parse --abbrev-ref HEAD').trim();
359
- const headHash = execCommand('git rev-parse HEAD').trim();
360
- // const headShortHash = execCommand('git rev-parse --short HEAD').trim();
361
- const headCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${headHash}`).trim();
362
- const headDdate = execCommand(`git log -n 1 --format="%at" ${headHash} | xargs -I{} date -d @{} +%d.%m.%Y_%H:%M:%S`).trim();
363
- execCommand(`git fetch origin ${branch} --prune`);
364
-
365
- const upstreamHash = execCommand(`git rev-parse ${branch}@{upstream}`).trim();
366
- // const upstreamShortHash = execCommand(`git rev-parse --short ${branch}@{upstream}`).trim();
367
- // const upstreamCommitMessage = execCommand(`git log -n 1 --pretty=format:%s ${upstreamHash}`).trim();
368
-
369
- return {
370
- branch,
371
- headDdate,
372
- headHash,
373
- headCommitMessage,
374
- upstreamHash,
375
- };
376
- } catch (error) {
377
- console.error('Error getting repo info:', error.message);
378
- return null;
379
- }
380
- }
381
-
382
- const colorizeHTML = (text) => text
383
- .replace(/<red>/g, '<span style="color:#ff0000;">')
384
- .replace(/<\/red>/g, '</span>')
385
- .replace(/<y>/g, '<span style="background-color:#ffff00;">')
386
- .replace(/<\/y>/g, '</span>')
387
- .replace(/<g>/g, '<span style="background-color:#00ff00;">')
388
- .replace(/<\/g>/g, '</span>')
389
- .replace(/<r>/g, '<span style="background-color:#ff0000; color:#ffffff;">')
390
- .replace(/<\/r>/g, '</span>')
391
- .replace(/\[ERROR]/g, '<span style="color:#ffffff; background-color: #ff0000">[ERROR]</span>');
392
-
393
- async function sendBuildNotification (emails, status, body, serviceName) {
394
- if (!emails) { return; }
395
- let s = '';
396
- if (status === 'FAIL') {
397
- s = `<r>FAIL</r> `;
398
- } else if (status === 'SUCCESS') {
399
- s = `<g>SUCCESS</g> `;
400
- }
401
- body = body.replace('<status>', s);
402
-
403
- // Create HTML email content
404
- const hostname = os.hostname();
405
- const htmlContent = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
406
- <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
407
- <head>
408
- <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
409
- <meta name="viewport" content="width=device-width, initial-scale=1">
410
- <title>${status} Update ${serviceName} (on ${hostname})</title>
411
- </head>
412
- <body>
413
- <pre>
414
- ${colorizeHTML(clearColors(body))}
415
- </pre></body></html>`;
416
-
417
- // Send to each email address
418
- const emailArray = emails.split(',').map((email) => email.trim()).filter((email) => email);
419
-
420
- for (let i = 0; i < emailArray.length; i++) {
421
- const emailAddress = emailArray[i];
422
- try {
423
- logIt(`Sending update notification to: ${emailAddress}`);
424
- const subject = `${status} Update: ${serviceName} (on ${hostname})`;
425
-
426
- // Подаем тело письма через stdin, задаем Content-Type
427
- const command = `mail -a "Content-Type: text/html; charset=UTF-8" -s "${subject.replace(/"/g, '\\"')}" "${emailAddress}"`;
428
- const child = spawn('/bin/bash', ['-lc', command], { stdio: ['pipe', 'inherit', 'inherit'] });
429
- child.stdin.write(htmlContent);
430
- child.stdin.end();
431
-
432
- await new Promise((resolve, reject) => {
433
- child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`mail exit code ${code}`))));
434
- child.on('error', reject);
435
- });
436
- } catch (error) {
437
- console.error(`Failed to send email to ${emailAddress}:`, error.message);
438
- }
439
- }
440
- }
441
-
442
- const printCurrenBranch = () => {
443
- const i = getRepoInfo();
444
- logIt(`Current branch: ${colorG.lg(i.branch)}
445
- Last commit: ${colorG.lg(i.headHash)}, date: ${colorG.lg(i.headDdate)}
446
- Commit message: ${colorG.lg(i.headCommitMessage)}`);
447
- return i;
448
- };
449
-
450
- let scriptsDirName = fs.existsSync(path.join(CWD, '_sh/npm/yarn-ci.sh')) ? '_sh' : 'scripts';
451
-
452
- const reinstallDependencies = () => {
453
- logIt('CLEAN INSTALL DEPENDENCIES', true);
454
-
455
- execCommand('rm -rf node_modules/');
456
- execWithNODE('yarn install --frozen-lockfile');
457
- logIt('Dependencies installed');
458
-
459
- // Patch node modules if patch file exists
460
- const patchFile = path.join(scriptsDirName, 'patch_node_modules.js');
461
- if (fs.existsSync(patchFile)) {
462
- logIt('PATCH NODE MODULES', true);
463
- execWithNODE(`node --no-node-snapshot ${patchFile}`);
464
- logIt('Node modules patched');
465
- }
466
- };
467
-
468
- const compile = () => {
469
- logIt('TYPESCRIPT BUILD', true);
470
- execWithNODE('yarn cb', { silent: true });
471
- logIt('TypeScript build completed');
472
- };
473
-
474
- const buildQuasar = () => {
475
- if (!fs.existsSync(path.join(CWD, 'quasar.config.js'))) {
476
- return;
477
- }
478
- logIt('BUILD QUASAR', true);
479
- execWithNODE(`node ./${scriptsDirName}/quasar-prepare-color-vars.mjs`);
480
- const result = execWithNODE('yarn quasar build');
481
- logIt(result);
482
- logIt('Quasar build completed');
483
- };
484
-
485
- const restartService = (serviceName, args) => {
486
- let srvc = '';
487
- if (systemctlServiceExists(serviceName)) {
488
- srvc = 'systemctl';
489
- } else if (pm2ServiceExists(serviceName)) {
490
- srvc = 'pm2';
491
- } else {
492
- logIt(`Service ${serviceName} not found in systemctl or PM2`);
493
- return;
494
- }
495
- logIt(`Restarting service ${serviceName} via ${srvc}`, true);
496
- execCommand(`${srvc} restart "${serviceName}"`);
497
- logIt(`Service restarted`);
498
- };
499
-
500
- /**
501
- * Main update function
502
- */
503
- async function main () {
504
- logTryUpdate();
505
- fs.writeFileSync(runTimeLogFile, '');
506
- const args = parseArgs();
507
-
508
- if (args.help) {
509
- showHelp();
510
- return;
511
- }
512
-
513
- // Get service information
514
- const serviceName = getServiceName();
515
-
516
- logIt(`<status>Update <y>${colorG.y(serviceName)}</y> ${nowPretty()}`);
517
-
518
- logIt(`Working directory: ${colorG.y(CWD)}`);
519
- // Load configuration
520
- const config = loadConfig();
521
-
522
- let from = ' DEFAULT';
523
- if (nodeVersion) {
524
- from = ' .envrc';
525
- } else if (config.nodeVersion) {
526
- nodeVersion = config.nodeVersion;
527
- from = ' deploy/config.yaml';
528
- }
529
-
530
- logIt(`Using Node.js version: ${nodeVersion || DEFAULT_NODE_VERSION}${from}`);
531
-
532
- // Override branch if specified in arguments
533
- const expectedBranch = args.expectedBranch || config.branch;
534
- let updateDeployedLogFile = false;
535
- try {
536
- // 1) Если есть локальные изменения — откатить
537
- const hasChanges = execCommand('git status --porcelain').trim().length > 0;
538
- if (hasChanges) {
539
- logIt(`Found uncommited changes. Reset to HEAD...`);
540
- execCommand('git reset --hard HEAD');
541
- execCommand(`git clean -fd`);
542
- }
543
-
544
- let needUpdate = false;
545
- let updateReason = args.force ? 'force' : '';
546
- const repoInfo = getRepoInfo();
547
- let { branch, headHash, upstreamHash } = repoInfo;
548
-
549
- // 2) Если ветка не та — жестко переключиться на голову удаленной expectedBranch
550
- const expectedUpstream = `origin/${expectedBranch}`;
551
- if (branch !== expectedBranch) {
552
- needUpdate = true;
553
- updateReason += `${updateReason ? '. ' : ''}branch !== expectedBranch (${branch} != ${expectedBranch})`;
554
- logIt(`Switch to branch ${expectedBranch}...`);
555
- execCommand(`git fetch origin ${expectedBranch} --prune`);
556
- execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
557
- execCommand(`git reset --hard ${expectedUpstream}`);
558
- execCommand(`git clean -fd`);
559
- const i = printCurrenBranch();
560
- branch = i.branch;
561
- headHash = i.headHash;
562
- upstreamHash = i.upstreamHash;
563
- if (branch !== expectedBranch) {
564
- throw new Error(`Failed to switch to branch ${expectedBranch}`);
565
- }
566
- }
567
-
568
- if (headHash !== upstreamHash) {
569
- // 3) Ветка та же, но надо подтянуть изменения
570
- needUpdate = true;
571
- updateReason += `${updateReason ? '. ' : ''}headHash !== upstreamHash (${headHash} != ${upstreamHash})`;
572
- printCurrenBranch();
573
- logIt(`FOUND CHANGES. UPDATE branch ${expectedBranch}...`);
574
- execCommand(`git fetch origin ${expectedBranch} --prune`);
575
- execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
576
- execCommand(`git reset --hard ${expectedUpstream}`);
577
- execCommand(`git clean -fd`);
578
- printCurrenBranch();
579
- }
580
-
581
- if (needUpdate || args.force) {
582
- updateDeployedLogFile = true;
583
- logTryUpdate(updateReason);
584
- reinstallDependencies();
585
- compile();
586
- buildQuasar();
587
- restartService(serviceName, args);
588
-
589
- // Add completion info to build log
590
- logIt(`Update completed successfully at ${new Date().toISOString().replace('T', ' ').substring(0, 19)}`);
591
- // Send build notification if email is configured
592
- if (config.email) {
593
- await sendBuildNotification(config.email, 'SUCCESS', logBuffer, serviceName);
594
- } else {
595
- logIt('EMAIL not found');
596
- }
597
- }
598
- } catch (err) {
599
- const message = String(err.message).includes(err.stderr)
600
- ? err.message
601
- : [err.stderr, err.message].join('\n');
602
- logError(message);
603
- if (config.email) {
604
- await sendBuildNotification(config.email, 'FAIL', logBuffer, serviceName);
605
- }
606
- } finally {
607
- logIt('#FINISH#');
608
- if (updateDeployedLogFile) {
609
- fs.copyFileSync(runTimeLogFile, lastDeployLogFile);
610
- }
611
- execCommand(`rm -rf "${runTimeLogFile}"`);
612
- }
613
- }
614
-
615
- // Handle process termination gracefully
616
- process.on('SIGINT', () => {
617
- console.log('\nUpdate process interrupted');
618
- process.exit(1);
619
- });
620
-
621
- process.on('SIGTERM', () => {
622
- console.log('\nUpdate process terminated');
623
- process.exit(1);
624
- });
625
-
626
- main().then(() => {
627
- process.exit(0);
628
- }).catch((error) => {
629
- console.error('Update failed:', error.message);
630
- process.exit(1);
631
- });
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
+ });