Compare commits

..
Author SHA1 Message Date
EthanHealy01andGitHub fb28de4d5e Merge branch 'main' into dev/ChangeBrowserLabelToMatchWorktree 2026-07-09 21:03:23 +01:00
EthanHealy01 eb754d12c3 feat(dev): prefix browser tab title with worktree name in dev
When running task dev / dev:all / dev:saas / dev:portal from a worktree,
the frontend dev server injects the worktree folder basename (e.g. wt1)
as a build-time constant, and the app prefixes the browser tab title with
it so concurrent worktrees are distinguishable instead of all reading
"Stirling PDF".

Only the folder basename is exposed (never path/host/user), and only at
vite dev-serve time — production builds inject an empty string and the
feature compiles to a no-op. Desktop (tauri dev) is unaffected.
2026-07-09 14:19:33 +01:00
18 changed files with 205 additions and 619 deletions
+69 -106
View File
@@ -15,16 +15,9 @@ version: '3'
# stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`)
# because modern Windows excludes cwd from cmd's search path.
vars:
GRADLE: '{{if eq OS "windows"}}cmd /c ".\gradlew.bat"{{else}}./gradlew{{end}}'
TEST: '{{.GRADLE}} test --no-daemon'
FORMAT: '{{.GRADLE}} spotlessApply'
FORMATCHECK: '{{.GRADLE}} spotlessCheck'
CLEAN: '{{.GRADLE}} clean'
tasks:
dev:
desc: "Start the backend dev server"
desc: "Start backend dev server"
cmds:
- task: dev:proprietary
vars:
@@ -36,7 +29,7 @@ tasks:
POLICIES_ENABLED: '{{.POLICIES_ENABLED}}'
dev:proprietary:
desc: "Start the backend dev server in proprietary mode"
desc: "Start backend dev server in proprietary mode"
# `dotenv:` reads from the root Taskfile's directory (".") because this
# subtaskfile is included with `dir: .`. Local overrides in
# .env.proprietary.local win over the committed .env.proprietary defaults.
@@ -52,16 +45,22 @@ tasks:
env:
SERVER_PORT: '{{.PORT}}'
cmds:
- '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}{{.GRADLE}} :stirling-pdf:bootRun'
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
platforms: [windows]
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .POLICIES_ENABLED}}POLICIES_ENABLED={{.POLICIES_ENABLED}} {{end}}./gradlew :stirling-pdf:bootRun'
platforms: [linux, darwin]
dev:bundled:
desc: "Start the backend with the frontend bundled into the JAR"
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
ignore_error: true
cmds:
- '{{.CLEAN}} bootRun -PbuildWithFrontend=true'
- cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true"
platforms: [windows]
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
platforms: [linux, darwin]
dev:saas:
desc: "Start the backend in SaaS flavor"
desc: "Start backend in SaaS flavor against Supabase"
# `dotenv:` reads from the root Taskfile's directory (".") because this
# subtaskfile is included with `dir: .`.
dotenv: ['app/.env.saas.local', 'app/.env.saas']
@@ -80,104 +79,71 @@ tasks:
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
cmds:
- '{{.GRADLE}} :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}'
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}"
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}}
platforms: [linux, darwin]
build:
desc: "Build the backend"
desc: "Full backend build"
cmds:
- '{{.CLEAN}} build'
- cmd: cmd /c ".\gradlew.bat clean build"
platforms: [windows]
- cmd: ./gradlew clean build
platforms: [linux, darwin]
build:fast:
desc: "Build the backend without running tests"
desc: "Build without tests"
cmds:
- '{{.CLEAN}} build -x test'
- cmd: cmd /c ".\gradlew.bat clean build -x test"
platforms: [windows]
- cmd: ./gradlew clean build -x test
platforms: [linux, darwin]
build:ci:
desc: "Build the backend for CI"
desc: "Build for CI (formatting checked separately)"
cmds:
- '{{.GRADLE}} build -PnoSpotless'
prematrix:*:
desc: "Run the backend test matrix"
vars:
MATRIXNAME: '{{index .MATCH 0}}'
cmds:
- for:
matrix:
FLAVOR: ["proprietary", "core", "saas"]
LOGIN: ["true", "false"]
SEC: ["true", "false"]
task: '{{.MATRIXNAME}}:matrix'
vars:
STIRLING_FLAVOR: '{{.ITEM.FLAVOR}}'
SECURITY_ENABLELOGIN: '{{.ITEM.LOGIN}}'
DOCKER_ENABLE_SECURITY: '{{.ITEM.SEC}}'
- cmd: cmd /c ".\gradlew.bat build -PnoSpotless"
platforms: [windows]
- cmd: ./gradlew build -PnoSpotless
platforms: [linux, darwin]
test:
desc: "Run the backend test matrix"
desc: "Run backend tests"
cmds:
- task: prematrix:test
test:matrix:
internal: true
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
desc: "Run backend tests for one configuration"
# Cover the backend with the main build/property combinations that change
# Gradle behavior in this repo.
cmds:
- '{{.TEST}}'
- '{{.TEST}} -PbuildWithFrontend=true'
- '{{.TEST}} -PprototypesMode=true'
- '{{.TEST}} -PbuildWithFrontend=true -PprototypesMode=true'
- cmd: cmd /c ".\gradlew.bat test"
platforms: [windows]
- cmd: ./gradlew test
platforms: [linux, darwin]
format:
desc: "Run the backend formatting matrix"
desc: "Auto-fix code formatting"
cmds:
- task: prematrix:format
format:matrix:
internal: true
desc: "Apply backend formatting for one configuration"
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
cmds:
- '{{.FORMAT}}'
- '{{.FORMAT}} -PbuildWithFrontend=true'
- '{{.FORMAT}} -PprototypesMode=true'
- cmd: cmd /c ".\gradlew.bat spotlessApply"
platforms: [windows]
- cmd: ./gradlew spotlessApply
platforms: [linux, darwin]
format:check:
desc: "Check the backend formatting matrix"
desc: "Check code formatting"
cmds:
- task: prematrix:format:check
format:check:matrix:
internal: true
desc: "Check backend formatting for one configuration"
env:
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN}}'
STIRLING_FLAVOR: '{{.STIRLING_FLAVOR}}'
DOCKER_ENABLE_SECURITY: '{{.DOCKER_ENABLE_SECURITY}}'
# Mirror the test matrix so formatting checks use the same backend
# configuration variants.
cmds:
- '{{.FORMATCHECK}}'
- '{{.FORMATCHECK}} -PbuildWithFrontend=true'
- '{{.FORMATCHECK}} -PprototypesMode=true'
- cmd: cmd /c ".\gradlew.bat spotlessCheck"
platforms: [windows]
- cmd: ./gradlew spotlessCheck
platforms: [linux, darwin]
fix:
desc: "Apply backend fixes"
desc: "Auto-fix backend"
cmds:
- task: format
swagger:
desc: "Generate the backend OpenAPI docs"
desc: "Generate OpenAPI docs"
cmds:
- '{{.GRADLE}} :stirling-pdf:copySwaggerDoc'
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
platforms: [windows]
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
platforms: [linux, darwin]
sources:
- app/core/src/main/java/**/*.java
- app/proprietary/src/main/java/**/*.java
@@ -186,43 +152,40 @@ tasks:
- SwaggerDoc.json
check:
desc: "Run the backend quality gate"
desc: "Backend quality gate"
cmds:
- task: format:check
- task: test
version:
desc: "Print the backend version"
desc: "Print project version"
silent: true
cmds:
- cmd: pwsh -NoProfile -File scripts/backend-version.ps1
- cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1
platforms: [windows]
- cmd: ./gradlew printVersion --quiet | tail -1
platforms: [linux, darwin]
licenses:check:
desc: "Check backend dependency licenses"
desc: "Check dependency licenses"
cmds:
- '{{.GRADLE}} checkLicense --no-parallel'
- cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel"
platforms: [windows]
- cmd: ./gradlew checkLicense --no-parallel
platforms: [linux, darwin]
licenses:generate:
desc: "Generate the backend dependency license report"
env:
# Use the SaaS flavor so the license report includes all dependencies.
STIRLING_FLAVOR: 'saas'
desc: "Check and generate dependency license report"
cmds:
- '{{.GRADLE}} checkLicense generateLicenseReport --no-parallel'
licenses:generate:copy:
desc: "Generate and copy the backend dependency license report"
deps: [licenses:generate]
cmds:
- cmd: cp build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
platforms: [linux, darwin]
- cmd: powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path 'app/core/src/main/resources/static' | Out-Null; Copy-Item -Force 'build/reports/dependency-license/index.json' 'app/core/src/main/resources/static/3rdPartyLicenses.json'"
- cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel"
platforms: [windows]
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
platforms: [linux, darwin]
clean:
desc: "Clean backend build artifacts"
desc: "Clean build artifacts"
cmds:
- '{{.CLEAN}}'
- cmd: cmd /c ".\gradlew.bat clean"
platforms: [windows]
- cmd: ./gradlew clean
platforms: [linux, darwin]
+6
View File
@@ -80,6 +80,12 @@ tasks:
OPEN: '{{.OPEN | default ""}}'
env:
BACKEND_URL: '{{.BACKEND_URL}}'
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
# full path, hostname, or user. Consumed at dev-serve time by vite.config
# and dropped from production builds.
STIRLING_DEV_LABEL:
sh: basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cmds:
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
+11 -11
View File
@@ -6,7 +6,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -29,19 +29,19 @@ spotless {
}
}
dependencies {
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api "com.google.guava:guava:${guavaVersion}"
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api "org.apache.commons:commons-lang3:${rootProject.ext.commonsLang3}"
api "org.apache.commons:commons-lang3:${commonsLang3}"
api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
api "org.apache.pdfbox:pdfbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:pdfbox-io:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.10' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
@@ -60,7 +60,7 @@ dependencies {
exclude group: 'com.google.code.gson', module: 'gson'
}
api "com.stirling:jpdfium:${rootProject.ext.jpdfiumVersion}"
api "com.stirling:jpdfium:${jpdfiumVersion}"
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
@@ -75,12 +75,12 @@ dependencies {
}
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
jpdfiumPlatforms.each { platform ->
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${rootProject.ext.jpdfiumVersion}"
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
}
// Bucket4j (local in-process token bucket for RateLimitStore default impl)
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
testImplementation "com.tngtech.archunit:archunit-junit5:${rootProject.ext.archunitVersion}"
testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}"
}
-2
View File
@@ -1,2 +0,0 @@
# Auto-generated by MSW (`msw init`); regenerated verbatim, not hand-formatted.
src/main/resources/static/mockServiceWorker.js
+20 -23
View File
@@ -13,7 +13,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/resources/static/**', 'src/main/java/org/apache/**'
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -67,21 +67,21 @@ dependencies {
exclude group: 'com.fasterxml.jackson.jaxrs'
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
}
implementation "commons-io:commons-io:${rootProject.ext.commonsIoVersion}"
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "org.bouncycastle:bcpkix-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "commons-io:commons-io:$commonsIoVersion"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation 'io.micrometer:micrometer-core'
implementation 'com.google.zxing:core:3.5.4'
implementation "org.commonmark:commonmark:${rootProject.ext.commonmarkVersion}" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:${rootProject.ext.commonmarkVersion}"
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
// General PDF dependencies
implementation "org.apache.pdfbox:preflight:${rootProject.ext.pdfboxVersion}"
implementation "org.apache.pdfbox:xmpbox:${rootProject.ext.pdfboxVersion}"
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion"
implementation 'org.verapdf:validation-model:1.28.2'
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
implementation "org.mozilla:rhino:${rootProject.ext.rhinoVersion}"
implementation "org.mozilla:rhino:${rhinoVersion}"
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
implementation 'javax.xml.bind:jaxb-api:2.3.1'
@@ -89,33 +89,33 @@ dependencies {
implementation 'com.sun.xml.bind:jaxb-core:4.0.7'
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
implementation "org.apache.xmlgraphics:batik-bridge:${rootProject.ext.batikVersion}"
implementation "org.apache.xmlgraphics:batik-bridge:${batikVersion}"
// Required by TwelveMonkeys imageio-batik SPI (SVGImageReaderSpi) during ImageIO init
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${rootProject.ext.batikVersion}"
runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${batikVersion}"
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:${rootProject.ext.imageioVersion}"
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
@@ -135,6 +135,7 @@ sourceSets {
}
// Disable regular jar
jar {
enabled = false
@@ -291,11 +292,7 @@ tasks.register('npmBuild', Exec) {
group = 'frontend'
description = 'Build editor frontend application'
workingDir file('../..')
// Pin the repo-root Taskfile explicitly so the Exec task does not inherit
// an unrelated TASKFILE override from the surrounding environment.
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ?
['cmd', '/c', 'task', '--taskfile', 'Taskfile.yml', frontendBuildTask] :
['task', '--taskfile', 'Taskfile.yml', frontendBuildTask]
commandLine = ['task', frontendBuildTask]
inputs.dir(new File(frontendEditorDir, 'src'))
inputs.dir(new File(frontendEditorDir, 'public'))
inputs.file(new File(frontendDir, 'package.json'))
@@ -20,18 +20,17 @@
--cc-separator-border-color: #e0e0e0;
/* Toggle colors mirror Mantine Switch (light scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #007bff);
--cc-toggle-off-bg: var(--mantine-color-gray-3, #dee2e6);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-on-bg: #007bff;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #ffffff;
--cc-toggle-off-knob-bg: #ffffff;
--cc-toggle-enabled-icon-color: #ffffff;
--cc-toggle-disabled-icon-color: #ffffff;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #f1f3f4);
--cc-toggle-readonly-knob-bg: var(--mantine-color-gray-0, #f8f9fa);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-toggle-readonly-bg: #f1f3f4;
--cc-toggle-readonly-knob-bg: #79747e;
--cc-toggle-readonly-knob-icon-color: #f1f3f4;
--cc-section-category-border: #e0e0e0;
@@ -70,18 +69,17 @@
--cc-separator-border-color: #555555;
/* Toggle colors mirror Mantine Switch (dark scheme) */
--cc-toggle-on-bg: var(--mantine-primary-color-filled, #4dabf7);
--cc-toggle-off-bg: var(--mantine-color-dark-5, #555555);
--cc-toggle-on-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-off-knob-bg: var(--mantine-color-white, #ffffff);
--cc-toggle-on-bg: #4dabf7;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #2d2d2d;
--cc-toggle-off-knob-bg: #2d2d2d;
--cc-toggle-enabled-icon-color: #2d2d2d;
--cc-toggle-disabled-icon-color: #2d2d2d;
--cc-toggle-readonly-bg: var(--mantine-color-disabled, #555555);
--cc-toggle-readonly-knob-bg: var(--mantine-color-dark-3, #8e8e8e);
--cc-toggle-readonly-knob-icon-color: transparent;
--cc-toggle-readonly-bg: #555555;
--cc-toggle-readonly-knob-bg: #8e8e8e;
--cc-toggle-readonly-knob-icon-color: #555555;
--cc-section-category-border: #555555;
@@ -178,16 +176,9 @@
color: var(--cc-primary-color) !important;
}
/* Banner sits above the chat FAB but behind all modals and onboarding; value
is Z_INDEX_COOKIE_CONSENT_BANNER, set as this variable by useCookieConsent */
/* Lower z-index so cookie banner appears behind onboarding modals */
#cc-main {
z-index: var(--z-index-cookie-consent) !important;
}
/* Preferences dialog sits above the settings modal it opens from; value is
Z_INDEX_COOKIE_PREFERENCES_MODAL, set as this variable by useCookieConsent */
.show--preferences #cc-main {
z-index: var(--z-index-cookie-preferences) !important;
z-index: 100 !important;
}
/* Ensure consent modal text is visible in both themes */
@@ -212,63 +203,3 @@
#cc-main .cm__link {
color: var(--cc-primary-color) !important;
}
/* ── Category toggles restyled to match Mantine Switch (size sm) ──────────
Mantine sm metrics: 38×20 track, 14px plain thumb, 2.5px inline padding,
150ms ease transitions, no icon inside the thumb. Colors come from the
--cc-toggle-* variables above, which point at the Mantine palette. */
#cc-main .section__toggle,
#cc-main .section__toggle-wrapper,
#cc-main .toggle__icon,
#cc-main .toggle__label {
width: 38px !important;
height: 20px !important;
border-radius: 1000px !important;
}
/* Track: flat fill, no outline ring or border */
#cc-main .toggle__icon {
border: none !important;
box-shadow: none !important;
transition: background-color 150ms ease !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
border: none !important;
box-shadow: none !important;
}
/* Always-enabled categories = Mantine disabled switch (must out-prioritise
the !important checked-track rule above) */
#cc-main .section__toggle:checked:disabled ~ .toggle__icon {
background: var(--cc-toggle-readonly-bg) !important;
border: none !important;
box-shadow: none !important;
}
#cc-main .section__toggle:disabled {
cursor: not-allowed !important;
}
/* Thumb: small plain circle, vertically centred, no drop shadow */
#cc-main .toggle__icon-circle {
width: 14px !important;
height: 14px !important;
top: 3px !important;
left: 2.5px !important;
box-shadow: none !important;
transition:
transform 150ms ease,
background-color 150ms ease !important;
}
/* Checked thumb travel: 38 14 2.5 = 21.5px end position */
#cc-main .section__toggle:checked ~ .toggle__icon .toggle__icon-circle {
transform: translateX(19px) !important;
}
/* Mantine switches have no check/cross glyph inside the thumb */
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
display: none !important;
}
@@ -1,349 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.14.6'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}
@@ -147,17 +147,11 @@ class SvgToPdfTest {
ImageIO.write(red, "png", external.toFile());
try {
String rootRelativeExternal =
"/"
+ external.toAbsolutePath()
.toString()
.replace('\\', '/')
.replaceFirst("^/+", "");
String svg =
"<svg xmlns=\"http://www.w3.org/2000/svg\" "
+ "xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\">"
+ "<image x=\"0\" y=\"0\" width=\"100\" height=\"100\" xlink:href=\""
+ rootRelativeExternal
+ external.toUri()
+ "\"/></svg>";
byte[] pdf;
+17 -17
View File
@@ -11,7 +11,7 @@ spotless {
java {
target 'src/**/java/**/*.java'
targetExclude 'src/main/java/org/apache/**'
googleJavaFormat(rootProject.ext.googleJavaFormatVersion).aosp().reorderImports(false)
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
// google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25
suppressLintsFor { setStep('google-java-format') }
@@ -35,13 +35,13 @@ spotless {
}
dependencies {
implementation project(':common')
api "com.google.guava:guava:${rootProject.ext.guavaVersion}"
api "com.google.guava:guava:${guavaVersion}"
api 'org.springframework:spring-jdbc'
api 'org.springframework:spring-webmvc'
api 'org.springframework.session:spring-session-core'
api "org.springframework.security:spring-security-core:${rootProject.ext.springSecuritySamlVersion}"
api "org.springframework.security:spring-security-saml2-service-provider:${rootProject.ext.springSecuritySamlVersion}"
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
api 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -55,30 +55,30 @@ dependencies {
api 'com.github.ben-manes.caffeine:caffeine'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
implementation "com.bucket4j:bucket4j_jdk17-core:${rootProject.ext.bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
// Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide
// token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window
// boundary doubling).
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${rootProject.ext.bucket4jVersion}"
implementation "com.bucket4j:bucket4j_jdk17-lettuce:${bucket4jVersion}"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "org.bouncycastle:bcprov-jdk18on:${rootProject.ext.bouncycastleVersion}"
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "com.google.code.gson:gson:${rootProject.ext.gsonVersion}"
implementation "com.google.code.gson:gson:${gsonVersion}"
api 'io.micrometer:micrometer-registry-prometheus'
api "io.jsonwebtoken:jjwt-api:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${rootProject.ext.jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${rootProject.ext.jwtVersion}"
api "io.jsonwebtoken:jjwt-api:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}"
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases
runtimeOnly 'org.postgresql:postgresql:42.7.11'
implementation('com.coveo:saml-client:5.0.0') {
exclude group: 'org.opensaml', module: 'opensaml-core'
}
implementation "software.amazon.awssdk:s3:${rootProject.ext.awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${rootProject.ext.awsSdkVersion}"
implementation "software.amazon.awssdk:s3:${awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}"
// @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the
// root) so policy.source repositories can be exercised against embedded H2.
@@ -86,10 +86,10 @@ dependencies {
// Testcontainers: real MinIO/LocalStack (S3) and Valkey for integration tests in CI without
// manually-started instances. Tests skip cleanly when Docker is unavailable.
testImplementation "org.testcontainers:testcontainers:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${rootProject.ext.testcontainersMinioVersion}"
testImplementation "org.testcontainers:testcontainers:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:minio:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:localstack:${testcontainersMinioVersion}"
testImplementation "org.testcontainers:junit-jupiter:${testcontainersMinioVersion}"
}
tasks.register('prepareKotlinBuildScriptModel') {}
@@ -24,7 +24,7 @@ import stirling.software.common.model.ApplicationProperties;
* the full production bean method {@code valkeyConnectionFactory()} so the parse, credential
* wiring, and eager-handshake all run exactly as at boot.
*/
@Testcontainers(disabledWithoutDocker = true)
@Testcontainers
@EnabledIf("isDockerAvailable")
class LiveValkeyAuthIntegrationTest {
@@ -28,7 +28,7 @@ import stirling.software.common.model.ApplicationProperties;
* to reproduce a partition rather than {@code stop} (which would fail fast with
* connection-refused).
*/
@Testcontainers(disabledWithoutDocker = true)
@Testcontainers
@EnabledIf("isDockerAvailable")
class LiveValkeyChaosTest {
@@ -39,7 +39,7 @@ import stirling.software.common.model.ApplicationProperties;
* unavailable - without that guard, {@code @Testcontainers} would throw {@code initializationError}
* (test FAILURE, not skip) on CI runners without Docker.
*/
@Testcontainers(disabledWithoutDocker = true)
@Testcontainers
@EnabledIf("isDockerAvailable")
class LiveValkeyIntegrationTest {
+3 -6
View File
@@ -226,7 +226,6 @@ subprojects {
resolutionStrategy.force "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcutil-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.apache.santuario:xmlsec:3.0.5"
}
dependencyManagement {
@@ -277,7 +276,6 @@ subprojects {
def jacocoReport = tasks.named("jacocoTestReport")
tasks.withType(Test).configureEach {
jvmArgs "--enable-native-access=ALL-UNNAMED"
useJUnitPlatform()
finalizedBy(jacocoReport)
}
@@ -290,8 +288,7 @@ subprojects {
html.required.set(true)
}
doLast {
def reportBaseDir = layout.buildDirectory.dir("reports/jacoco/test").get().asFile
def xmlReport = new File(reportBaseDir, "jacocoTestReport.xml")
def xmlReport = reports.xml.outputLocation.get().asFile
if (!xmlReport.exists()) {
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
return
@@ -370,7 +367,7 @@ subprojects {
}
logger.lifecycle(separator)
def htmlReport = new File(reportBaseDir, "html")
def htmlReport = reports.html.outputLocation.get().asFile
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
if (rows.any { it[3] == "FAIL" }) {
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
@@ -660,7 +657,7 @@ tasks.register('compileRestartHelper', JavaCompile) {
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
classpath = files()
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
def restartMajorVersion = rootProject.ext.modernJavaVersion
def restartMajorVersion = project.ext.modernJavaVersion
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
sourceCompatibility = restartCompatibility
targetCompatibility = restartCompatibility
@@ -0,0 +1,39 @@
/**
* Prefixes the browser tab title with the current worktree name during local
* development so multiple concurrently-running worktrees (e.g. wt1, wt2, spdf1)
* are distinguishable at a glance instead of all showing "Stirling PDF".
*
* The label is injected as a build-time constant by vite.config, sourced from
* the top-level dev tasks. It is an empty string in production builds, so this
* whole feature compiles away to a no-op outside `vite` dev-serve.
*/
const LABEL =
typeof __DEV_WORKTREE_LABEL__ === "string" ? __DEV_WORKTREE_LABEL__ : "";
export function applyDevWorktreeLabel(): void {
if (!LABEL || typeof document === "undefined") {
return;
}
const prefix = `[${LABEL}] `;
const ensurePrefixed = () => {
if (!document.title.startsWith(prefix)) {
// The app rewrites document.title on route/tool changes; re-apply the
// prefix on top of whatever the app just set.
document.title = prefix + document.title;
}
};
ensurePrefixed();
const titleEl = document.querySelector("title");
if (titleEl) {
new MutationObserver(ensurePrefixed).observe(titleEl, {
childList: true,
characterData: true,
subtree: true,
});
}
}
+3
View File
@@ -14,9 +14,12 @@ import { BrowserRouter } from "react-router-dom";
import App from "@app/App";
import "@app/i18n"; // Initialize i18next
import { BASE_PATH } from "@app/constants/app";
import { applyDevWorktreeLabel } from "@app/utils/applyDevWorktreeLabel";
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
applyDevWorktreeLabel();
if (typeof window !== "undefined") {
const scheduleCompilation = () => {
if (typeof requestIdleCallback === "function") {
+7
View File
@@ -29,3 +29,10 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/**
* Dev-only worktree folder basename injected by vite.config at dev-serve time
* (empty string in production builds). Used to prefix the browser tab title so
* concurrent worktrees are distinguishable.
*/
declare const __DEV_WORKTREE_LABEL__: string;
+10 -1
View File
@@ -182,7 +182,13 @@ const TSCONFIG_MAP: Record<BuildMode, string> = {
prototypes: "./tsconfig.prototypes.vite.json",
};
export default defineConfig(async ({ mode }) => {
export default defineConfig(async ({ mode, command }) => {
// Dev-only browser-tab label (worktree folder basename) surfaced by the
// top-level dev tasks so concurrent worktrees have distinguishable tabs.
// Only injected during `vite` (dev serve) — never baked into a production
// build — and carries only the folder name, no path/host/user info.
const devWorktreeLabel =
command === "serve" ? (process.env.STIRLING_DEV_LABEL ?? "") : "";
// Load env files relative to this config (frontend/editor/), regardless of
// where the build was invoked from. The previous `process.cwd()` worked when
// this file lived at frontend/, but after the editor was moved under
@@ -250,6 +256,9 @@ export default defineConfig(async ({ mode }) => {
};
return {
define: {
__DEV_WORKTREE_LABEL__: JSON.stringify(devWorktreeLabel),
},
plugins: [
react(),
...(runSubpath ? [subpathBareRedirectPlugin(runSubpath)] : []),
-9
View File
@@ -1,9 +0,0 @@
$ErrorActionPreference = 'Stop'
$output = & "$PSScriptRoot/../gradlew.bat" printVersion --quiet 2>&1
$lines = @($output | Where-Object { $_.ToString().Trim() })
if ($lines.Count -eq 0) {
exit 0
}
Write-Output $lines[-1]