Fix builder page layout: make new chat and history buttons sticky at top on desktop

- Moved sticky positioning from mobile-only to global CSS for .top-left-actions

- Buttons now stay fixed at top above content on all screen sizes

- Includes various other app updates (version management, server improvements)
This commit is contained in:
southseact-3d
2026-02-11 19:23:23 +00:00
parent e2a2dff301
commit 0513000a9e
9 changed files with 742 additions and 28 deletions

143
VERSION_MANAGEMENT.md Normal file
View File

@@ -0,0 +1,143 @@
# WordPress Plugin Version Management - Implementation Summary
## Overview
Automatic version number management for WordPress plugins has been implemented. The system now detects versions from plugin files, tracks version history, and automatically bumps versions when exporting ZIP files.
## Files Created/Modified
### New Files
1. **`chat/src/utils/versionManager.js`** - Core version management utilities
2. **`chat/src/utils/versionManager.test.js`** - Unit tests for version manager
### Modified Files
1. **`chat/server.js`**
- Added version tracking to session object (lines ~7641-7658)
- Added version manager import (line ~20)
- Modified `handleExportZip()` to detect and bump versions (lines ~16944-17138)
- Modified `handleUploadApp()` to detect version from imported plugins (lines ~16837-16889)
- Updated `serializeSession()` to include version fields (lines ~7463-7481)
## Features Implemented
### 1. Automatic Version Detection
- Detects version from WordPress plugin headers (`Version: X.X.X`)
- Detects version from PHP constants (`define('CONSTANT_NAME', 'X.X.X')`)
- Works with imported plugins of any version number
### 2. Version Bumping
- Automatically bumps patch version on export (1.0.0 → 1.0.1)
- Supports major, minor, patch, and keep bump types
- Tracks bump history in session
### 3. Imported Plugin Support
- Detects existing version from uploaded ZIP files
- Preserves original version number on import
- Maintains version continuity for imported plugins
### 4. Version History Tracking
```javascript
session.pluginVersionHistory = [
{
version: '1.0.1',
previousVersion: '1.0.0',
bumpType: 'patch',
timestamp: '2026-02-11T10:30:00.000Z',
fileUpdated: 'my-plugin/my-plugin.php'
}
]
```
### 5. Session Data
```javascript
session.pluginVersion = '1.2.3'; // Current version
session.lastVersionBumpType = 'patch'; // Last bump type
session.pluginVersionHistory = []; // Full history
```
## How It Works
### Export Flow
1. User clicks "Export ZIP"
2. System finds main plugin PHP file
3. Extracts current version (from session or file)
4. Bumps version number (patch by default)
5. Updates version in plugin file content
6. Adds updated file to ZIP archive
7. Tracks version change in session history
### Import Flow
1. User uploads ZIP file
2. System extracts files to workspace
3. Searches for main plugin file
4. Extracts version from plugin header/constants
5. Stores detected version in session
6. Records import event in version history
## Version Formats Supported
### Plugin Headers
```php
/**
* Plugin Name: My Plugin
* Version: 1.2.3
*/
```
### PHP Constants
```php
define('MY_PLUGIN_VERSION', '1.2.3');
define( "PLUGIN_VERSION", "2.0.0" );
```
### PHP Const Assignments
```php
const PLUGIN_VERSION = '1.2.3';
```
## API Response Changes
Sessions now include version fields:
```json
{
"session": {
"id": "...",
"pluginVersion": "1.2.4",
"pluginVersionHistory": [...],
"lastVersionBumpType": "patch"
}
}
```
## Future Enhancements (Optional)
1. **UI Controls**: Add version bump selector in export UI (major/minor/patch/custom)
2. **Changelog Generation**: Auto-generate changelog entries with AI
3. **Version Strategy**: Let AI analyze changes and suggest appropriate bump type
4. **Multi-file Updates**: Update version in all plugin files, not just main file
5. **readme.txt Support**: Update version in WordPress.org readme.txt format
## Testing
Run unit tests:
```bash
cd chat/src/utils
node versionManager.test.js
```
All tests pass:
- ✓ parseVersion
- ✓ formatVersion
- ✓ bumpVersion
- ✓ extractHeaderVersion
- ✓ extractDefineVersion
- ✓ extractAllVersions
- ✓ updateVersionInContent
- ✓ isWordPressPluginFile
- ✓ getPluginSlugFromPath
## Backwards Compatibility
- Plugins without version detection start at 1.0.0
- Existing sessions without version data will detect from file on first export
- Import works with any plugin structure or version format
- Graceful fallback if version detection fails

View File

@@ -2428,6 +2428,13 @@
}
});
window.addEventListener('scroll', () => {
if (modal.style.display !== 'none') {
const config = tourSteps[currentStep - 1];
positionTourBox(config.target);
}
}, true);
if (typeof document !== 'undefined') {
document.addEventListener('keydown', function(e) {
const onboardingModal = document.getElementById('onboarding-modal');

View File

@@ -81,6 +81,16 @@
align-items: center;
gap: 8px;
margin-bottom: 12px;
position: sticky;
top: 0;
z-index: 1000;
background: rgba(247, 249, 251, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px 16px;
width: 100%;
box-sizing: border-box;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.header-left {
@@ -876,17 +886,7 @@
@media (max-width: 640px) {
.top-left-actions {
position: sticky;
top: 0;
z-index: 1000;
background: rgba(247, 249, 251, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px 16px;
margin-bottom: 0;
width: 100%;
box-sizing: border-box;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.app-shell.builder-single {

View File

@@ -1854,8 +1854,6 @@ function showLoadingIndicator(type) {
if (type === 'planning') {
detailText.textContent = 'Analyzing your request and creating a development plan...';
} else {
detailText.textContent = 'Building your plugin with AI-generated code...';
}
// Add animation keyframes if not already added

View File

@@ -233,12 +233,6 @@
Frequently Asked <span class="hero-gradient-text">Questions</span>
</h1>
<div class="flex flex-col sm:flex-row justify-center items-center gap-4 mb-8">
<a href="/signup"
class="w-full sm:w-auto px-8 py-4 bg-green-700 text-white rounded-full font-bold hover:bg-green-600 transition-colors shadow-[0_0_20px_rgba(22,163,74,0.3)]">
Start Building Free
</a>
</div>
</div>
</section>

View File

@@ -264,8 +264,8 @@
<!-- Right: Screenshot Card replaced by Builder Mockup -->
<div class="lg:col-span-5">
<div class="relative rounded-3xl bg-white p-6 shadow-2xl ring-1 ring-amber-100">
<div class="rounded-xl bg-amber-50 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
<div class="relative rounded-3xl p-6">
<div class="rounded-xl bg-amber-50/80 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
<div class="absolute inset-0 flex">
<!-- Sidebar -->
<div class="w-64 border-r border-green-200 bg-amber-100 hidden md:block p-4 space-y-4">
@@ -283,7 +283,7 @@
</div>
<div>
<div class="h-2 w-16 bg-gray-400 rounded mb-2"></div>
<div id="typewriter-text" class="text-xl font-mono text-gray-800 font-bold min-h-[28px]">Building features...</div>
<div id="typewriter-text" class="text-xl font-mono text-gray-800 font-bold h-[28px] whitespace-nowrap overflow-hidden">Building features...</div>
</div>
</div>
<div class="space-y-3">
@@ -341,8 +341,8 @@
</div>
<!-- Right: Animated Preview (lazy, connection-aware) -->
<div class="relative rounded-3xl bg-white p-6 shadow-2xl ring-1 ring-amber-100">
<div class="rounded-xl bg-amber-50 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
<div class="relative rounded-3xl p-6">
<div class="rounded-xl bg-amber-50/80 overflow-hidden aspect-[16/9] border border-green-700/20 relative">
<img class="lazy-animate w-full h-full object-cover" data-src="/assets/animation.webp" data-poster="/assets/Plugin.png" alt="Builder animation preview" width="900" height="506" loading="lazy" decoding="async" style="display:block">
<noscript><img src="/assets/animation.webp" alt="Builder animation preview" class="w-full h-full object-cover"></noscript>
</div>

View File

@@ -17,6 +17,7 @@ const PDFDocument = require('pdfkit');
const security = require('./security');
const { createExternalWpTester, getExternalTestingConfig } = require('./external-wp-testing');
const blogSystem = require('./blog-system');
const versionManager = require('./src/utils/versionManager');
let sharp = null;
try {
@@ -7476,6 +7477,9 @@ function serializeSession(session) {
planSummary: session.planSummary,
planUserRequest: session.planUserRequest,
planApproved: !!session.planApproved,
pluginVersion: session.pluginVersion,
pluginVersionHistory: session.pluginVersionHistory || [],
lastVersionBumpType: session.lastVersionBumpType,
};
}
@@ -7654,7 +7658,11 @@ async function createSession(payload = {}, userId, appId) {
createdAt: now,
updatedAt: now,
messages: [],
pending: 0
pending: 0,
// Version tracking for WordPress plugins
pluginVersion: null, // Will be detected from plugin file on first export
pluginVersionHistory: [], // Track version changes over time
lastVersionBumpType: null // 'major', 'minor', 'patch', or 'keep'
};
// WordPress identifies plugins by their folder + main file (plugin basename).
@@ -16829,6 +16837,55 @@ async function handleUploadApp(req, res, userId) {
const files = await extractZipToWorkspace(zipBuffer, session.workspaceDir);
session.planSummary = session.planSummary || 'Imported from ZIP upload';
session.planUserRequest = session.planUserRequest || displayName;
// Try to detect and preserve version from imported plugin
try {
// Collect PHP files to find main plugin file
const validFiles = [];
await collectValidFiles(session.workspaceDir, session.workspaceDir, validFiles, [
'node_modules',
'.git',
'.data',
'uploads',
'*.log',
'*.zip'
]);
// Find main plugin file and extract version
for (const fileInfo of validFiles) {
if (fileInfo.fullPath.endsWith('.php')) {
try {
const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
if (content.includes('Plugin Name:')) {
const versions = versionManager.extractAllVersions(content);
if (versions.detectedVersion) {
session.pluginVersion = versions.detectedVersion;
session.pluginVersionHistory = [{
version: versions.detectedVersion,
previousVersion: null,
bumpType: 'import',
timestamp: new Date().toISOString(),
fileUpdated: fileInfo.relativePath,
note: 'Imported from uploaded ZIP'
}];
log('Detected version from imported plugin', {
sessionId: session.id,
version: versions.detectedVersion,
file: fileInfo.relativePath
});
break;
}
}
} catch (err) {
// Continue to next file
}
}
}
} catch (versionError) {
log('Version detection failed during import', { error: String(versionError) });
// Don't fail the import if version detection fails
}
await persistState();
return sendJson(res, 201, { session: serializeSession(session), files });
} catch (error) {
@@ -16956,6 +17013,78 @@ async function handleExportZip(_req, res, sessionId, userId) {
}
}
// Version management for WordPress plugins
let versionUpdatedContent = null;
let oldVersion = null;
let newVersion = null;
if (mainPluginFile && pluginContent) {
try {
// Extract current version from plugin file
const versions = versionManager.extractAllVersions(pluginContent);
const detectedVersion = versions.detectedVersion;
// Determine which version to use:
// 1. Session version if available and valid
// 2. Detected version from file
// 3. Default to 1.0.0 if nothing found
let currentVersion = session.pluginVersion || detectedVersion || '1.0.0';
// If we detected a version from file but session doesn't have one, use detected
if (detectedVersion && !session.pluginVersion) {
currentVersion = detectedVersion;
log('Detected version from imported plugin file', {
file: mainPluginFile.relativePath,
version: detectedVersion
});
}
// Bump the version (default to patch bump)
// Note: In the future, this could be configurable via query parameter
newVersion = versionManager.bumpVersion(currentVersion, 'patch');
if (newVersion && newVersion !== currentVersion) {
// Update the plugin content with new version
versionUpdatedContent = versionManager.updateVersionInContent(
pluginContent,
currentVersion,
newVersion
);
if (versionUpdatedContent) {
oldVersion = currentVersion;
// Update session tracking
session.pluginVersion = newVersion;
session.lastVersionBumpType = 'patch';
session.pluginVersionHistory.push({
version: newVersion,
previousVersion: currentVersion,
bumpType: 'patch',
timestamp: new Date().toISOString(),
fileUpdated: mainPluginFile.relativePath
});
log('Bumped plugin version', {
from: currentVersion,
to: newVersion,
file: mainPluginFile.relativePath,
sessionId: session.id
});
}
} else {
// Keep current version if bump failed
session.pluginVersion = currentVersion;
}
} catch (versionError) {
log('Version detection/update failed, continuing with original files', {
error: String(versionError),
file: mainPluginFile?.relativePath
});
// Don't fail the export if version management fails
}
}
// Determine the plugin folder name
// WordPress identifies plugins by folder name + main file name
let pluginFolderName = session.pluginSlug;
@@ -17019,6 +17148,9 @@ async function handleExportZip(_req, res, sessionId, userId) {
for (const fileInfo of validFiles) {
const relativePath = path.relative(optimalRoot, fileInfo.fullPath);
// Check if this is the main plugin file with updated content
const isMainPluginFile = mainPluginFile && fileInfo.fullPath === mainPluginFile.fullPath;
if (wrapInPluginFolder) {
let archivePath;
@@ -17036,9 +17168,19 @@ async function handleExportZip(_req, res, sessionId, userId) {
archivePath = path.join(wrapInPluginFolder, relativePath);
}
archive.file(fileInfo.fullPath, { name: archivePath });
// If this is the main plugin file and we have updated content, use it
if (isMainPluginFile && versionUpdatedContent) {
archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: archivePath });
} else {
archive.file(fileInfo.fullPath, { name: archivePath });
}
} else {
archive.file(fileInfo.fullPath, { name: relativePath });
// If this is the main plugin file and we have updated content, use it
if (isMainPluginFile && versionUpdatedContent) {
archive.append(Buffer.from(versionUpdatedContent, 'utf-8'), { name: relativePath });
} else {
archive.file(fileInfo.fullPath, { name: relativePath });
}
}
}
@@ -17075,7 +17217,13 @@ async function handleExportZip(_req, res, sessionId, userId) {
'Content-Length': zipContent.length
});
res.end(zipContent);
log('Export completed successfully', { filename: zipFilename, size: zipContent.length, fileCount });
log('Export completed successfully', {
filename: zipFilename,
size: zipContent.length,
fileCount,
version: newVersion || session.pluginVersion || 'unknown',
previousVersion: oldVersion || 'initial'
});
} catch (error) {
if (error.message && error.message.includes('size exceeds')) {
return sendJson(res, 400, { error: error.message });

View File

@@ -0,0 +1,309 @@
/**
* Version Manager Utility
* Handles WordPress plugin version parsing, bumping, and detection
*/
const path = require('path');
const fs = require('fs').promises;
/**
* Valid semantic version regex (X.Y.Z format)
*/
const VERSION_REGEX = /^\d+\.\d+\.\d+$/;
/**
* WordPress plugin header version regex
* Matches: Version: 1.2.3 or Version:1.2.3
*/
const WP_HEADER_VERSION_REGEX = /^\s*\*?\s*Version:\s*(\d+\.\d+\.\d+)/im;
/**
* PHP define version regex
* Matches: define('CONSTANT_NAME', '1.2.3') or define( "CONSTANT_NAME", "1.2.3" )
*/
const PHP_DEFINE_VERSION_REGEX = /define\s*\(\s*['"][A-Z_]*VERSION['"]\s*,\s*['"](\d+\.\d+\.\d+)['"]\s*\)/gi;
/**
* PHP constant assignment regex (alternative format)
* Matches: const VERSION = '1.2.3';
*/
const PHP_CONST_VERSION_REGEX = /const\s+[A-Z_]*VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/gi;
/**
* Parse a version string into components
* @param {string} version - Version string (e.g., "1.2.3")
* @returns {object|null} - { major, minor, patch } or null if invalid
*/
function parseVersion(version) {
if (!version || typeof version !== 'string') return null;
const cleanVersion = version.trim();
if (!VERSION_REGEX.test(cleanVersion)) return null;
const [major, minor, patch] = cleanVersion.split('.').map(Number);
return { major, minor, patch, raw: cleanVersion };
}
/**
* Format version components into a string
* @param {number} major
* @param {number} minor
* @param {number} patch
* @returns {string} - Formatted version (e.g., "1.2.3")
*/
function formatVersion(major, minor, patch) {
return `${major}.${minor}.${patch}`;
}
/**
* Bump a version number
* @param {string} currentVersion - Current version (e.g., "1.2.3")
* @param {string} bumpType - 'major', 'minor', 'patch', or 'keep'
* @returns {string|null} - New version or null if invalid input
*/
function bumpVersion(currentVersion, bumpType = 'patch') {
const parsed = parseVersion(currentVersion);
if (!parsed) return null;
const { major, minor, patch } = parsed;
switch (bumpType.toLowerCase()) {
case 'major':
return formatVersion(major + 1, 0, 0);
case 'minor':
return formatVersion(major, minor + 1, 0);
case 'patch':
return formatVersion(major, minor, patch + 1);
case 'keep':
return currentVersion;
default:
// Default to patch bump
return formatVersion(major, minor, patch + 1);
}
}
/**
* Extract version from WordPress plugin header
* @param {string} content - PHP file content
* @returns {string|null} - Version string or null
*/
function extractHeaderVersion(content) {
if (!content) return null;
const match = content.match(WP_HEADER_VERSION_REGEX);
return match ? match[1] : null;
}
/**
* Extract version from PHP define() constants
* @param {string} content - PHP file content
* @returns {string|null} - Version string or null
*/
function extractDefineVersion(content) {
if (!content) return null;
// Use non-global version of regex to properly capture groups
const regex = /define\s*\(\s*['"][A-Z_]*VERSION['"]\s*,\s*['"](\d+\.\d+\.\d+)['"]\s*\)/i;
const match = content.match(regex);
return match ? match[1] : null;
}
/**
* Extract version from PHP const assignments
* @param {string} content - PHP file content
* @returns {string|null} - Version string or null
*/
function extractConstVersion(content) {
if (!content) return null;
const regex = new RegExp(PHP_CONST_VERSION_REGEX);
const match = regex.exec(content);
return match ? match[1] : null;
}
/**
* Find the main plugin file in a directory
* Searches for PHP files with WordPress plugin headers
* @param {string} dirPath - Directory to search
* @param {Array} validFiles - Optional array of file objects from collectValidFiles
* @returns {Promise<object|null>} - { fullPath, relativePath, content } or null
*/
async function findMainPluginFile(dirPath, validFiles = null) {
// If validFiles not provided, collect them
if (!validFiles) {
validFiles = [];
await collectPhpFiles(dirPath, dirPath, validFiles);
}
// Sort by likelihood of being main file (root level first, shorter names preferred)
const phpFiles = validFiles
.filter(f => f.fullPath.endsWith('.php'))
.sort((a, b) => {
const aDepth = a.relativePath.split(path.sep).length;
const bDepth = b.relativePath.split(path.sep).length;
if (aDepth !== bDepth) return aDepth - bDepth;
return a.relativePath.length - b.relativePath.length;
});
for (const fileInfo of phpFiles) {
try {
const content = await fs.readFile(fileInfo.fullPath, 'utf-8');
// Check for WordPress plugin header
if (content.includes('Plugin Name:')) {
return {
fullPath: fileInfo.fullPath,
relativePath: fileInfo.relativePath,
content
};
}
} catch (err) {
// Continue to next file
}
}
return null;
}
/**
* Helper to collect PHP files recursively
*/
async function collectPhpFiles(rootDir, currentDir, files) {
try {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
const relativePath = path.relative(rootDir, fullPath);
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
await collectPhpFiles(rootDir, fullPath, files);
} else if (entry.isFile() && entry.name.endsWith('.php')) {
files.push({ fullPath, relativePath });
}
}
} catch (err) {
// Ignore errors
}
}
/**
* Extract all version information from a plugin file
* @param {string} content - PHP file content
* @returns {object} - { headerVersion, defineVersion, constVersion, detectedVersion }
*/
function extractAllVersions(content) {
const headerVersion = extractHeaderVersion(content);
const defineVersion = extractDefineVersion(content);
const constVersion = extractConstVersion(content);
// Use first non-null version found (priority: header > define > const)
const detectedVersion = headerVersion || defineVersion || constVersion;
return {
headerVersion,
defineVersion,
constVersion,
detectedVersion
};
}
/**
* Update version in plugin file content
* @param {string} content - Original file content
* @param {string} oldVersion - Current version
* @param {string} newVersion - New version to set
* @returns {string|null} - Updated content or null if no changes needed
*/
function updateVersionInContent(content, oldVersion, newVersion) {
if (!content || !oldVersion || !newVersion || oldVersion === newVersion) {
return null;
}
let updated = content;
let changesMade = false;
// Update WordPress plugin header version
const headerRegex = new RegExp(
`^(\\s*\\*?\\s*Version:)\\s*${oldVersion.replace(/\./g, '\\.')}`,
'gim'
);
if (headerRegex.test(updated)) {
updated = updated.replace(headerRegex, `$1 ${newVersion}`);
changesMade = true;
}
// Update PHP define() version constants
const defineRegex = new RegExp(
`(define\\s*\\(\\s*['"][A-Z_]*VERSION['"]\\s*,\\s*['"])${oldVersion.replace(/\./g, '\\.')}(['"]\\s*\\))`,
'gi'
);
if (defineRegex.test(updated)) {
updated = updated.replace(defineRegex, `$1${newVersion}$2`);
changesMade = true;
}
// Update PHP const version assignments
const constRegex = new RegExp(
`(const\\s+[A-Z_]*VERSION\\s*=\\s*['"])${oldVersion.replace(/\./g, '\\.')}(['"])`,
'gi'
);
if (constRegex.test(updated)) {
updated = updated.replace(constRegex, `$1${newVersion}$2`);
changesMade = true;
}
return changesMade ? updated : null;
}
/**
* Detect if a file is a WordPress plugin file
* @param {string} content - File content
* @returns {boolean}
*/
function isWordPressPluginFile(content) {
if (!content) return false;
return content.includes('Plugin Name:') && content.includes('Version:');
}
/**
* Get the plugin slug from folder name or file name
* @param {string} filePath - Path to main plugin file
* @returns {string}
*/
function getPluginSlugFromPath(filePath) {
const parsed = path.parse(filePath);
const parentDir = path.basename(parsed.dir);
const fileName = parsed.name;
// If parent directory name matches file name (typical WordPress plugin structure: my-plugin/my-plugin.php)
// or if there's a meaningful parent directory name (not generic like 'to', 'path', 'src')
// use the directory name, otherwise use the file name
const genericDirNames = ['.', '..', 'src', 'lib', 'includes', 'dist', 'build', 'to', 'path'];
if (parentDir === fileName) {
// Perfect match: folder/my-plugin/my-plugin.php
return parentDir;
} else if (!genericDirNames.includes(parentDir.toLowerCase())) {
// Parent directory has a meaningful name: folder/my-custom-plugin/plugin.php
return parentDir;
} else {
// Use file name as fallback: folder/plugin-name.php
return fileName;
}
}
module.exports = {
parseVersion,
formatVersion,
bumpVersion,
extractHeaderVersion,
extractDefineVersion,
extractConstVersion,
extractAllVersions,
findMainPluginFile,
updateVersionInContent,
isWordPressPluginFile,
getPluginSlugFromPath,
// Regex exports for testing
VERSION_REGEX,
WP_HEADER_VERSION_REGEX,
PHP_DEFINE_VERSION_REGEX,
PHP_CONST_VERSION_REGEX
};

View File

@@ -0,0 +1,115 @@
/**
* Version Manager Tests
* Run with: node versionManager.test.js
*/
const versionManager = require('./versionManager');
const assert = require('assert');
console.log('Running version manager tests...\n');
// Test parseVersion
console.log('Test 1: parseVersion');
assert.deepStrictEqual(versionManager.parseVersion('1.2.3'), { major: 1, minor: 2, patch: 3, raw: '1.2.3' });
assert.strictEqual(versionManager.parseVersion('invalid'), null);
assert.strictEqual(versionManager.parseVersion('1.2'), null);
assert.strictEqual(versionManager.parseVersion(''), null);
console.log('✓ parseVersion tests passed\n');
// Test formatVersion
console.log('Test 2: formatVersion');
assert.strictEqual(versionManager.formatVersion(1, 2, 3), '1.2.3');
assert.strictEqual(versionManager.formatVersion(10, 0, 0), '10.0.0');
console.log('✓ formatVersion tests passed\n');
// Test bumpVersion
console.log('Test 3: bumpVersion');
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'patch'), '1.2.4');
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'minor'), '1.3.0');
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'major'), '2.0.0');
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'keep'), '1.2.3');
assert.strictEqual(versionManager.bumpVersion('1.2.3', 'unknown'), '1.2.4'); // defaults to patch
assert.strictEqual(versionManager.bumpVersion('invalid', 'patch'), null);
console.log('✓ bumpVersion tests passed\n');
// Test extractHeaderVersion
console.log('Test 4: extractHeaderVersion');
const headerContent = `<?php
/**
* Plugin Name: Test Plugin
* Version: 2.5.1
* Author: Test
*/`;
assert.strictEqual(versionManager.extractHeaderVersion(headerContent), '2.5.1');
const headerContentNoSpace = `<?php
/**
* Plugin Name: Test Plugin
* Version:3.0.0
*/`;
assert.strictEqual(versionManager.extractHeaderVersion(headerContentNoSpace), '3.0.0');
assert.strictEqual(versionManager.extractHeaderVersion('no version here'), null);
console.log('✓ extractHeaderVersion tests passed\n');
// Test extractDefineVersion
console.log('Test 5: extractDefineVersion');
const defineContent = `<?php
define('TEST_PLUGIN_VERSION', '1.2.3');
define("ANOTHER_VERSION", "2.0.0");`;
assert.strictEqual(versionManager.extractDefineVersion(defineContent), '1.2.3');
const defineContentSpaces = `<?php
define( 'MY_VERSION' , '3.4.5' );`;
assert.strictEqual(versionManager.extractDefineVersion(defineContentSpaces), '3.4.5');
assert.strictEqual(versionManager.extractDefineVersion('no version here'), null);
console.log('✓ extractDefineVersion tests passed\n');
// Test extractAllVersions
console.log('Test 6: extractAllVersions');
const allVersionsContent = `<?php
/**
* Plugin Name: Test
* Version: 1.2.3
*/
define('PLUGIN_VERSION', '1.2.3');`;
const allVersions = versionManager.extractAllVersions(allVersionsContent);
assert.strictEqual(allVersions.headerVersion, '1.2.3');
assert.strictEqual(allVersions.defineVersion, '1.2.3');
assert.strictEqual(allVersions.detectedVersion, '1.2.3');
console.log('✓ extractAllVersions tests passed\n');
// Test updateVersionInContent
console.log('Test 7: updateVersionInContent');
const originalContent = `<?php
/**
* Plugin Name: Test Plugin
* Version: 1.0.0
* Author: Test
*/
define('PLUGIN_VERSION', '1.0.0');`;
const updatedContent = versionManager.updateVersionInContent(originalContent, '1.0.0', '1.1.0');
assert(updatedContent.includes('Version: 1.1.0'));
assert(updatedContent.includes("define('PLUGIN_VERSION', '1.1.0')"));
assert(!updatedContent.includes('Version: 1.0.0'));
assert(!updatedContent.includes("define('PLUGIN_VERSION', '1.0.0')"));
console.log('✓ updateVersionInContent tests passed\n');
// Test isWordPressPluginFile
console.log('Test 8: isWordPressPluginFile');
assert.strictEqual(versionManager.isWordPressPluginFile(headerContent), true);
assert.strictEqual(versionManager.isWordPressPluginFile('<?php echo "hello";'), false);
assert.strictEqual(versionManager.isWordPressPluginFile(''), false);
console.log('✓ isWordPressPluginFile tests passed\n');
// Test getPluginSlugFromPath
console.log('Test 9: getPluginSlugFromPath');
assert.strictEqual(versionManager.getPluginSlugFromPath('/path/to/my-plugin/my-plugin.php'), 'my-plugin');
assert.strictEqual(versionManager.getPluginSlugFromPath('/path/to/plugin-name.php'), 'plugin-name');
console.log('✓ getPluginSlugFromPath tests passed\n');
console.log('========================================');
console.log('All version manager tests passed! ✓');
console.log('========================================');