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

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;
@@ -16977,7 +17106,7 @@ async function handleExportZip(_req, res, sessionId, userId) {
log('Plugin folder will be normalized', { file: mainPluginFile.relativePath, existingFolder, pluginSlug: pluginFolderName });
}
}
// Determine the optimal root to avoid nested wrappers
const optimalRoot = findOptimalExportRoot(validFiles, session.workspaceDir);
@@ -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('========================================');