feat(desktop): support remote lite build mode without bundled JRE/JAR (#417)

* feat(desktop): support remote lite build mode without bundled JRE/JAR

Add a dual packaging mode system controlled by the BUILD_MODE env var:

- **local** (default): Full build bundling JRE + Spring Boot JAR, identical
  to the previous behavior.  Supports both embedded local backend and
  remote server connection.

- **remote** (lite): Omits the ~530 MB JRE/JAR resources, producing an
  installer that is ~81% smaller (97 MB vs 523 MB on macOS arm64).  The
  app only supports connecting to a remote server; the "local" option is
  hidden from the splash connection chooser.

Changes:
- Replace static electron-builder.json with dynamic electron-builder.cjs
  that conditionally includes extraResources based on BUILD_MODE
- Add build mode detection at runtime (checks JAR existence) with graceful
  fallback to remote-only mode
- Add IPC handler app:get-build-mode and expose via preload
- Hide "本地运行" option in splash when running a remote build
- Ignore stale 'local' saved config in remote builds
- Add package scripts: package:mac:local, package:mac:remote, etc.
- Add missing build scripts: build.sh, download-jre.sh, build-all-platforms.sh
- Add no-op afterPack hook (trim-playwright-driver.cjs) placeholder
- Add cross-env devDependency for cross-platform BUILD_MODE support

* feat(desktop): add white-label branding system for build-time rebranding

Add a Vite plugin (scripts/branding.cjs) that replaces hardcoded "MateClaw"
strings at build time, enabling white-label/OEM rebranding without modifying
any source code.

Configuration:
- Edit branding.config.json (name, tagline, team, copyright, appId, githubUrl)
- Or set BRAND_* env vars (BRAND_NAME, BRAND_TAGLINE, BRAND_TEAM, etc.)

Usage:
  # Default build (MateClaw brand)
  npm run package:mac

  # Custom brand via env vars
  BRAND_NAME=MyAI BRAND_TAGLINE="Smart AI Helper" npm run package:mac:remote

  # Or edit branding.config.json and build normally
  npm run package:mac:remote

Replacements applied at build time:
- Brand name (window title, About dialog, error messages, console logs)
- Tagline, team name, copyright line
- GitHub repo/issues URLs
- Logo file path
- electron-builder config (productName, appId, artifactName, dmg title, publish repo)

The branding plugin runs in Vite's transform hook, covering the renderer
(App.vue, index.html), electron main process, and preload script.

Server-coupled strings (H2 database name, Spring Boot property names) are
intentionally NOT replaced to avoid breaking backend compatibility.

---------

Co-authored-by: qiaozhipeng <qiaozhipeng@daojia-inc.com>
This commit is contained in:
Joe0720 2026-06-26 09:29:24 +08:00 committed by GitHub
parent d7d409245b
commit e79fb00fec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 676 additions and 119 deletions

View File

@ -0,0 +1,9 @@
{
"name": "MateClaw",
"tagline": "AI Personal Assistant",
"team": "MateClaw Team",
"copyright": "Copyright © 2026 MateClaw Team",
"appId": "vip.mate.mateclaw",
"githubUrl": "https://github.com/matevip/mateclaw",
"logoFile": "mateclaw_logo_s.png"
}

View File

@ -0,0 +1,139 @@
/**
* electron-builder.cjs Dynamic build configuration.
*
* Two packaging modes are controlled by the BUILD_MODE environment variable:
*
* BUILD_MODE=local (default) Full build: bundles the embedded JRE and
* Spring Boot JAR so the desktop app can run a
* local backend. Original behavior.
*
* BUILD_MODE=remote Lightweight build: omits the JRE/JAR
* resources (~530 MB smaller on macOS). The app
* can only connect to a remote server the
* "local" connection option is hidden in the
* splash UI.
*
* Branding is controlled by branding.config.json or BRAND_* env vars.
* See scripts/branding.cjs for details.
*
* Usage:
* BUILD_MODE=remote npx electron-builder --mac
* npm run package:mac:remote
* BRAND_NAME=MyAI npm run package:mac:remote
*/
'use strict'
const { loadBrandConfig } = require('./scripts/branding.cjs')
const mode = process.env.BUILD_MODE === 'remote' ? 'remote' : 'local'
const brand = loadBrandConfig(__dirname)
// Derive a short slug from the brand name for artifact file names.
// "MyAI" → "MyAI", "Cool App" → "Cool_App"
const brandSlug = brand.name.replace(/\s+/g, '_')
// Parse GitHub URL for publish config (owner/repo)
let githubOwner = 'matevip'
let githubRepo = 'mateclaw'
const ghMatch = brand.githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/)
if (ghMatch) {
githubOwner = ghMatch[1]
githubRepo = ghMatch[2]
}
/** @type {import('electron-builder').Configuration} */
const config = {
appId: brand.appId,
productName: brand.name,
copyright: brand.copyright,
directories: { output: 'release' },
publish: [
{
provider: 'github',
owner: githubOwner,
repo: githubRepo,
},
],
files: ['dist-electron', 'dist'],
afterPack: 'scripts/trim-playwright-driver.cjs',
// extraResources: only bundle JRE + JAR in local mode.
// In remote mode this array is empty — the packaged app contains only the
// Electron + Vue shell, cutting ~530 MB from the installer.
extraResources:
mode === 'local'
? [
{
from: 'resources/jre/${os}-${arch}/',
to: 'jre/',
filter: ['**/*'],
},
{
from: 'resources/app.jar',
to: 'app.jar',
},
]
: [],
mac: {
category: 'public.app-category.productivity',
target: [
{ target: 'dmg', arch: ['arm64', 'x64'] },
{ target: 'zip', arch: ['arm64', 'x64'] },
],
icon: 'build/icon.icns',
hardenedRuntime: true,
gatekeeperAssess: false,
entitlements: 'build/entitlements.mac.plist',
entitlementsInherit: 'build/entitlements.mac.inherit.plist',
// Differentiate installers so users can tell local vs remote builds apart.
artifactName:
mode === 'remote'
? `${brandSlug}_Remote_${'$'}{version}_${'$'}{arch}.${'$'}{ext}`
: `${brandSlug}_${'$'}{version}_${'$'}{arch}.${'$'}{ext}`,
},
dmg: {
contents: [
{ x: 130, y: 220 },
{ x: 410, y: 220, type: 'link', path: '/Applications' },
],
title: `${brand.name} ${'$'}{version}`,
},
win: {
target: [
{ target: 'nsis', arch: 'x64' },
{ target: 'nsis', arch: 'arm64' },
],
icon: 'build/icon.ico',
artifactName:
mode === 'remote'
? `${brandSlug}_Remote_${'$'}{version}_${'$'}{arch}_Setup.${'$'}{ext}`
: `${brandSlug}_${'$'}{version}_${'$'}{arch}_Setup.${'$'}{ext}`,
},
nsis: {
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
deleteAppDataOnUninstall: false,
installerIcon: 'build/icon.ico',
uninstallerIcon: 'build/icon.ico',
installerHeaderIcon: 'build/icon.ico',
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
linux: {
target: ['AppImage'],
icon: 'build/icon.png',
category: 'Utility',
artifactName:
mode === 'remote'
? `${brandSlug}_Remote_${'$'}{version}.${'$'}{ext}`
: `${brandSlug}_${'$'}{version}.${'$'}{ext}`,
},
}
module.exports = config

View File

@ -1,96 +0,0 @@
{
"appId": "vip.mate.mateclaw",
"productName": "MateClaw",
"copyright": "Copyright © 2026 MateClaw Team",
"directories": {
"output": "release"
},
"publish": [
{
"provider": "github",
"owner": "matevip",
"repo": "mateclaw"
}
],
"files": [
"dist-electron",
"dist"
],
"afterPack": "scripts/trim-playwright-driver.cjs",
"extraResources": [
{
"from": "resources/jre/${os}-${arch}/",
"to": "jre/",
"filter": ["**/*"]
},
{
"from": "resources/app.jar",
"to": "app.jar"
}
],
"mac": {
"category": "public.app-category.productivity",
"target": [
{
"target": "dmg",
"arch": ["arm64", "x64"]
},
{
"target": "zip",
"arch": ["arm64", "x64"]
}
],
"icon": "build/icon.icns",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
"artifactName": "MateClaw_${version}_${arch}.${ext}"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
],
"title": "MateClaw ${version}"
},
"win": {
"target": [
{
"target": "nsis",
"arch": "x64"
},
{
"target": "nsis",
"arch": "arm64"
}
],
"icon": "build/icon.ico",
"artifactName": "MateClaw_${version}_${arch}_Setup.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": false,
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"linux": {
"target": ["AppImage"],
"icon": "build/icon.png",
"category": "Utility",
"artifactName": "MateClaw_${version}.${ext}"
}
}

View File

@ -47,6 +47,31 @@ const trustedCertHosts = new Set<string>()
// handler, which prompts the user before loading the page. // handler, which prompts the user before loading the page.
const insecureAgent = new https.Agent({ rejectUnauthorized: false }) const insecureAgent = new https.Agent({ rejectUnauthorized: false })
// ─── Build Mode Detection ────────────────────────────────────────────────────
/**
* The desktop app ships in two variants:
*
* "local" bundles the JRE and Spring Boot JAR in extraResources so the
* app can run an embedded backend. This is the traditional full
* build.
*
* "remote" omits the JRE/JAR (~530 MB lighter). The app can only connect
* to a remote server. The "local" option is hidden in the splash
* connection chooser.
*
* Detection is done at runtime by checking whether the JAR exists in the
* resources directory. This avoids any build-time code injection the same
* main-process code runs in both builds; the only difference is whether the
* JAR/JRE files are present.
*/
function detectBuildMode(): 'local' | 'remote' {
const jarPath = getJarPath()
return existsSync(jarPath) ? 'local' : 'remote'
}
const BUILD_MODE = detectBuildMode()
interface UpdaterState { interface UpdaterState {
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error' status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'
version?: string version?: string
@ -131,6 +156,15 @@ function getAvailablePort(): Promise<number> {
} }
async function startJavaBackend(): Promise<void> { async function startJavaBackend(): Promise<void> {
// Remote builds have no bundled JRE/JAR — refuse to start the local backend
// and guide the user toward the connection chooser instead of showing a
// generic "file not found" error.
if (BUILD_MODE === 'remote') {
console.log('[MateClaw] Remote build — local backend unavailable, showing connection chooser')
sendToWindow('backend:status', 'choose')
return
}
BACKEND_PORT = await getAvailablePort() BACKEND_PORT = await getAvailablePort()
BACKEND_URL = `http://localhost:${BACKEND_PORT}` BACKEND_URL = `http://localhost:${BACKEND_PORT}`
console.log(`[MateClaw] Using dynamic port: ${BACKEND_PORT}`) console.log(`[MateClaw] Using dynamic port: ${BACKEND_PORT}`)
@ -308,6 +342,16 @@ async function bootConnection(): Promise<void> {
} }
const cfg = loadConfig() const cfg = loadConfig()
// Remote builds cannot start a local backend. If the user previously
// saved 'local' mode (e.g. they upgraded from a full build), ignore the
// stale preference and fall through to the connection chooser.
if (BUILD_MODE === 'remote' && cfg.mode === 'local') {
connectionMode = null
sendToWindow('backend:status', 'choose')
return
}
if (cfg.mode === 'local') { if (cfg.mode === 'local') {
connectionMode = 'local' connectionMode = 'local'
await startJavaBackend() await startJavaBackend()
@ -544,6 +588,10 @@ function registerIpcHandlers(): void {
ipcMain.handle('app:get-version', () => app.getVersion()) ipcMain.handle('app:get-version', () => app.getVersion())
// Let the renderer know whether this is a local (full) or remote (lite) build
// so the splash can hide the "local" connection option in remote builds.
ipcMain.handle('app:get-build-mode', () => BUILD_MODE)
ipcMain.handle('app:get-backend-url', () => BACKEND_URL) ipcMain.handle('app:get-backend-url', () => BACKEND_URL)
ipcMain.handle('app:is-backend-ready', () => backendReady) ipcMain.handle('app:is-backend-ready', () => backendReady)
@ -576,6 +624,9 @@ function registerIpcHandlers(): void {
servers: cfg.servers, servers: cfg.servers,
// The renderer shows the chooser on first run or when "Switch Server" forced it. // The renderer shows the chooser on first run or when "Switch Server" forced it.
forceChoose: forceChooser, forceChoose: forceChooser,
// Tell the renderer which build variant is running so it can hide the
// "local" option in remote (lite) builds.
buildMode: BUILD_MODE,
} }
}) })
@ -584,6 +635,11 @@ function registerIpcHandlers(): void {
}) })
ipcMain.handle('connection:use-local', async () => { ipcMain.handle('connection:use-local', async () => {
// Remote builds have no bundled JRE/JAR — reject the local mode request.
if (BUILD_MODE === 'remote') {
sendToWindow('backend:crashed', '此版本为轻量版Remote不支持本地内嵌后端。请选择连接远程服务器。')
return
}
forceChooser = false forceChooser = false
saveConfig({ mode: 'local' }) saveConfig({ mode: 'local' })
connectionMode = 'local' connectionMode = 'local'

View File

@ -5,6 +5,7 @@ contextBridge.exposeInMainWorld('mateClawAPI', {
// Platform info // Platform info
getPlatform: () => ipcRenderer.invoke('app:get-platform'), getPlatform: () => ipcRenderer.invoke('app:get-platform'),
getVersion: () => ipcRenderer.invoke('app:get-version'), getVersion: () => ipcRenderer.invoke('app:get-version'),
getBuildMode: () => ipcRenderer.invoke('app:get-build-mode'),
getBackendUrl: () => ipcRenderer.invoke('app:get-backend-url'), getBackendUrl: () => ipcRenderer.invoke('app:get-backend-url'),
isBackendReady: () => ipcRenderer.invoke('app:is-backend-ready'), isBackendReady: () => ipcRenderer.invoke('app:is-backend-ready'),
getUserDataPath: () => ipcRenderer.invoke('app:get-user-data-path'), getUserDataPath: () => ipcRenderer.invoke('app:get-user-data-path'),

View File

@ -14,8 +14,14 @@
"setup": "npm run setup:jar && npm run setup:jre", "setup": "npm run setup:jar && npm run setup:jre",
"setup:all-platforms": "bash scripts/build-all-platforms.sh --all", "setup:all-platforms": "bash scripts/build-all-platforms.sh --all",
"package:mac": "npm run build && electron-builder --mac", "package:mac": "npm run build && electron-builder --mac",
"package:mac:local": "npm run build && cross-env BUILD_MODE=local electron-builder --mac",
"package:mac:remote": "npm run build && cross-env BUILD_MODE=remote electron-builder --mac",
"package:win": "npm run build && electron-builder --win", "package:win": "npm run build && electron-builder --win",
"package:win:local": "npm run build && cross-env BUILD_MODE=local electron-builder --win",
"package:win:remote": "npm run build && cross-env BUILD_MODE=remote electron-builder --win",
"package:all": "bash scripts/build-all-platforms.sh --all", "package:all": "bash scripts/build-all-platforms.sh --all",
"package:all:local": "bash scripts/build-all-platforms.sh --local",
"package:all:remote": "bash scripts/build-all-platforms.sh --remote",
"publish:github": "bash scripts/publish-github.sh", "publish:github": "bash scripts/publish-github.sh",
"publish:github:draft": "bash scripts/publish-github.sh --draft" "publish:github:draft": "bash scripts/publish-github.sh --draft"
}, },
@ -25,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"cross-env": "^10.1.0",
"electron": "^33.3.1", "electron": "^33.3.1",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"typescript": "^5.7.3", "typescript": "^5.7.3",
@ -32,5 +39,11 @@
"vite-plugin-electron": "^0.28.8", "vite-plugin-electron": "^0.28.8",
"vite-plugin-electron-renderer": "^0.14.6", "vite-plugin-electron-renderer": "^0.14.6",
"vue-tsc": "^2.2.0" "vue-tsc": "^2.2.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
} }
} }

View File

@ -8,13 +8,19 @@ importers:
.: .:
dependencies: dependencies:
electron-updater:
specifier: ^6.3.9
version: 6.8.9
vue: vue:
specifier: ^3.5.13 specifier: ^3.5.13
version: 3.5.31(typescript@5.9.3) version: 3.5.31(typescript@5.9.3)
devDependencies: devDependencies:
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.4(vite@6.4.1(@types/node@25.5.0))(vue@3.5.31(typescript@5.9.3)) version: 5.2.4(vite@6.4.3(@types/node@25.5.0))(vue@3.5.31(typescript@5.9.3))
cross-env:
specifier: ^10.1.0
version: 10.1.0
electron: electron:
specifier: ^33.3.1 specifier: ^33.3.1
version: 33.4.11 version: 33.4.11
@ -26,13 +32,13 @@ importers:
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^6.0.7 specifier: ^6.0.7
version: 6.4.1(@types/node@25.5.0) version: 6.4.3(@types/node@25.5.0)
vite-plugin-electron: vite-plugin-electron:
specifier: ^0.28.8 specifier: ^0.28.8
version: 0.28.8(vite-plugin-electron-renderer@0.14.6) version: 0.28.8(vite-plugin-electron-renderer@0.14.7)
vite-plugin-electron-renderer: vite-plugin-electron-renderer:
specifier: ^0.14.6 specifier: ^0.14.6
version: 0.14.6 version: 0.14.7
vue-tsc: vue-tsc:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.12(typescript@5.9.3) version: 2.2.12(typescript@5.9.3)
@ -90,6 +96,9 @@ packages:
resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==}
engines: {node: '>=16.4'} engines: {node: '>=16.4'}
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -673,6 +682,10 @@ packages:
resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==} resolution: {integrity: sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
builder-util-runtime@9.7.0:
resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==}
engines: {node: '>=12.0.0'}
builder-util@25.1.7: builder-util@25.1.7:
resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==} resolution: {integrity: sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==}
@ -788,6 +801,11 @@ packages:
crc@3.8.0: crc@3.8.0:
resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==}
cross-env@10.1.0:
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
engines: {node: '>=20'}
hasBin: true
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -883,6 +901,9 @@ packages:
electron-publish@25.1.7: electron-publish@25.1.7:
resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==} resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==}
electron-updater@6.8.9:
resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==}
electron@33.4.11: electron@33.4.11:
resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==}
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
@ -1046,6 +1067,10 @@ packages:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true hasBin: true
glob@7.2.0:
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Glob versions prior to v9 are no longer supported
@ -1205,8 +1230,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
js-yaml@4.1.1: js-yaml@4.2.0:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
hasBin: true hasBin: true
json-buffer@3.0.1: json-buffer@3.0.1:
@ -1245,9 +1270,16 @@ packages:
lodash.difference@4.5.0: lodash.difference@4.5.0:
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
lodash.flatten@4.4.0: lodash.flatten@4.4.0:
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.isplainobject@4.0.6: lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
@ -1703,6 +1735,9 @@ packages:
temp-file@3.4.0: temp-file@3.4.0:
resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==}
tiny-typed-emitter@2.1.0:
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
tinyglobby@0.2.15: tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -1761,8 +1796,8 @@ packages:
resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
engines: {node: '>=0.6.0'} engines: {node: '>=0.6.0'}
vite-plugin-electron-renderer@0.14.6: vite-plugin-electron-renderer@0.14.7:
resolution: {integrity: sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==} resolution: {integrity: sha512-hHBMKuZ24MB2SIxG7U7ix+DDEnvxou7Bgy/TdhYxNz3S5N3Yh7Hjvj9blfMeGEJ0oaZJn7y5z0V/RyDmJ5OuCA==}
vite-plugin-electron@0.28.8: vite-plugin-electron@0.28.8:
resolution: {integrity: sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==} resolution: {integrity: sha512-ir+B21oSGK9j23OEvt4EXyco9xDCaF6OGFe0V/8Zc0yL2+HMyQ6mmNQEIhXsEsZCSfIowBpwQBeHH4wVsfraeg==}
@ -1772,8 +1807,8 @@ packages:
vite-plugin-electron-renderer: vite-plugin-electron-renderer:
optional: true optional: true
vite@6.4.1: vite@6.4.3:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -1974,6 +2009,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@epic-web/invariant@1.0.0': {}
'@esbuild/aix-ppc64@0.25.12': '@esbuild/aix-ppc64@0.25.12':
optional: true optional: true
@ -2225,9 +2262,9 @@ snapshots:
'@types/node': 20.19.37 '@types/node': 20.19.37
optional: true optional: true
'@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@25.5.0))(vue@3.5.31(typescript@5.9.3))': '@vitejs/plugin-vue@5.2.4(vite@6.4.3(@types/node@25.5.0))(vue@3.5.31(typescript@5.9.3))':
dependencies: dependencies:
vite: 6.4.1(@types/node@25.5.0) vite: 6.4.3(@types/node@25.5.0)
vue: 3.5.31(typescript@5.9.3) vue: 3.5.31(typescript@5.9.3)
'@volar/language-core@2.4.15': '@volar/language-core@2.4.15':
@ -2387,7 +2424,7 @@ snapshots:
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
is-ci: 3.0.1 is-ci: 3.0.1
isbinaryfile: 5.0.7 isbinaryfile: 5.0.7
js-yaml: 4.1.1 js-yaml: 4.2.0
json5: 2.2.3 json5: 2.2.3
lazy-val: 1.0.5 lazy-val: 1.0.5
minimatch: 10.2.5 minimatch: 10.2.5
@ -2404,7 +2441,7 @@ snapshots:
archiver-utils@2.1.0: archiver-utils@2.1.0:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.0
graceful-fs: 4.2.11 graceful-fs: 4.2.11
lazystream: 1.0.1 lazystream: 1.0.1
lodash.defaults: 4.2.0 lodash.defaults: 4.2.0
@ -2509,6 +2546,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
builder-util-runtime@9.7.0:
dependencies:
debug: 4.4.3
sax: 1.6.0
transitivePeerDependencies:
- supports-color
builder-util@25.1.7: builder-util@25.1.7:
dependencies: dependencies:
7zip-bin: 5.2.0 7zip-bin: 5.2.0
@ -2523,7 +2567,7 @@ snapshots:
http-proxy-agent: 7.0.2 http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6 https-proxy-agent: 7.0.6
is-ci: 3.0.1 is-ci: 3.0.1
js-yaml: 4.1.1 js-yaml: 4.2.0
source-map-support: 0.5.21 source-map-support: 0.5.21
stat-mode: 1.0.0 stat-mode: 1.0.0
temp-file: 3.4.0 temp-file: 3.4.0
@ -2656,6 +2700,11 @@ snapshots:
buffer: 5.7.1 buffer: 5.7.1
optional: true optional: true
cross-env@10.1.0:
dependencies:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@ -2715,7 +2764,7 @@ snapshots:
builder-util-runtime: 9.2.10 builder-util-runtime: 9.2.10
fs-extra: 10.1.0 fs-extra: 10.1.0
iconv-lite: 0.6.3 iconv-lite: 0.6.3
js-yaml: 4.1.1 js-yaml: 4.2.0
optionalDependencies: optionalDependencies:
dmg-license: 1.0.11 dmg-license: 1.0.11
transitivePeerDependencies: transitivePeerDependencies:
@ -2793,6 +2842,19 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron-updater@6.8.9:
dependencies:
builder-util-runtime: 9.7.0
fs-extra: 10.1.0
js-yaml: 4.2.0
lazy-val: 1.0.5
lodash.escaperegexp: 4.1.2
lodash.isequal: 4.5.0
semver: 7.7.4
tiny-typed-emitter: 2.1.0
transitivePeerDependencies:
- supports-color
electron@33.4.11: electron@33.4.11:
dependencies: dependencies:
'@electron/get': 2.0.3 '@electron/get': 2.0.3
@ -3000,6 +3062,15 @@ snapshots:
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 1.11.1 path-scurry: 1.11.1
glob@7.2.0:
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.5
once: 1.4.0
path-is-absolute: 1.0.1
glob@7.2.3: glob@7.2.3:
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
@ -3175,7 +3246,7 @@ snapshots:
filelist: 1.0.6 filelist: 1.0.6
picocolors: 1.1.1 picocolors: 1.1.1
js-yaml@4.1.1: js-yaml@4.2.0:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
@ -3212,8 +3283,12 @@ snapshots:
lodash.difference@4.5.0: {} lodash.difference@4.5.0: {}
lodash.escaperegexp@4.1.2: {}
lodash.flatten@4.4.0: {} lodash.flatten@4.4.0: {}
lodash.isequal@4.5.0: {}
lodash.isplainobject@4.0.6: {} lodash.isplainobject@4.0.6: {}
lodash.union@4.6.0: {} lodash.union@4.6.0: {}
@ -3706,6 +3781,8 @@ snapshots:
async-exit-hook: 2.0.1 async-exit-hook: 2.0.1
fs-extra: 10.1.0 fs-extra: 10.1.0
tiny-typed-emitter@2.1.0: {}
tinyglobby@0.2.15: tinyglobby@0.2.15:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
@ -3757,13 +3834,13 @@ snapshots:
extsprintf: 1.4.1 extsprintf: 1.4.1
optional: true optional: true
vite-plugin-electron-renderer@0.14.6: {} vite-plugin-electron-renderer@0.14.7: {}
vite-plugin-electron@0.28.8(vite-plugin-electron-renderer@0.14.6): vite-plugin-electron@0.28.8(vite-plugin-electron-renderer@0.14.7):
optionalDependencies: optionalDependencies:
vite-plugin-electron-renderer: 0.14.6 vite-plugin-electron-renderer: 0.14.7
vite@6.4.1(@types/node@25.5.0): vite@6.4.3(@types/node@25.5.0):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)

View File

@ -0,0 +1,153 @@
/**
* scripts/branding.cjs Vite plugin for build-time white-label branding.
*
* Reads brand settings from branding.config.json (or BRAND_* env overrides)
* and replaces hardcoded "MateClaw" strings in all built files source code
* stays untouched.
*
* Supported env overrides:
* BRAND_NAME, BRAND_TAGLINE, BRAND_TEAM, BRAND_COPYRIGHT,
* BRAND_APP_ID, BRAND_GITHUB_URL, BRAND_LOGO_FILE
*/
'use strict'
const fs = require('fs')
const path = require('path')
function loadBrandConfig(rootDir) {
const configPath = path.join(rootDir, 'branding.config.json')
let config = {}
if (fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
}
// Env vars override the config file.
const env = process.env
return {
name: env.BRAND_NAME || config.name || 'MateClaw',
tagline: env.BRAND_TAGLINE || config.tagline || 'AI Personal Assistant',
team: env.BRAND_TEAM || config.team || 'MateClaw Team',
copyright: env.BRAND_COPYRIGHT || config.copyright || 'Copyright © 2026 MateClaw Team',
appId: env.BRAND_APP_ID || config.appId || 'vip.mate.mateclaw',
githubUrl: env.BRAND_GITHUB_URL || config.githubUrl || 'https://github.com/matevip/mateclaw',
logoFile: env.BRAND_LOGO_FILE || config.logoFile || 'mateclaw_logo_s.png',
}
}
/**
* Build the string-replacement table.
*
* Order matters: longer/more-specific patterns are replaced first to avoid
* partial matches (e.g. "MateClaw Team" before "MateClaw").
*/
function buildReplacements(brand) {
const replacements = []
// 1. Copyright line (most specific)
replacements.push([
'Copyright © 2026 MateClaw Team',
brand.copyright,
])
// 2. Team name
replacements.push(['MateClaw Team', brand.team])
// 3. GitHub URLs
replacements.push([
'https://github.com/matevip/mateclaw/issues',
brand.githubUrl + '/issues',
])
replacements.push([
'https://github.com/matevip/mateclaw',
brand.githubUrl,
])
// 4. Logo file path
replacements.push([
'mateclaw_logo_s.png',
brand.logoFile,
])
// 5. Tagline
replacements.push([
'AI Personal Assistant',
brand.tagline,
])
// 6. Split-span brand name in App.vue template:
// <span class="mate">Mate</span><span class="claw">Claw</span>
// Replace the inner text so styling classes are preserved but the text
// changes. We split the brand name: first half gets "mate" class, second
// half gets "claw" class. If it's a single word, it all goes in "mate".
var half = Math.ceil(brand.name.length / 2)
var firstPart = brand.name.slice(0, half)
var secondPart = brand.name.slice(half)
replacements.push([
'>Mate</span><span class="claw">Claw<',
'>' + firstPart + '</span><span class="claw">' + secondPart + '<',
])
// 7. Brand name (catch-all, must come last)
// Only replace the exact word "MateClaw", not "mateclaw" (lowercase,
// which is used in H2 database paths and Spring Boot properties that
// are coupled with the server and must NOT change).
replacements.push(['MateClaw', brand.name])
return replacements
}
function applyReplacements(code, replacements) {
var result = code
for (var i = 0; i < replacements.length; i++) {
var from = replacements[i][0]
var to = replacements[i][1]
// Use split/join for reliable literal string replacement (no regex
// escaping issues).
result = result.split(from).join(to)
}
return result
}
/**
* Vite plugin entry point.
*
* Usage in vite.config.ts:
* import { brandingPlugin } from './scripts/branding.cjs'
* plugins: [brandingPlugin()]
*/
function brandingPlugin(options) {
options = options || {}
var rootDir = options.rootDir || process.cwd()
var brand = loadBrandConfig(rootDir)
var replacements = buildReplacements(brand)
var isDefault =
brand.name === 'MateClaw' &&
brand.tagline === 'AI Personal Assistant' &&
brand.team === 'MateClaw Team'
if (!isDefault) {
console.log('[branding] White-label build: "' + brand.name + '" (tagline: "' + brand.tagline + '")')
}
return {
name: 'mateclaw-branding',
enforce: 'pre',
// Transform JS/TS/Vue source before compilation
transform: function (code, id) {
if (id.indexOf('node_modules') !== -1) return null
// Only process source files that might contain brand strings.
if (!/\.(ts|js|vue|html|css|cjs|mjs)$/.test(id)) return null
var result = applyReplacements(code, replacements)
return result !== code ? { code: result, map: null } : null
},
// Transform index.html
transformIndexHtml: function (html) {
return applyReplacements(html, replacements)
},
}
}
module.exports = { brandingPlugin: brandingPlugin, loadBrandConfig: loadBrandConfig, buildReplacements: buildReplacements }

View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# scripts/build-all-platforms.sh — Build MateClaw desktop packages for all
# platforms (macOS + Windows) in the specified build mode.
#
# Usage:
# scripts/build-all-platforms.sh --all # local mode (default), both platforms
# scripts/build-all-platforms.sh --local # local mode, both platforms
# scripts/build-all-platforms.sh --remote # remote mode, both platforms
#
set -euo pipefail
MODE="--all"
case "${1:-}" in
--all) MODE="local" ;;
--local) MODE="local" ;;
--remote) MODE="remote" ;;
*) echo "Usage: $0 [--all|--local|--remote]"; exit 1 ;;
esac
echo "==> Building all platforms, BUILD_MODE=$MODE"
if [ "$MODE" = "remote" ]; then
echo "==> macOS (remote/lite)"
BUILD_MODE=remote npx electron-builder --mac
echo "==> Windows (remote/lite)"
BUILD_MODE=remote npx electron-builder --win
else
echo "==> macOS (local/full)"
BUILD_MODE=local npx electron-builder --mac
echo "==> Windows (local/full)"
BUILD_MODE=local npx electron-builder --win
fi
echo "==> All builds complete."

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
#
# scripts/build.sh — Build the MateClaw Spring Boot backend JAR and place it
# at resources/app.jar so electron-builder can bundle it into the desktop app.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SERVER_DIR="$(cd "$PROJECT_ROOT/../mateclaw-server" && pwd)"
RESOURCES_DIR="$PROJECT_ROOT/resources"
echo "==> Building mateclaw-server JAR from $SERVER_DIR"
# Build the Spring Boot fat JAR (skip tests for packaging speed)
cd "$SERVER_DIR"
mvn clean package -DskipTests -Dmaven.test.skip=true -q
# Locate the built JAR
JAR_FILE=$(ls "$SERVER_DIR"/target/mateclaw-server-*.jar 2>/dev/null | head -1)
if [ -z "$JAR_FILE" ]; then
echo "ERROR: Could not find built JAR in $SERVER_DIR/target/"
exit 1
fi
echo "==> Copying $JAR_FILE$RESOURCES_DIR/app.jar"
mkdir -p "$RESOURCES_DIR"
cp "$JAR_FILE" "$RESOURCES_DIR/app.jar"
echo "==> Done. JAR size: $(du -h "$RESOURCES_DIR/app.jar" | cut -f1)"

View File

@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# scripts/download-jre.sh — Download Eclipse Temurin JRE 21 for the current
# macOS architecture (or both) and extract into resources/jre/<platform>/.
#
# Usage:
# scripts/download-jre.sh # auto-detect current arch
# scripts/download-jre.sh arm64 # arm64 only
# scripts/download-jre.sh x64 # x64 only
# scripts/download-jre.sh all # both arches
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
JRE_DIR="$PROJECT_ROOT/resources/jre"
# Temurin 21 (LTS) JRE downloads via Adoptium API
ADOPTIUM_BASE="https://api.adoptium.net/v3/binary/latest/21/ga/mac"
download_and_extract() {
local arch="$1"
local folder="$2"
local url="$ADOPTIUM_BASE/$arch/jre/hotspot/normal/eclipse?project=jdk"
local tmpfile="$JRE_DIR/jre-mac-$arch.tar.gz"
echo "==> Downloading Temurin 21 JRE for macOS $arch"
mkdir -p "$JRE_DIR"
curl -L --fail -o "$tmpfile" "$url"
echo "==> Extracting to $JRE_DIR/$folder"
rm -rf "$JRE_DIR/$folder"
mkdir -p "$JRE_DIR/$folder"
# Temurin macOS tar.gz extracts to: jdk-21.x.x+jre/Contents/Home/...
# We want $folder/Contents/Home/... so move the inner Contents up.
local extract_tmp="$JRE_DIR/.tmp-$arch"
rm -rf "$extract_tmp"
mkdir -p "$extract_tmp"
tar -xzf "$tmpfile" -C "$extract_tmp"
# Find the extracted top-level directory and move its Contents
local extracted_dir
extracted_dir=$(find "$extract_tmp" -maxdepth 1 -type d -name "jdk-*" | head -1)
if [ -z "$extracted_dir" ]; then
# Fallback: some tarballs extract Contents directly
if [ -d "$extract_tmp/Contents" ]; then
mv "$extract_tmp/Contents" "$JRE_DIR/$folder/Contents"
else
echo "ERROR: Could not find extracted JDK directory"
exit 1
fi
else
mv "$extracted_dir/Contents" "$JRE_DIR/$folder/Contents"
fi
rm -rf "$extract_tmp" "$tmpfile"
# Verify java binary exists
local java_bin="$JRE_DIR/$folder/Contents/Home/bin/java"
if [ -f "$java_bin" ]; then
echo "==> OK: $java_bin"
else
echo "ERROR: java binary not found at $java_bin"
exit 1
fi
}
TARGET="${1:-auto}"
if [ "$TARGET" = "auto" ]; then
case "$(uname -m)" in
arm64) TARGET="arm64" ;;
x86_64) TARGET="x64" ;;
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;;
esac
fi
case "$TARGET" in
arm64) download_and_extract "aarch64" "mac-arm64" ;;
x64) download_and_extract "x64" "mac-x64" ;;
all) download_and_extract "aarch64" "mac-arm64"
download_and_extract "x64" "mac-x64" ;;
*) echo "Usage: $0 [arm64|x64|all]"; exit 1 ;;
esac
echo "==> JRE setup complete."

View File

@ -0,0 +1,6 @@
// afterPack hook: no-op placeholder
// The original trim-playwright-driver.cjs was not included in the open-source release.
// This no-op allows electron-builder to complete packaging.
exports.default = async function () {
// intentionally empty
}

View File

@ -16,6 +16,8 @@ const remoteUrlInput = ref('')
const testing = ref(false) const testing = ref(false)
const testResult = ref<{ ok: boolean; msg: string } | null>(null) const testResult = ref<{ ok: boolean; msg: string } | null>(null)
const recentServers = ref<RemoteServer[]>([]) const recentServers = ref<RemoteServer[]>([])
// Build variant: 'local' = full (bundles JRE+JAR), 'remote' = lite (connect to remote server only)
const buildMode = ref<'local' | 'remote'>('local')
let BACKEND_URL = '' let BACKEND_URL = ''
@ -235,7 +237,21 @@ onMounted(async () => {
const cfg = await window.mateClawAPI.getConnectionConfig() const cfg = await window.mateClawAPI.getConnectionConfig()
recentServers.value = cfg.servers || [] recentServers.value = cfg.servers || []
remoteUrlInput.value = cfg.remoteUrl || '' remoteUrlInput.value = cfg.remoteUrl || ''
if (cfg.forceChoose || !cfg.mode) { buildMode.value = cfg.buildMode || 'local'
// Remote (lite) builds: skip the mode chooser, go straight to the
// remote server form the "local" option is not available.
if (buildMode.value === 'remote') {
if (cfg.forceChoose || !cfg.mode || cfg.mode === 'local') {
status.value = 'connection-select'
connView.value = 'remote-form'
} else {
connectionMode.value = cfg.mode
if (await window.mateClawAPI.isBackendReady()) {
handleBackendReady()
}
}
} else if (cfg.forceChoose || !cfg.mode) {
status.value = 'connection-select' status.value = 'connection-select'
connView.value = 'choose' connView.value = 'choose'
} else { } else {
@ -325,7 +341,11 @@ function createParticles() {
<template v-if="connView === 'choose'"> <template v-if="connView === 'choose'">
<div class="lang-title">选择连接方式 / Connection</div> <div class="lang-title">选择连接方式 / Connection</div>
<div class="lang-options"> <div class="lang-options">
<button class="lang-card" @click="chooseLocal"> <button
v-if="buildMode === 'local'"
class="lang-card"
@click="chooseLocal"
>
<span class="lang-flag">💻</span> <span class="lang-flag">💻</span>
<span class="lang-label">本地运行</span> <span class="lang-label">本地运行</span>
<span class="lang-desc">在本机内嵌运行服务</span> <span class="lang-desc">在本机内嵌运行服务</span>
@ -336,6 +356,10 @@ function createParticles() {
<span class="lang-desc">接入集中部署的服务器</span> <span class="lang-desc">接入集中部署的服务器</span>
</button> </button>
</div> </div>
<!-- Remote (lite) build notice -->
<div v-if="buildMode === 'remote'" class="remote-build-notice">
当前为轻量版客户端仅支持连接远程服务器
</div>
</template> </template>
<!-- Remote server form --> <!-- Remote server form -->
@ -900,6 +924,16 @@ function createParticles() {
font-size: 11px; font-size: 11px;
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.remote-build-notice {
font-size: 11px;
color: var(--text-tertiary);
text-align: center;
padding: 8px 12px;
border-radius: 8px;
background: var(--bg-elevated);
border: 1px solid var(--border);
width: 100%;
}
/* /*
Remote Connection Form Remote Connection Form

View File

@ -25,6 +25,7 @@ interface ConnectionConfigState {
remoteUrl: string remoteUrl: string
servers: RemoteServer[] servers: RemoteServer[]
forceChoose: boolean forceChoose: boolean
buildMode: 'local' | 'remote'
} }
interface ConnectionTestResult { interface ConnectionTestResult {
@ -36,6 +37,7 @@ interface ConnectionTestResult {
interface MateClawAPI { interface MateClawAPI {
getPlatform: () => Promise<string> getPlatform: () => Promise<string>
getVersion: () => Promise<string> getVersion: () => Promise<string>
getBuildMode: () => Promise<'local' | 'remote'>
getBackendUrl: () => Promise<string> getBackendUrl: () => Promise<string>
isBackendReady: () => Promise<boolean> isBackendReady: () => Promise<boolean>
getUserDataPath: () => Promise<string> getUserDataPath: () => Promise<string>

View File

@ -3,14 +3,24 @@ import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron' import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer' import renderer from 'vite-plugin-electron-renderer'
import { resolve } from 'path' import { resolve } from 'path'
import { brandingPlugin } from './scripts/branding.cjs'
export default defineConfig(({ command }) => { export default defineConfig(({ command }) => {
const isServe = command === 'serve' const isServe = command === 'serve'
const isBuild = command === 'build' const isBuild = command === 'build'
// Shared branding plugin instance — applied to the renderer build as well
// as the electron main/preload builds so brand strings are replaced
// everywhere without touching source code.
const brand = brandingPlugin()
return { return {
plugins: [ plugins: [
vue(), vue(),
// White-label branding: replaces "MateClaw" with the configured brand
// name at build time. Source code stays untouched. Configure via
// branding.config.json or BRAND_* env vars.
brand,
electron([ electron([
{ {
entry: 'electron/main/index.ts', entry: 'electron/main/index.ts',
@ -18,6 +28,7 @@ export default defineConfig(({ command }) => {
args.startup() args.startup()
}, },
vite: { vite: {
plugins: [brand],
build: { build: {
sourcemap: isServe, sourcemap: isServe,
minify: isBuild, minify: isBuild,
@ -34,6 +45,7 @@ export default defineConfig(({ command }) => {
args.reload() args.reload()
}, },
vite: { vite: {
plugins: [brand],
build: { build: {
sourcemap: isServe ? 'inline' : undefined, sourcemap: isServe ? 'inline' : undefined,
minify: isBuild, minify: isBuild,