maiass 5.7.31

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.
@@ -0,0 +1,902 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import colors from './colors.js';
5
+ import { SYMBOLS } from './symbols.js';
6
+ import { log, logger } from './logger.js';
7
+ import { loadEnvironmentConfig } from './config.js';
8
+ import { debuglog } from 'util';
9
+
10
+ /**
11
+ * Execute git command safely
12
+ * @param {string} command - Git command to execute
13
+ * @param {boolean} silent - Whether to suppress errors
14
+ * @returns {Promise<Object>} Command result with success, output, and error
15
+ */
16
+ function executeGitCommand(command, silent = false) {
17
+ try {
18
+ const result = execSync(`git ${command}`, {
19
+ encoding: 'utf8',
20
+ stdio: silent ? 'pipe' : ['pipe', 'pipe', 'ignore']
21
+ });
22
+
23
+ return {
24
+ success: true,
25
+ output: result.trim(),
26
+ error: null
27
+ };
28
+ } catch (error) {
29
+ return {
30
+ success: false,
31
+ output: '',
32
+ error: error.message
33
+ };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Supported version file types and their parsing/updating logic
39
+ */
40
+ const VERSION_FILE_TYPES = {
41
+ json: {
42
+ extensions: ['.json'],
43
+ detect: (content) => {
44
+ try {
45
+ const parsed = JSON.parse(content);
46
+ return parsed.version !== undefined;
47
+ } catch {
48
+ return false;
49
+ }
50
+ },
51
+ extract: (content) => {
52
+ try {
53
+ const parsed = JSON.parse(content);
54
+ return parsed.version || null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ },
59
+ update: (content, newVersion) => {
60
+ try {
61
+ const parsed = JSON.parse(content);
62
+ parsed.version = newVersion;
63
+ return JSON.stringify(parsed, null, 2) + '\n';
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ },
69
+ text: {
70
+ extensions: ['.txt', '.version', ''],
71
+ detect: (content, filename) => {
72
+ // Simple text files that contain just a version number
73
+ const basename = path.basename(filename).toLowerCase();
74
+ if (basename === 'version' || basename.includes('version')) {
75
+ return /^\d+\.\d+\.\d+/.test(content.trim());
76
+ }
77
+ return false;
78
+ },
79
+ extract: (content) => {
80
+ const match = content.match(/^(\d+\.\d+\.\d+)/);
81
+ return match ? match[1] : null;
82
+ },
83
+ update: (content, newVersion) => {
84
+ return content.replace(/^\d+\.\d+\.\d+/, newVersion);
85
+ }
86
+ },
87
+ php: {
88
+ extensions: ['.php','pattern'],
89
+ detect: (content) => {
90
+ return /Version:\s*\d+\.\d+\.\d+/.test(content) ||
91
+ /define\s*\(\s*['"].*VERSION['"]/.test(content);
92
+ },
93
+ extract: (content) => {
94
+ // WordPress style header
95
+ let match = content.match(/Version:\s*(\d+\.\d+\.\d+)/);
96
+ if (match) return match[1];
97
+
98
+ // PHP define
99
+ match = content.match(/define\s*\(\s*['"].*VERSION['"],\s*['"](\d+\.\d+\.\d+)['"]/);
100
+ return match ? match[1] : null;
101
+ },
102
+ update: (content, newVersion) => {
103
+ // Update WordPress style header
104
+ content = content.replace(
105
+ /(Version:\s*)\d+\.\d+\.\d+/g,
106
+ `$1${newVersion}`
107
+ );
108
+
109
+ // Update PHP define
110
+ content = content.replace(
111
+ /(define\s*\(\s*['"].*VERSION['"],\s*['"])\d+\.\d+\.\d+(['"])/g,
112
+ `$1${newVersion}$2`
113
+ );
114
+
115
+ return content;
116
+ }
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Parse semantic version string
122
+ * @param {string} version - Version string (e.g., "1.2.3")
123
+ * @returns {Object|null} Parsed version object or null if invalid
124
+ */
125
+ export function parseVersion(version) {
126
+ if (!version) return null;
127
+
128
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
129
+ if (!match) return null;
130
+
131
+ return {
132
+ major: parseInt(match[1], 10),
133
+ minor: parseInt(match[2], 10),
134
+ patch: parseInt(match[3], 10),
135
+ prerelease: match[4] || null,
136
+ raw: version
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Compare two semantic versions
142
+ * @param {string} version1 - First version
143
+ * @param {string} version2 - Second version
144
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
145
+ */
146
+ export function compareVersions(version1, version2) {
147
+ const v1 = parseVersion(version1);
148
+ const v2 = parseVersion(version2);
149
+
150
+ if (!v1 || !v2) return 0;
151
+
152
+ if (v1.major !== v2.major) return v1.major - v2.major;
153
+ if (v1.minor !== v2.minor) return v1.minor - v2.minor;
154
+ if (v1.patch !== v2.patch) return v1.patch - v2.patch;
155
+
156
+ return 0;
157
+ }
158
+
159
+ /**
160
+ * Bump version according to type
161
+ * @param {string} currentVersion - Current version string
162
+ * @param {string} bumpType - Type of bump (major, minor, patch)
163
+ * @returns {string|null} New version string or null if invalid
164
+ */
165
+ export function bumpVersion(currentVersion, bumpType) {
166
+ const parsed = parseVersion(currentVersion);
167
+ if (!parsed) return null;
168
+
169
+ switch (bumpType.toLowerCase()) {
170
+ case 'major':
171
+ return `${parsed.major + 1}.0.0`;
172
+ case 'minor':
173
+ return `${parsed.major}.${parsed.minor + 1}.0`;
174
+ case 'patch':
175
+ return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
176
+ default:
177
+ // Check if it's a specific version
178
+ if (parseVersion(bumpType)) {
179
+ return bumpType;
180
+ }
181
+ return null;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Get WordPress plugin/theme configuration from environment variables
187
+ * @param {string} projectPath - Path to project directory
188
+ * @returns {Object} WordPress configuration
189
+ */
190
+ function getWordPressConfig(projectPath = process.cwd()) {
191
+ logger.debug(`Loading WordPress config from: ${projectPath}`);
192
+
193
+ // Check if .env.maiass exists in current directory
194
+ const envFile = path.join(projectPath, '.env.maiass');
195
+ logger.debug(`Looking for .env.maiass at: ${envFile}`);
196
+
197
+ if (fs.existsSync(envFile)) {
198
+ logger.debug(`.env.maiass file exists`);
199
+ try {
200
+ const envContent = fs.readFileSync(envFile, 'utf8');
201
+ logger.debug(`.env.maiass content:`);
202
+ logger.debug(envContent);
203
+ } catch (error) {
204
+ logger.error(`Could not read .env.maiass: ${error.message}`);
205
+ }
206
+ } else {
207
+ logger.debug(`.env.maiass file not found`);
208
+ }
209
+
210
+ const envVars = loadEnvironmentConfig();
211
+ logger.debug(`Environment variables loaded`);
212
+ // Load environment variables
213
+ // Show all loaded environment variables for debugging
214
+ logger.debug(`All loaded environment variables:`);
215
+ const relevantVars = ['MAIASS_PLUGIN_PATH', 'MAIASS_THEME_PATH', 'MAIASS_VERSION_CONSTANT', 'MAIASS_REPO_TYPE'];
216
+ relevantVars.forEach(varName => {
217
+ const value = envVars[varName];
218
+ logger.debug(` ${varName}: ${value || '(not set)'}`);
219
+ });
220
+
221
+ const config = {
222
+ pluginPath: envVars.MAIASS_PLUGIN_PATH || null,
223
+ themePath: envVars.MAIASS_THEME_PATH || null,
224
+ versionConstant: envVars.MAIASS_VERSION_CONSTANT || null
225
+ };
226
+ logger.debug(`WordPress config loaded:`, config);
227
+ logger.debug(`WordPress config:`);
228
+ logger.debug(` Plugin Path: ${config.pluginPath || '(not set)'}`);
229
+ logger.debug(` Theme Path: ${config.themePath || '(not set)'}`);
230
+ logger.debug(` Version Constant: ${config.versionConstant || '(not set)'}`);
231
+
232
+ return config;
233
+ }
234
+
235
+ /**
236
+ * Convert slug to uppercase with underscores for PHP constant
237
+ * @param {string} slug - Plugin/theme slug
238
+ * @returns {string} Formatted constant name
239
+ */
240
+ function slugToConstant(slug) {
241
+ return slug
242
+ .replace(/[^a-zA-Z0-9_]/g, '_') // Replace non-alphanumeric with underscores
243
+ .replace(/_+/g, '_') // Replace multiple underscores with single
244
+ .replace(/^_|_$/g, '') // Remove leading/trailing underscores
245
+ .toUpperCase();
246
+ }
247
+
248
+ /**
249
+ * Generate version constant name from plugin/theme path
250
+ * @param {string} pluginOrThemePath - Path to plugin or theme
251
+ * @returns {string} Generated constant name
252
+ */
253
+ function generateVersionConstant(pluginOrThemePath) {
254
+ let slug;
255
+
256
+ if (pluginOrThemePath.includes('wp-content/plugins/')) {
257
+ slug = pluginOrThemePath.split('wp-content/plugins/')[1].split('/')[0];
258
+ } else if (pluginOrThemePath.includes('wp-content/themes/')) {
259
+ slug = pluginOrThemePath.split('wp-content/themes/')[1].split('/')[0];
260
+ } else {
261
+ // Extract last directory name as slug
262
+ slug = path.basename(pluginOrThemePath);
263
+ }
264
+
265
+ return `${slugToConstant(slug)}_VERSION`;
266
+ }
267
+
268
+ /**
269
+ * Update WordPress theme style.css version header
270
+ * @param {string} filePath - Path to the style.css file
271
+ * @param {string} newVersion - New version value
272
+ * @returns {boolean} Success status
273
+ */
274
+ function updateThemeStyleVersion(filePath, newVersion) {
275
+ logger.debug(`Checking theme style.css: ${filePath}`);
276
+
277
+ if (!fs.existsSync(filePath)) {
278
+ logger.debug(`File not found: ${filePath}`);
279
+ return false;
280
+ }
281
+
282
+ logger.debug(`style.css exists: ${filePath}`);
283
+
284
+ try {
285
+ let content = fs.readFileSync(filePath, 'utf8');
286
+ logger.debug(` style.css content length: ${content.length} characters`);
287
+
288
+ // WordPress theme header pattern for Version (handles various formats)
289
+ // Matches: "Version:", "version:", "* Version:", "* version:", etc.
290
+ const versionPattern = /^(\s*\*?\s*[Vv]ersion:\s*)([0-9]+\.[0-9]+\.[0-9]+.*)$/gm;
291
+ logger.debug(` Search pattern: ${versionPattern.source}`);
292
+
293
+ // Test the pattern and show results
294
+ const matches = content.match(versionPattern);
295
+ if (matches) {
296
+ logger.debug(` Found ${matches.length} version header(s):`);
297
+ matches.forEach((match, index) => {
298
+ console.log(colors.Gray(` ${index + 1}: ${match.trim()}`));
299
+ });
300
+
301
+ // Replace with uniform format: always write "Version: x.x.x"
302
+ content = content.replace(versionPattern, `Version: ${newVersion}`);
303
+ console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Updated theme version in style.css (standardized format)`));
304
+ } else {
305
+ logger.debug(` No version header found in style.css`);
306
+
307
+ // Show the first part of the file to help debug
308
+ const lines = content.split('\n');
309
+ logger.debug(` First 15 lines of style.css:`);
310
+ lines.slice(0, 15).forEach((line, index) => {
311
+ console.log(colors.Gray(` ${index + 1}: ${line}`));
312
+ });
313
+
314
+ return false;
315
+ }
316
+
317
+ fs.writeFileSync(filePath, content, 'utf8');
318
+ logger.debug(` style.css written successfully`);
319
+ return true;
320
+ } catch (error) {
321
+ console.error(colors.Red(`${SYMBOLS.CROSS} Error updating ${filePath}: ${error.message}`));
322
+ return false;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Update PHP version constant in file
328
+ * @param {string} filePath - Path to the PHP file
329
+ * @param {string} constantName - Name of the constant to update
330
+ * @param {string} newVersion - New version value
331
+ * @returns {boolean} Success status
332
+ */
333
+ function updatePhpVersionConstant(filePath, constantName, newVersion) {
334
+ logger.debug(` Checking PHP file: ${filePath}`);
335
+
336
+ if (!fs.existsSync(filePath)) {
337
+ logger.debug(` File not found: ${filePath}`);
338
+ return false;
339
+ }
340
+
341
+ logger.debug(` File exists: ${filePath}`);
342
+
343
+ try {
344
+ let content = fs.readFileSync(filePath, 'utf8');
345
+ logger.debug(` File content length: ${content.length} characters`);
346
+
347
+ const definePattern = new RegExp(`^\\s*define\\s*\\(\\s*['"]${constantName}['"].*$`, 'gm');
348
+ logger.debug(` Search pattern: ${definePattern.source}`);
349
+ logger.debug(` Looking for constant: ${constantName}`);
350
+
351
+ // Test the pattern and show results
352
+ const matches = content.match(definePattern);
353
+ if (matches) {
354
+ logger.debug(` Found ${matches.length} match(es):`);
355
+ matches.forEach((match, index) => {
356
+ console.log(colors.Gray(` ${index + 1}: ${match.trim()}`));
357
+ });
358
+ } else {
359
+ logger.debug(` No matches found for pattern`);
360
+
361
+ // Show a sample of the file content to help debug
362
+ const lines = content.split('\n');
363
+ logger.debug(` First 10 lines of file:`);
364
+ lines.slice(0, 10).forEach((line, index) => {
365
+ console.log(colors.Gray(` ${index + 1}: ${line}`));
366
+ });
367
+
368
+ // Look for any define statements
369
+ const anyDefinePattern = /^\s*define\s*\(/gm;
370
+ const defineMatches = content.match(anyDefinePattern);
371
+ if (defineMatches) {
372
+ logger.debug(` Found ${defineMatches.length} define statement(s) in file`);
373
+ } else {
374
+ logger.debug(` No define statements found in file`);
375
+ }
376
+ }
377
+
378
+ const newDefine = `define('${constantName}', '${newVersion}');`;
379
+ logger.debug(` New define statement: ${newDefine}`);
380
+
381
+ if (definePattern.test(content)) {
382
+ // Replace existing define
383
+ content = content.replace(definePattern, newDefine);
384
+ console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Updated ${constantName} in ${path.basename(filePath)}`));
385
+ } else {
386
+ // Add new define after opening PHP tag
387
+ const phpOpenTag = /<\?php/;
388
+ logger.debug(` Looking for PHP opening tag...`);
389
+
390
+ if (phpOpenTag.test(content)) {
391
+ logger.debug(` Found PHP opening tag, adding new define`);
392
+ content = content.replace(phpOpenTag, `<?php\n\n${newDefine}`);
393
+ console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Added ${constantName} to ${path.basename(filePath)}`));
394
+ } else {
395
+ console.log(colors.BYellow(`${SYMBOLS.WARNING} Could not find PHP opening tag in ${path.basename(filePath)}`));
396
+ return false;
397
+ }
398
+ }
399
+
400
+ fs.writeFileSync(filePath, content, 'utf8');
401
+ logger.debug(` File written successfully`);
402
+ return true;
403
+ } catch (error) {
404
+ console.error(colors.Red(`${SYMBOLS.CROSS} Error updating ${filePath}: ${error.message}`));
405
+ return false;
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Update WordPress plugin/theme version files
411
+ * @param {string} newVersion - New version to set
412
+ * @param {string} projectPath - Path to project directory
413
+ * @returns {boolean} Success status
414
+ */
415
+ function updateWordPressVersions(newVersion, projectPath = process.cwd()) {
416
+ const wpConfig = getWordPressConfig(projectPath);
417
+ let success = true;
418
+
419
+ // Handle plugin path
420
+ if (wpConfig.pluginPath) {
421
+ const pluginPath = path.isAbsolute(wpConfig.pluginPath)
422
+ ? wpConfig.pluginPath
423
+ : path.join(projectPath, wpConfig.pluginPath);
424
+
425
+ // Determine main plugin file
426
+ let mainPluginFile;
427
+ if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isDirectory()) {
428
+ // Look for main plugin file (usually matches directory name)
429
+ const pluginName = path.basename(pluginPath);
430
+ const possibleFiles = [
431
+ path.join(pluginPath, `${pluginName}.php`),
432
+ path.join(pluginPath, 'plugin.php'),
433
+ path.join(pluginPath, 'index.php')
434
+ ];
435
+
436
+ mainPluginFile = possibleFiles.find(file => fs.existsSync(file));
437
+ } else if (pluginPath.endsWith('.php')) {
438
+ mainPluginFile = pluginPath;
439
+ }
440
+
441
+ if (mainPluginFile) {
442
+ const constantName = wpConfig.versionConstant || generateVersionConstant(wpConfig.pluginPath);
443
+ if (!updatePhpVersionConstant(mainPluginFile, constantName, newVersion)) {
444
+ success = false;
445
+ }
446
+ } else {
447
+ console.log(colors.BYellow(`${SYMBOLS.WARNING} Could not find main plugin file in ${pluginPath}`));
448
+ }
449
+ }
450
+
451
+ // Handle theme path
452
+ if (wpConfig.themePath) {
453
+ logger.debug(` Processing theme path: ${wpConfig.themePath}`);
454
+
455
+ const themePath = path.isAbsolute(wpConfig.themePath)
456
+ ? wpConfig.themePath
457
+ : path.join(projectPath, wpConfig.themePath);
458
+
459
+ logger.debug(` Resolved theme path: ${themePath}`);
460
+ logger.debug(` Checking if theme path exists...`);
461
+
462
+ // Look for functions.php in theme directory
463
+ let functionsFile;
464
+ if (fs.existsSync(themePath)) {
465
+ logger.debug(` Theme path exists`);
466
+
467
+ if (fs.statSync(themePath).isDirectory()) {
468
+ logger.debug(` Theme path is a directory, looking for functions.php`);
469
+ functionsFile = path.join(themePath, 'functions.php');
470
+ logger.debug(` Functions file path: ${functionsFile}`);
471
+ } else {
472
+ logger.debug(` Theme path is a file`);
473
+ }
474
+ } else {
475
+ logger.debug(` Theme path does not exist: ${themePath}`);
476
+ }
477
+
478
+ if (wpConfig.themePath.endsWith('functions.php')) {
479
+ logger.debug(` Theme path ends with functions.php, using directly`);
480
+ functionsFile = themePath;
481
+ }
482
+
483
+ logger.debug(` Final functions file path: ${functionsFile || '(not determined)'}`);
484
+
485
+ if (functionsFile && fs.existsSync(functionsFile)) {
486
+ logger.debug(` Functions file exists, proceeding with update`);
487
+
488
+ const constantName = wpConfig.versionConstant || generateVersionConstant(wpConfig.themePath);
489
+ logger.debug(` Using constant name: ${constantName}`);
490
+
491
+ if (!updatePhpVersionConstant(functionsFile, constantName, newVersion)) {
492
+ success = false;
493
+ }
494
+ } else {
495
+ console.log(colors.BYellow(`${SYMBOLS.WARNING} Could not find functions.php in ${themePath}`));
496
+ if (functionsFile) {
497
+ logger.debug(` Expected functions.php at: ${functionsFile}`);
498
+ }
499
+ }
500
+
501
+ // Also update style.css if it exists in the theme directory
502
+ const styleFile = path.join(themePath, 'style.css');
503
+ logger.debug(` Checking for style.css at: ${styleFile}`);
504
+ debuglog(` Checking for style.css at: ${styleFile}`);
505
+ if (fs.existsSync(styleFile)) {
506
+ logger.debug(` style.css found, updating theme version header`);
507
+ if (!updateThemeStyleVersion(styleFile, newVersion)) {
508
+ success = false;
509
+ }
510
+ } else {
511
+ logger.debug(` style.css not found at: ${styleFile}`);
512
+ }
513
+ }
514
+
515
+ return success;
516
+ }
517
+
518
+ /**
519
+ * Detect version files in the current directory
520
+ * @param {string} projectPath - Path to project directory
521
+ * @returns {Array} Array of detected version files
522
+ */
523
+ export function detectVersionFiles(projectPath = process.cwd()) {
524
+ const versionFiles = [];
525
+
526
+ // Common version file patterns to check
527
+ const filesToCheck = [
528
+ 'package.json',
529
+ 'composer.json',
530
+ 'VERSION',
531
+ 'version.txt',
532
+ 'style.css', // WordPress themes
533
+ 'plugin.php', // WordPress plugins
534
+ 'functions.php'
535
+ ];
536
+
537
+ for (const filename of filesToCheck) {
538
+ const filePath = path.join(projectPath, filename);
539
+
540
+ if (fs.existsSync(filePath)) {
541
+ try {
542
+ const content = fs.readFileSync(filePath, 'utf8');
543
+ const ext = path.extname(filename);
544
+
545
+ // Determine file type and check if it contains version info
546
+ for (const [typeName, typeConfig] of Object.entries(VERSION_FILE_TYPES)) {
547
+ if (typeConfig.extensions.includes(ext) || typeConfig.extensions.includes('')) {
548
+ if (typeConfig.detect(content, filename)) {
549
+ const version = typeConfig.extract(content);
550
+ if (version) {
551
+ versionFiles.push({
552
+ path: filePath,
553
+ filename,
554
+ type: typeName,
555
+ currentVersion: version,
556
+ content
557
+ });
558
+ break; // Found matching type, move to next file
559
+ }
560
+ }
561
+ }
562
+ }
563
+ } catch (error) {
564
+ // Skip files that can't be read
565
+ continue;
566
+ }
567
+ }
568
+ }
569
+
570
+ return versionFiles;
571
+ }
572
+
573
+ /**
574
+ * Get the latest version from git tags
575
+ * @returns {Promise<string|null>} Latest version tag or null
576
+ */
577
+ export async function getLatestVersionFromTags() {
578
+ try {
579
+ const result = await executeGitCommand('tag -l');
580
+ if (!result.success) return null;
581
+
582
+ const tags = result.output
583
+ .split('\n')
584
+ .filter(tag => tag.trim())
585
+ .filter(tag => /^\d+\.\d+\.\d+$/.test(tag))
586
+ .sort((a, b) => compareVersions(b, a)); // Sort descending
587
+
588
+ return tags.length > 0 ? tags[0] : null;
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Get current project version from files or git tags
596
+ * @param {string} projectPath - Path to project directory
597
+ * @returns {Promise<Object>} Version information
598
+ */
599
+ export async function getCurrentVersion(projectPath = process.cwd()) {
600
+ const versionFiles = detectVersionFiles(projectPath);
601
+ const tagVersion = await getLatestVersionFromTags();
602
+
603
+ let primaryVersion = null;
604
+ let primarySource = null;
605
+
606
+ // Prioritize package.json if it exists
607
+ const packageJson = versionFiles.find(f => f.filename === 'package.json');
608
+ if (packageJson) {
609
+ primaryVersion = packageJson.currentVersion;
610
+ primarySource = 'package.json';
611
+ } else if (versionFiles.length > 0) {
612
+ // Use first detected version file
613
+ primaryVersion = versionFiles[0].currentVersion;
614
+ primarySource = versionFiles[0].filename;
615
+ } else if (tagVersion) {
616
+ primaryVersion = tagVersion;
617
+ primarySource = 'git tags';
618
+ }
619
+
620
+ return {
621
+ current: primaryVersion,
622
+ source: primarySource,
623
+ files: versionFiles,
624
+ tagVersion,
625
+ hasVersionFiles: versionFiles.length > 0
626
+ };
627
+ }
628
+
629
+ /**
630
+ * Update secondary version files based on MAIASS_VERSION_SECONDARY_FILES config
631
+ * Format: "file:type:pattern|file:type:pattern"
632
+ * Types: txt, json, php, pattern
633
+ * Pattern type uses {version} placeholder
634
+ * @param {string} newVersion - New version to set
635
+ * @param {string} config - Pipe-separated config string
636
+ * @param {boolean} dryRun - If true, don't actually write files
637
+ * @returns {Promise<Object>} Update results
638
+ */
639
+ async function updateSecondaryVersionFiles(newVersion, config, dryRun = false) {
640
+ const results = {
641
+ success: true,
642
+ updated: [],
643
+ failed: []
644
+ };
645
+
646
+ if (!config) return results;
647
+
648
+ // Parse pipe-separated config
649
+ const fileConfigs = config.split('|').filter(c => c.trim());
650
+
651
+ for (const fileConfig of fileConfigs) {
652
+ try {
653
+ // Split on colons: filename:type:pattern
654
+ const parts = fileConfig.split(':');
655
+ if (parts.length < 2) {
656
+ results.failed.push({
657
+ file: fileConfig,
658
+ error: 'Invalid config format (expected file:type:pattern)'
659
+ });
660
+ continue;
661
+ }
662
+
663
+ const filename = parts[0].trim();
664
+ const type = parts[1].trim() || 'txt';
665
+ let pattern = parts.slice(2).join(':').trim(); // Everything after second colon
666
+
667
+ // Remove escape backslashes from pattern (e.g., \" becomes ")
668
+ pattern = pattern.replace(/\\"/g, '"').replace(/\\'/g, "'");
669
+
670
+ if (!fs.existsSync(filename)) {
671
+ logger.warning(SYMBOLS.WARNING, `Skipping ${filename} (not found)`);
672
+ continue;
673
+ }
674
+
675
+ // Read file content
676
+ let content = fs.readFileSync(filename, 'utf8');
677
+ let updated = false;
678
+
679
+ if (type === 'pattern') {
680
+ // Pattern type: replace {version} placeholder in the pattern
681
+ // First escape all regex special chars in the pattern
682
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
683
+ // Then replace the escaped {version} with version regex
684
+ const searchPattern = escapedPattern.replace('\\{version\\}', '[0-9]+\\.[0-9]+\\.[0-9]+');
685
+ const replacePattern = pattern.replace('{version}', newVersion);
686
+ const regex = new RegExp(searchPattern, 'g');
687
+
688
+ if (content.match(regex)) {
689
+ content = content.replace(regex, replacePattern);
690
+ updated = true;
691
+ }
692
+ } else if (type === 'txt') {
693
+ // Text type: find line starting with pattern and replace version
694
+ const lines = content.split('\n');
695
+ for (let i = 0; i < lines.length; i++) {
696
+ if (lines[i].startsWith(pattern)) {
697
+ // Replace version number in this line
698
+ lines[i] = lines[i].replace(/\d+\.\d+\.\d+/, newVersion);
699
+ updated = true;
700
+ }
701
+ }
702
+ content = lines.join('\n');
703
+ } else if (type === 'json') {
704
+ // JSON type: update specific key
705
+ try {
706
+ const json = JSON.parse(content);
707
+ const keys = pattern.split('.');
708
+ let obj = json;
709
+ for (let i = 0; i < keys.length - 1; i++) {
710
+ obj = obj[keys[i]];
711
+ }
712
+ obj[keys[keys.length - 1]] = newVersion;
713
+ content = JSON.stringify(json, null, 2) + '\n';
714
+ updated = true;
715
+ } catch (e) {
716
+ results.failed.push({
717
+ file: filename,
718
+ error: `JSON parse error: ${e.message}`
719
+ });
720
+ continue;
721
+ }
722
+ }
723
+
724
+ if (updated && !dryRun) {
725
+ fs.writeFileSync(filename, content, 'utf8');
726
+ }
727
+
728
+ if (updated) {
729
+ logger.success(SYMBOLS.CHECKMARK, `Updated version to ${newVersion} in ${filename}`);
730
+ results.updated.push({
731
+ file: filename,
732
+ type,
733
+ pattern,
734
+ newVersion
735
+ });
736
+ }
737
+
738
+ } catch (error) {
739
+ results.failed.push({
740
+ file: fileConfig,
741
+ error: error.message
742
+ });
743
+ results.success = false;
744
+ }
745
+ }
746
+
747
+ return results;
748
+ }
749
+
750
+ /**
751
+ * Update version in all detected files
752
+ * @param {string} newVersion - New version to set
753
+ * @param {Array} versionFiles - Array of version files to update
754
+ * @param {boolean} dryRun - If true, don't actually write files
755
+ * @returns {Promise<Object>} Update results
756
+ */
757
+ export async function updateVersionFiles(newVersion, versionFiles, dryRun = false) {
758
+ const results = {
759
+ success: true,
760
+ updated: [],
761
+ failed: [],
762
+ dryRun
763
+ };
764
+
765
+ // Update primary version files
766
+ for (const file of versionFiles) {
767
+ try {
768
+ const typeConfig = VERSION_FILE_TYPES[file.type];
769
+ const updatedContent = typeConfig.update(file.content, newVersion);
770
+
771
+ if (!updatedContent) {
772
+ results.failed.push({
773
+ file: file.filename,
774
+ error: 'Failed to update content'
775
+ });
776
+ continue;
777
+ }
778
+
779
+ if (!dryRun) {
780
+ fs.writeFileSync(file.path, updatedContent, 'utf8');
781
+ }
782
+
783
+ results.updated.push({
784
+ file: file.filename,
785
+ path: file.path,
786
+ oldVersion: file.currentVersion,
787
+ newVersion
788
+ });
789
+
790
+ } catch (error) {
791
+ results.failed.push({
792
+ file: file.filename,
793
+ error: error.message
794
+ });
795
+ results.success = false;
796
+ }
797
+ }
798
+
799
+ // Update secondary version files if configured
800
+ const secondaryFiles = process.env.MAIASS_VERSION_SECONDARY_FILES;
801
+ if (secondaryFiles) {
802
+ logger.info(SYMBOLS.INFO, 'Updating secondary version files...');
803
+ const secondaryResults = await updateSecondaryVersionFiles(newVersion, secondaryFiles, dryRun);
804
+ results.updated.push(...secondaryResults.updated);
805
+ results.failed.push(...secondaryResults.failed);
806
+ if (!secondaryResults.success) {
807
+ results.success = false;
808
+ }
809
+ }
810
+
811
+ // Update WordPress plugin/theme versions if configured
812
+ if (!dryRun) {
813
+ // change to use logger
814
+ logger.info(SYMBOLS.INFO, 'Checking for WordPress plugin/theme version updates...');
815
+ const wpSuccess = updateWordPressVersions(newVersion);
816
+ if (!wpSuccess) {
817
+ results.success = false;
818
+ results.failed.push({
819
+ file: 'WordPress files',
820
+ error: 'Failed to update some WordPress plugin/theme version constants'
821
+ });
822
+ }
823
+ } else {
824
+ // For dry run, just check if WordPress config exists
825
+ const wpConfig = getWordPressConfig();
826
+ if (wpConfig.pluginPath || wpConfig.themePath) {
827
+ // change to use logger
828
+ logger.info(SYMBOLS.INFO, 'Would update WordPress plugin/theme versions (dry run)');
829
+ if (wpConfig.pluginPath) {
830
+ const constantName = wpConfig.versionConstant || generateVersionConstant(wpConfig.pluginPath);
831
+ logger.info(SYMBOLS.INFO, ` Plugin: ${wpConfig.pluginPath} (${constantName})`);
832
+ }
833
+ if (wpConfig.themePath) {
834
+ const constantName = wpConfig.versionConstant || generateVersionConstant(wpConfig.themePath);
835
+ logger.info(SYMBOLS.INFO, ` Theme: ${wpConfig.themePath} (${constantName})`);
836
+ }
837
+ }
838
+ }
839
+
840
+ return results;
841
+ }
842
+
843
+ /**
844
+ * Create git tag for version
845
+ * @param {string} version - Version to tag
846
+ * @param {string} message - Tag message
847
+ * @param {boolean} dryRun - If true, don't actually create tag
848
+ * @returns {Promise<Object>} Tag creation result
849
+ */
850
+ export async function createVersionTag(version, message = null, dryRun = false) {
851
+ const tagMessage = message || `Release version ${version}`;
852
+
853
+ if (dryRun) {
854
+ return {
855
+ success: true,
856
+ dryRun: true,
857
+ tag: version,
858
+ message: tagMessage
859
+ };
860
+ }
861
+
862
+ try {
863
+ // Check if tag already exists
864
+ const existingTag = await executeGitCommand(`tag -l ${version}`);
865
+ if (existingTag.success && existingTag.output.trim()) {
866
+ return {
867
+ success: false,
868
+ error: `Tag ${version} already exists`
869
+ };
870
+ }
871
+
872
+ // Create annotated tag
873
+ const result = await executeGitCommand(`tag -a ${version} -m "${tagMessage}"`);
874
+
875
+ return {
876
+ success: result.success,
877
+ tag: version,
878
+ message: tagMessage,
879
+ error: result.success ? null : result.error
880
+ };
881
+ } catch (error) {
882
+ return {
883
+ success: false,
884
+ error: error.message
885
+ };
886
+ }
887
+ }
888
+
889
+ /**
890
+ * Validate version string
891
+ * @param {string} version - Version string to validate
892
+ * @returns {Object} Validation result
893
+ */
894
+ export function validateVersion(version) {
895
+ const parsed = parseVersion(version);
896
+
897
+ return {
898
+ valid: parsed !== null,
899
+ parsed,
900
+ error: parsed ? null : 'Invalid semantic version format (expected: MAJOR.MINOR.PATCH)'
901
+ };
902
+ }