fa-mcp-sdk 0.4.9 → 0.4.12

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.
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.29.0",
52
52
  "dotenv": "^17.4.1",
53
- "fa-mcp-sdk": "^0.4.9"
53
+ "fa-mcp-sdk": "^0.4.12"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/express": "^5.0.6",
@@ -1,638 +1,638 @@
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
- });
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 {
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
+ 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) {
401
+ return;
402
+ }
403
+ let s = '';
404
+ if (status === 'FAIL') {
405
+ s = `<r>FAIL</r> `;
406
+ } else if (status === 'SUCCESS') {
407
+ s = `<g>SUCCESS</g> `;
408
+ }
409
+ body = body.replace('<status>', s);
410
+
411
+ // Create HTML email content
412
+ const hostname = os.hostname();
413
+ const htmlContent = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
414
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en">
415
+ <head>
416
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
417
+ <meta name="viewport" content="width=device-width, initial-scale=1">
418
+ <title>${status} Update ${serviceName} (on ${hostname})</title>
419
+ </head>
420
+ <body>
421
+ <pre>
422
+ ${colorizeHTML(clearColors(body))}
423
+ </pre></body></html>`;
424
+
425
+ // Send to each email address
426
+ const emailArray = emails.split(',').map((email) => email.trim()).filter((email) => email);
427
+
428
+ for (let i = 0; i < emailArray.length; i++) {
429
+ const emailAddress = emailArray[i];
430
+ try {
431
+ logIt(`Sending update notification to: ${emailAddress}`);
432
+ const subject = `${status} Update: ${serviceName} (on ${hostname})`;
433
+
434
+ const command = `mail -a "Content-Type: text/html; charset=UTF-8" -s "${subject.replace(/"/g, '\\"')}" "${emailAddress}"`;
435
+ const child = spawn('/bin/bash', ['-lc', command], { stdio: ['pipe', 'inherit', 'inherit'] });
436
+ child.stdin.write(htmlContent);
437
+ child.stdin.end();
438
+
439
+ await new Promise((resolve, reject) => {
440
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`mail exit code ${code}`))));
441
+ child.on('error', reject);
442
+ });
443
+ } catch (error) {
444
+ console.error(`Failed to send email to ${emailAddress}:`, error.message);
445
+ }
446
+ }
447
+ }
448
+
449
+ const printCurrenBranch = () => {
450
+ const i = getRepoInfo();
451
+ logIt(`Current branch: ${colorG.lg(i.branch)}
452
+ Last commit: ${colorG.lg(i.headHash)}, date: ${colorG.lg(i.headDdate)}
453
+ Commit message: ${colorG.lg(i.headCommitMessage)}`);
454
+ return i;
455
+ };
456
+
457
+ let scriptsDirName = fs.existsSync(path.join(CWD, '_sh/npm/yarn-ci.sh')) ? '_sh' : 'scripts';
458
+
459
+ const reinstallDependencies = () => {
460
+ logIt('CLEAN INSTALL DEPENDENCIES', true);
461
+
462
+ execCommand('rm -rf node_modules/');
463
+ execWithNODE('yarn install --frozen-lockfile');
464
+ logIt('Dependencies installed');
465
+
466
+ // Patch node modules if patch file exists
467
+ const patchFile = path.join(scriptsDirName, 'patch_node_modules.js');
468
+ if (fs.existsSync(patchFile)) {
469
+ logIt('PATCH NODE MODULES', true);
470
+ execWithNODE(`node --no-node-snapshot ${patchFile}`);
471
+ logIt('Node modules patched');
472
+ }
473
+ };
474
+
475
+ const compile = () => {
476
+ logIt('TYPESCRIPT BUILD', true);
477
+ execWithNODE('yarn cb', { silent: true });
478
+ logIt('TypeScript build completed');
479
+ };
480
+
481
+ const buildQuasar = () => {
482
+ if (!fs.existsSync(path.join(CWD, 'quasar.config.js'))) {
483
+ return;
484
+ }
485
+ logIt('BUILD QUASAR', true);
486
+ execWithNODE(`node ./${scriptsDirName}/quasar-prepare-color-vars.mjs`);
487
+ const result = execWithNODE('yarn quasar build');
488
+ logIt(result);
489
+ logIt('Quasar build completed');
490
+ };
491
+
492
+ const restartService = (serviceNamePM) => {
493
+ let srvc = '';
494
+ if (systemctlServiceExists(serviceNamePM)) {
495
+ srvc = 'systemctl';
496
+ } else if (pm2ServiceExists(serviceNamePM)) {
497
+ srvc = 'pm2';
498
+ } else {
499
+ logIt(`Service ${serviceNamePM} not found in systemctl or PM2`);
500
+ return;
501
+ }
502
+ logIt(`Restarting service ${serviceNamePM} via ${srvc}`, true);
503
+ execCommand(`${srvc} restart "${serviceNamePM}"`);
504
+ logIt(`Service restarted`);
505
+ };
506
+
507
+ /**
508
+ * Main update function
509
+ */
510
+ async function main () {
511
+ logTryUpdate();
512
+ fs.writeFileSync(runTimeLogFile, '');
513
+ const args = parseArgs();
514
+
515
+ if (args.help) {
516
+ showHelp();
517
+ return;
518
+ }
519
+
520
+ // Get service information
521
+ const { serviceName, serviceNamePM } = getServiceName();
522
+
523
+ logIt(`<status>Update <y>${colorG.y(serviceName)}</y> ${nowPretty()}`);
524
+
525
+ logIt(`Working directory: ${colorG.y(CWD)}`);
526
+ // Load configuration
527
+ const config = loadConfig();
528
+
529
+ let from = ' DEFAULT';
530
+ if (nodeVersion) {
531
+ from = ' .envrc';
532
+ } else if (config.nodeVersion) {
533
+ ({ nodeVersion } = config);
534
+ from = ' deploy/config.yaml';
535
+ }
536
+
537
+ logIt(`Using Node.js version: ${nodeVersion || DEFAULT_NODE_VERSION}${from}`);
538
+
539
+ // Override branch if specified in arguments
540
+ const expectedBranch = args.expectedBranch || config.branch;
541
+ let updateDeployedLogFile = false;
542
+ try {
543
+ // 1) If there are local changes, roll back
544
+ const hasChanges = execCommand('git status --porcelain').trim().length > 0;
545
+ if (hasChanges) {
546
+ logIt(`Found uncommited changes. Reset to HEAD...`);
547
+ execCommand('git reset --hard HEAD');
548
+ execCommand(`git clean -fd`);
549
+ }
550
+
551
+ let needUpdate = false;
552
+ let updateReason = args.force ? 'force' : '';
553
+ const repoInfo = getRepoInfo();
554
+ let { branch, headHash, upstreamHash } = repoInfo;
555
+
556
+ // 2) If the branch is not the same, hard switch to the head of the deleted expectedBranch
557
+ const expectedUpstream = `origin/${expectedBranch}`;
558
+ if (branch !== expectedBranch) {
559
+ needUpdate = true;
560
+ updateReason += `${updateReason ? '. ' : ''}branch !== expectedBranch (${branch} != ${expectedBranch})`;
561
+ logIt(`Switch to branch ${expectedBranch}...`);
562
+ execCommand(`git fetch origin ${expectedBranch} --prune`);
563
+ execCommand(`git checkout -B ${expectedBranch} ${expectedUpstream}`);
564
+ execCommand(`git reset --hard ${expectedUpstream}`);
565
+ execCommand(`git clean -fd`);
566
+ const i = printCurrenBranch();
567
+ ({ branch, headHash, upstreamHash } = i);
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
+ });
@@ -62,6 +62,8 @@
62
62
  <div class="form-group model-select-group">
63
63
  <label for="modelSelect">Model</label>
64
64
  <select id="modelSelect" name="modelSelect" required>
65
+ <option value="gpt-5.4">gpt-5.4</option>
66
+ <option value="gpt-5.3-codex">gpt-5.3-codex</option>
65
67
  <option value="gpt-5.2">gpt-5.2</option>
66
68
  <option value="gpt-5.1">gpt-5.1</option>
67
69
  <option value="gpt-5-nano">gpt-5-nano</option>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fa-mcp-sdk",
3
3
  "productName": "FA MCP SDK",
4
- "version": "0.4.9",
4
+ "version": "0.4.12",
5
5
  "description": "Core infrastructure and templates for building Model Context Protocol (MCP) servers with TypeScript",
6
6
  "type": "module",
7
7
  "main": "dist/core/index.js",