feat: 升级org.jetbrains.intellij.platform

This commit is contained in:
Gadfly
2025-12-16 13:47:55 +08:00
parent 9bd1841c68
commit 3f6d17aba2
9 changed files with 389 additions and 97 deletions
+2
View File
@@ -64,3 +64,5 @@ webview/src/version/version.ts
# 自定义沙箱目录(如果启用)
.sandbox/
logs/*.log
.intellijPlatform/
+196 -69
View File
@@ -1,132 +1,212 @@
import org.jetbrains.intellij.IntelliJPluginExtension
plugins {
id 'java'
id 'org.jetbrains.intellij' version '1.17.4'
id 'org.jetbrains.intellij.platform' version '2.10.5'
}
group 'com.github.idea-claude-code-gui'
version '0.1.0-beta3'
group = 'com.github.idea-claude-code-gui'
version = '0.1.0-beta3'
sourceCompatibility = 17
targetCompatibility = 17
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
intellijPlatform {
defaultRepositories()
}
}
dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
intellijPlatform {
intellijIdeaCommunity('2024.3.1')
instrumentationTools()
}
}
// 配置 IntelliJ Platform 插件
intellij {
version = '2024.3' // 使用 2024.3 稳定版本
type = 'IC' // IC = IntelliJ IDEA Community Edition
// 下载源码和 JavaDoc
downloadSources = true
// 插件依赖(如果需要)
plugins = []
// 禁用字节码插桩,避免生成 instrumented JAR
instrumentCode = false
}
tasks.withType(JavaCompile) {
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
// Ensure plugin.xml is patched with the correct since/until build and change notes
patchPluginXml {
// plugin compatibility range
sinceBuild = '233'
untilBuild = '263.*' // 兼容到 2026.3 版本,支持更新的 IDE 版本
untilBuild = '263.*'
changeNotes = """
<ul>
<li>初始版本发布</li>
<li>实现右侧工具栏窗口</li>
<li>最小支持IDEA 2023.3版本</li>
<li>兼容至 IDEA 2026.3 版本(build 263.*</li>
</ul>
"""
// Release notes shown in plugin repositories
// Dynamically load the latest changelog entry from CHANGELOG.md and convert to simple HTML.
def changelogFile = file('CHANGELOG.md')
def computedChangeNotes = ''
if (changelogFile.exists()) {
def lines = changelogFile.readLines('UTF-8')
// Find first section header (lines starting with '#####')
def start = -1
for (int i = 0; i < lines.size(); i++) {
if (lines[i].trim().startsWith('#####')) {
start = i
break
}
}
if (start >= 0) {
def sb = new StringBuilder()
boolean inList = false
int i = start + 1
while (i < lines.size() && !lines[i].trim().startsWith('#####')) {
def line = lines[i]
def t = line.trim()
if (t.startsWith('- ')) {
if (!inList) { sb.append('<ul>'); inList = true }
def item = t.substring(2)
// escape HTML special chars for safety
item = item.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
sb.append('<li>' + item + '</li>')
} else if (t.startsWith('![') || t.startsWith('<img')) {
if (inList) { sb.append('</ul>'); inList = false }
// convert markdown image syntax to img tag if necessary
if (t.startsWith('![')) {
def pattern = Pattern.compile(/!\[(.*?)\]\((.*?)\)/)
def matcher = pattern.matcher(t)
if (matcher.find()) {
def alt = matcher.group(1)
def src = matcher.group(2)
sb.append("<img alt=\"${alt}\" src=\"${src}\" width=\"500\" />")
} else {
sb.append(t)
}
} else {
sb.append(t)
}
} else if (t.length() == 0) {
if (inList) { sb.append('</ul>'); inList = false }
sb.append('<br/>')
} else {
if (inList) { sb.append('</ul>'); inList = false }
// paragraph
def para = t.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
sb.append('<p>' + para + '</p>')
}
i++
}
if (inList) { sb.append('</ul>') }
computedChangeNotes = sb.toString()
}
}
changeNotes = computedChangeNotes ?: '<p>No changelog found.</p>'
}
runIde {
// 启用 JCEF 支持
jvmArgs '-Djcef.sandbox.enable=false'
}
buildSearchableOptions {
// Disable product-specific searchable options/instrument tasks if present
// Use matching to cover task name variants like buildSearchableOptionsIC / buildSearchableOptionsIU etc.
tasks.matching { it.name.startsWith('buildSearchableOptions') }.configureEach {
enabled = false
}
// 禁用 instrumented JAR 生成(这是导致 "invalid plugin descriptor" 错误的根源)
// instrumented JAR 是用于代码覆盖率测试的,不应该被打包到最终的插件分发包中
tasks.named("instrumentCode") {
tasks.named('buildSearchableOptions') {
enabled = false
}
tasks.named("instrumentTestCode") {
tasks.named('instrumentCode') {
enabled = false
}
tasks.named('jarSearchableOptions') {
enabled = false
}
tasks.named('runIde') {
jvmArgumentProviders.add({
[
'-Djcef.sandbox.enable=false',
// 禁用插件自动卸载(避免检测到文件变更时自动卸载)
'-Didea.auto.reload.plugins=false',
// 禁用插件动态卸载
'-Didea.dynamic.plugins.allowed=false',
// 启用内部模式
'-Didea.is.internal=true',
// 禁用文件系统事件监控(可选)
// '-Didea.filewatcher.disabled=true',
// 增加插件加载超时
'-Didea.plugins.load.timeout=60000'
]
} as CommandLineArgumentProvider)
// 设置系统属性
systemProperty 'idea.auto.reload.plugins', 'false'
systemProperty 'idea.dynamic.plugins.allowed', 'false'
systemProperty 'idea.is.internal', 'true'
systemProperty 'idea.plugin.in.sandbox.mode', 'true'
// 复制自定义配置到sandbox
doFirst {
def sandboxConfig = file('sandbox-idea.properties')
if (sandboxConfig.exists()) {
def sandboxDir = layout.buildDirectory.dir("idea-sandbox").get().asFile
def configDir = new File(sandboxDir, 'config')
configDir.mkdirs()
copy {
from sandboxConfig
into configDir
rename { 'idea.properties' }
}
println "已复制sandbox配置到: ${configDir}/idea.properties"
}
}
}
// 统一的 AI Bridge(合并 Claude 和 Codex
def aiBridgeDir = file("ai-bridge")
def aiBridgePackDir = file("$buildDir/ai-bridge-pack")
def aiBridgeArchive = new File(aiBridgePackDir, "ai-bridge.zip")
def aiBridgePackDir = layout.buildDirectory.dir('ai-bridge-pack').get().asFile
def aiBridgeArchive = new File(aiBridgePackDir, 'ai-bridge.zip')
// 自动构建 webview
def webviewDir = file('webview')
tasks.register('buildWebview', Exec) {
workingDir webviewDir
// 根据操作系统选择命令
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
commandLine 'cmd', '/c', 'npm', 'run', 'build'
} else {
commandLine 'npm', 'run', 'build'
}
// 只在 webview 目录存在时执行(使用变量避免配置缓存问题)
onlyIf { webviewDir.exists() }
// 输出构建信息
doFirst {
println "Building webview..."
}
doLast {
println "Webview build completed."
}
doFirst { println "Building webview..." }
doLast { println "Webview build completed." }
}
tasks.register('packageAiBridge', Zip) {
onlyIf { aiBridgeDir.exists() }
archiveFileName = "ai-bridge.zip"
archiveFileName = 'ai-bridge.zip'
destinationDirectory = aiBridgePackDir
from(aiBridgeDir)
exclude("node_modules/.pnpm/**")
exclude('node_modules/.pnpm/**')
includeEmptyDirs = true
}
// 提取插件名称,避免在闭包中访问 extensions
def intellijExt = project.extensions.getByType(IntelliJPluginExtension)
def pluginName = intellijExt.pluginName.getOrElse(project.name)
// 提取插件名称
def pluginName = project.name
tasks.named("prepareSandbox") {
dependsOn("buildWebview", "packageAiBridge")
def sandboxRootProvider = layout.buildDirectory.dir("idea-sandbox")
tasks.matching { it.name.startsWith('prepareSandbox') }.configureEach {
dependsOn('buildWebview', 'packageAiBridge')
def sandboxRootProvider = layout.buildDirectory.dir('idea-sandbox')
doLast {
if (!aiBridgeDir.exists()) {
throw new GradleException("ai-bridge 目录不存在,请确保已拉取并安装依赖。")
if (!aiBridgeDir.exists() && !aiBridgeArchive.exists()) {
throw new GradleException('ai-bridge 目录不存在,请确保已拉取并安装依赖。')
}
def sandboxRoot = sandboxRootProvider.get().asFile
def pluginDir = new File(sandboxRoot, "plugins/${pluginName}")
// 部署 ai-bridge
def extractedDir = new File(pluginDir, "ai-bridge")
def archiveTarget = new File(pluginDir, "ai-bridge.zip")
def extractedDir = new File(pluginDir, 'ai-bridge')
def archiveTarget = new File(pluginDir, 'ai-bridge.zip')
project.delete(extractedDir)
project.delete(archiveTarget)
project.copy {
@@ -136,6 +216,53 @@ tasks.named("prepareSandbox") {
}
}
tasks.named("buildPlugin") {
dependsOn("packageAiBridge")
// 配置 prepareSandbox 之后的任务,确保 ai-bridge.zip 被包含
tasks.matching { it.name == 'preparePluginForDistribution' || it.name == 'composedJar' }.configureEach {
dependsOn('packageAiBridge')
}
tasks.named('buildPlugin') {
dependsOn('packageAiBridge')
doLast {
// 解压插件 zip,添加 ai-bridge.zip,然后重新打包
def distributionFile = archiveFile.get().asFile
if (distributionFile.exists() && aiBridgeArchive.exists()) {
def tempDir = file("${layout.buildDirectory.get()}/tmp/plugin-repack")
delete(tempDir)
// 解压
copy {
from zipTree(distributionFile)
into tempDir
}
// 找到插件目录并添加 ai-bridge.zip
def pluginDir = new File(tempDir, pluginName)
if (pluginDir.exists()) {
copy {
from aiBridgeArchive
into pluginDir
}
// 删除 searchableOptions jar(如果存在)
fileTree(new File(pluginDir, 'lib')).matching {
include '**/*searchableOptions*.jar'
}.each { jarFile ->
delete(jarFile)
println "已删除 searchableOptions jar: ${jarFile.name}"
}
// 重新打包
delete(distributionFile)
ant.zip(destfile: distributionFile) {
fileset(dir: tempDir)
}
println "已将 ai-bridge.zip 添加到插件包并移除 searchableOptions jar"
}
delete(tempDir)
}
}
}
+3
View File
@@ -1 +1,4 @@
kotlin.code.style=official
# org.gradle.unsafe.configuration-cache=true
org.jetbrains.intellij.buildFeature.selfUpdateCheck=false
+5
View File
@@ -0,0 +1,5 @@
idea.auto.reload.plugins=false
idea.is.internal=true
idea.plugin.in.sandbox.mode=true
idea.plugins.load.timeout=60000
+9 -1
View File
@@ -1 +1,9 @@
rootProject.name = 'idea-claude-code-gui'
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = 'idea-claude-code-gui'
@@ -757,26 +757,48 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
private void pushUsageUpdateFromMessages(List<ClaudeSession.Message> messages) {
try {
System.out.println("[Backend] pushUsageUpdateFromMessages called with " + messages.size() + " messages");
JsonObject lastUsage = null;
for (int i = messages.size() - 1; i >= 0; i--) {
ClaudeSession.Message msg = messages.get(i);
if (msg.type != ClaudeSession.Message.Type.ASSISTANT || msg.raw == null) continue;
if (!msg.raw.has("message")) continue;
JsonObject message = msg.raw.getAsJsonObject("message");
if (message.has("usage")) {
lastUsage = message.getAsJsonObject("usage");
if (msg.type != ClaudeSession.Message.Type.ASSISTANT || msg.raw == null) {
continue;
}
// 检查不同的可能结构
if (msg.raw.has("message")) {
JsonObject message = msg.raw.getAsJsonObject("message");
if (message.has("usage")) {
lastUsage = message.getAsJsonObject("usage");
break;
}
}
// 检查usage是否在raw的根级别
if (msg.raw.has("usage")) {
lastUsage = msg.raw.getAsJsonObject("usage");
break;
}
}
if (lastUsage == null) {
System.out.println("[Backend] WARNING: No usage info found in messages!");
}
int inputTokens = lastUsage != null && lastUsage.has("input_tokens") ? lastUsage.get("input_tokens").getAsInt() : 0;
int cacheWriteTokens = lastUsage != null && lastUsage.has("cache_creation_input_tokens") ? lastUsage.get("cache_creation_input_tokens").getAsInt() : 0;
int cacheReadTokens = lastUsage != null && lastUsage.has("cache_read_input_tokens") ? lastUsage.get("cache_read_input_tokens").getAsInt() : 0;
int outputTokens = lastUsage != null && lastUsage.has("output_tokens") ? lastUsage.get("output_tokens").getAsInt() : 0;
int usedTokens = inputTokens + cacheWriteTokens + cacheReadTokens;
int usedTokens = inputTokens + cacheWriteTokens + cacheReadTokens + outputTokens;
int maxTokens = MODEL_CONTEXT_LIMITS.getOrDefault(currentModel, 200_000);
int percentage = Math.min(100, maxTokens > 0 ? (int) ((usedTokens * 100.0) / maxTokens) : 0);
System.out.println("[Backend] Pushing usage update: input=" + inputTokens + ", cacheWrite=" + cacheWriteTokens + ", cacheRead=" + cacheReadTokens + ", output=" + outputTokens + ", total=" + usedTokens + ", max=" + maxTokens + ", percentage=" + percentage + "%");
JsonObject usageUpdate = new JsonObject();
usageUpdate.addProperty("percentage", percentage);
usageUpdate.addProperty("totalTokens", usedTokens);
@@ -786,13 +808,22 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
String usageJson = new Gson().toJson(usageUpdate);
SwingUtilities.invokeLater(() -> {
String js = "if (window.onUsageUpdate) { window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "'); }";
if (browser != null && !disposed) {
// 使用安全的调用方式,检查函数是否存在
String js = "(function() {" +
" if (typeof window.onUsageUpdate === 'function') {" +
" window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "');" +
" console.log('[Backend->Frontend] Usage update sent successfully');" +
" } else {" +
" console.warn('[Backend->Frontend] window.onUsageUpdate not found');" +
" }" +
"})();";
browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
}
});
} catch (Exception e) {
System.err.println("[Backend] Failed to push usage update: " + e.getMessage());
e.printStackTrace();
}
}
@@ -842,8 +873,17 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
usageUpdate.addProperty("maxTokens", maxTokens);
String usageJson = new Gson().toJson(usageUpdate);
String js = "if (window.onUsageUpdate) { window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "'); }";
if (browser != null && !disposed) {
// 使用安全的调用方式
String js = "(function() {" +
" if (typeof window.onUsageUpdate === 'function') {" +
" window.onUsageUpdate('" + JsUtils.escapeJs(usageJson) + "');" +
" console.log('[Backend->Frontend] Usage reset for new session');" +
" } else {" +
" console.warn('[Backend->Frontend] window.onUsageUpdate not found');" +
" }" +
"})();";
browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
}
});
@@ -879,14 +919,34 @@ public class ClaudeSDKToolWindow implements ToolWindowFactory, DumbAware {
private void callJavaScript(String functionName, String... args) {
if (disposed || browser == null) {
System.err.println("[ClaudeSDKToolWindow] 无法调用 JS 函数 " + functionName + ": disposed=" + disposed + ", browser=" + (browser == null ? "null" : "exists"));
return;
}
try {
String js = JsUtils.buildJsCall(functionName, args);
browser.getCefBrowser().executeJavaScript(js, browser.getCefBrowser().getURL(), 0);
} catch (Exception e) {
System.err.println("[ClaudeSDKToolWindow] 调用 JS 函数失败: " + functionName + ", 错误: " + e.getMessage());
}
SwingUtilities.invokeLater(() -> {
if (disposed || browser == null) {
return;
}
try {
String js = JsUtils.buildJsCall(functionName, args);
// 先检查函数是否存在,再调用
String checkAndCall =
"(function() {" +
" if (typeof window." + functionName + " === 'function') {" +
" " + js +
" console.log('[Backend->Frontend] Successfully called " + functionName + "');" +
" } else {" +
" console.warn('[Backend->Frontend] Function " + functionName + " not found on window');" +
" }" +
"})();";
browser.getCefBrowser().executeJavaScript(checkAndCall, browser.getCefBrowser().getURL(), 0);
} catch (Exception e) {
System.err.println("[ClaudeSDKToolWindow] 调用 JS 函数失败: " + functionName + ", 错误: " + e.getMessage());
e.printStackTrace();
}
});
}
/**
@@ -546,6 +546,44 @@ public class ClaudeSession {
loading = false;
updateState();
System.out.println("[ClaudeSession] Message end received, loading set to false");
} else if ("result".equals(type) && content.startsWith("{")) {
// 处理结果消息(包含最终的usage信息)
try {
JsonObject resultJson = gson.fromJson(content, JsonObject.class);
System.out.println("[ClaudeSession] Result message received");
// 如果当前消息的raw中usage为0,则用result中的usage进行更新
if (currentAssistantMessage != null && currentAssistantMessage.raw != null) {
JsonObject message = currentAssistantMessage.raw.has("message") && currentAssistantMessage.raw.get("message").isJsonObject()
? currentAssistantMessage.raw.getAsJsonObject("message")
: null;
// 检查当前消息的usage是否全为0
boolean needsUsageUpdate = false;
if (message != null && message.has("usage")) {
JsonObject usage = message.getAsJsonObject("usage");
int inputTokens = usage.has("input_tokens") ? usage.get("input_tokens").getAsInt() : 0;
int outputTokens = usage.has("output_tokens") ? usage.get("output_tokens").getAsInt() : 0;
if (inputTokens == 0 && outputTokens == 0) {
needsUsageUpdate = true;
}
} else {
needsUsageUpdate = true;
}
if (needsUsageUpdate && resultJson.has("usage")) {
JsonObject resultUsage = resultJson.getAsJsonObject("usage");
if (message != null) {
message.add("usage", resultUsage);
currentAssistantMessage.raw = currentAssistantMessage.raw;
notifyMessageUpdate();
System.out.println("[ClaudeSession] Updated assistant message usage from result message");
}
}
}
} catch (Exception e) {
System.err.println("[ClaudeSession] Failed to parse result message: " + e.getMessage());
}
} else if ("slash_commands".equals(type)) {
// 处理斜杠命令列表
try {
@@ -158,14 +158,14 @@ public class BridgeDirectoryResolver {
try {
String pluginsRoot = PathManager.getPluginsPath();
if (pluginsRoot != null && !pluginsRoot.isEmpty()) {
if (!pluginsRoot.isEmpty()) {
addCandidate(possibleDirs, Paths.get(pluginsRoot, PLUGIN_DIR_NAME, SDK_DIR_NAME).toFile());
addCandidate(possibleDirs, Paths.get(pluginsRoot, PLUGIN_ID, SDK_DIR_NAME).toFile());
}
// 使用系统路径下的 plugins 目录代替已废弃的 getPluginTempPath()
String systemPath = PathManager.getSystemPath();
if (systemPath != null && !systemPath.isEmpty()) {
if (!systemPath.isEmpty()) {
Path sandboxPath = Paths.get(systemPath, "plugins");
addCandidate(possibleDirs, sandboxPath.resolve(PLUGIN_DIR_NAME).resolve(SDK_DIR_NAME).toFile());
addCandidate(possibleDirs, sandboxPath.resolve(PLUGIN_ID).resolve(SDK_DIR_NAME).toFile());
@@ -282,7 +282,55 @@ public class BridgeDirectoryResolver {
}
}
}
return null;
// 如果在插件目录或 lib 下找不到,尝试查找常见的 sandbox 顶级 plugins 目录和 system/config 下的 plugins
List<File> fallbackCandidates = new ArrayList<>();
try {
// 向上查找祖先,寻找可能的 idea-sandbox 根目录或包含顶级 plugins 的目录
File ancestor = pluginDir;
int climbs = 0;
while (climbs < 6) {
File parent = ancestor.getParentFile();
if (parent == null) break;
File maybeTopPlugins = new File(parent, "plugins");
if (maybeTopPlugins.exists() && maybeTopPlugins.isDirectory()) {
fallbackCandidates.add(new File(maybeTopPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
fallbackCandidates.add(new File(maybeTopPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
}
// system/config siblings under this parent
File maybeSystemPlugins = new File(parent, "system/plugins");
File maybeConfigPlugins = new File(parent, "config/plugins");
if (maybeSystemPlugins.exists() && maybeSystemPlugins.isDirectory()) {
fallbackCandidates.add(new File(maybeSystemPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
fallbackCandidates.add(new File(maybeSystemPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
}
if (maybeConfigPlugins.exists() && maybeConfigPlugins.isDirectory()) {
fallbackCandidates.add(new File(maybeConfigPlugins, PLUGIN_DIR_NAME + File.separator + SDK_ARCHIVE_NAME));
fallbackCandidates.add(new File(maybeConfigPlugins, PLUGIN_ID + File.separator + SDK_ARCHIVE_NAME));
}
ancestor = parent;
climbs++;
}
} catch (Throwable ignore) {
// ignore fallback discovery errors
}
// 打印并尝试这些候选路径
for (File f : fallbackCandidates) {
System.out.println("[BridgeResolver] 尝试候选路径: " + f.getAbsolutePath() + " (存在: " + f.exists() + ")");
if (f.exists()) {
archiveFile = f;
break;
}
}
if (!archiveFile.exists()) {
return null;
}
}
File extractedDir = new File(pluginDir, SDK_DIR_NAME);
@@ -372,26 +420,27 @@ public class BridgeDirectoryResolver {
}
/**
* 手动设置 claude-bridge 目录路径
* 手动设置 claude-bridge 目录路径.
*/
public void setSdkDir(String path) {
this.cachedSdkDir = new File(path);
}
/**
* 获取当前使用的 claude-bridge 目录
* 获取当前使用的 claude-bridge 目录.
*/
public File getSdkDir() {
if (cachedSdkDir == null) {
return findSdkDir();
if (this.cachedSdkDir == null) {
return this.findSdkDir();
}
return cachedSdkDir;
return this.cachedSdkDir;
}
/**
* 清除缓存
* 清除缓存.
*/
public void clearCache() {
this.cachedSdkDir = null;
}
}
+4 -4
View File
@@ -16,14 +16,14 @@ const buildGradlePath = path.join(projectRoot, 'build.gradle');
const buildGradleContent = fs.readFileSync(buildGradlePath, 'utf8');
// 提取版本号
// 查找类似 version '0.1.0-beta3' 这样的行
let versionMatch = buildGradleContent.match(/^version\s+'(.+)'$/m);
// 查找类似 version = '0.1.0-beta3' 这样的行
let versionMatch = buildGradleContent.match(/^version\s*=\s*'(.+)'$/m);
if (!versionMatch) {
// 如果上面的正则失败,尝试另一种方式
const lines = buildGradleContent.split('\n');
const versionLine = lines.find(line => line.trim().startsWith('version '));
const versionLine = lines.find(line => line.trim().startsWith('version ='));
if (versionLine) {
const match = versionLine.match(/version\s+'(.+)'/);
const match = versionLine.match(/version\s*=\s*'(.+)'/);
if (match) {
versionMatch = match;
}