diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml new file mode 100644 index 00000000..a10c1183 --- /dev/null +++ b/.github/workflows/desktop-build.yml @@ -0,0 +1,60 @@ +name: desktop-build + +on: + workflow_dispatch: + pull_request: + paths: + - 'src-tauri/**' + - 'tools/desktop/**' + - 'web-ui/**' + - 'cli.js' + - 'cli/**' + - 'lib/**' + - 'plugins/**' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/desktop-build.yml' + +permissions: + contents: read + +jobs: + tauri: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: macOS + os: macos-latest + - name: Windows + os: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install dependencies + run: npm ci + + - name: Prepare desktop config + run: npm run desktop:prepare + + - name: Build desktop app + run: npm run desktop:build + + - name: Upload desktop bundles + uses: actions/upload-artifact@v4 + with: + name: codexmate-desktop-${{ matrix.name }} + path: src-tauri/target/release/bundle/** + if-no-files-found: error diff --git a/doc/desktop.md b/doc/desktop.md new file mode 100644 index 00000000..15c840c4 --- /dev/null +++ b/doc/desktop.md @@ -0,0 +1,37 @@ +# Codex Mate Desktop (Tauri) + +Codex Mate 的桌面版使用 Tauri 作为 Windows / macOS 外壳,复用现有 Node CLI 与 Web UI 服务。 + +## 架构 + +- Tauri 负责桌面窗口、系统打包和平台安装包。 +- 现有 `cli.js run --host 127.0.0.1 --no-browser` 继续提供本地 Web UI 与 `/api`。 +- 桌面窗口加载 `http://127.0.0.1:3737`,避免重写现有 Web UI API。 +- 生产包会把 `cli.js`、`cli/`、`lib/`、`plugins/`、`web-ui/` 和 `node_modules/` 作为 Tauri resources 带入包内。 + +## 命令 + +```bash +npm run desktop:prepare +npm run desktop:dev +npm run desktop:build +``` + +## 本地要求 + +桌面构建需要: + +- Node.js 18+ +- Rust / Cargo +- Tauri 对应平台依赖 + +当前实现仍通过系统 `node` 启动打包进 resources 的 Codex Mate 后端。后续如果要做完全免 Node 安装的分发,需要把 Node runtime 或预编译 sidecar 纳入打包流程。 + +## CI + +`.github/workflows/desktop-build.yml` 会在 GitHub Actions 上构建: + +- macOS bundle / dmg +- Windows installer bundle(由当前 Tauri 平台支持的 targets 决定) + +构建产物会以 `codexmate-desktop-macOS` / `codexmate-desktop-Windows` artifact 上传。 diff --git a/package-lock.json b/package-lock.json index 5ace61d7..e03950ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codexmate", - "version": "0.0.35", + "version": "0.0.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codexmate", - "version": "0.0.35", + "version": "0.0.33", "license": "Apache-2.0", "dependencies": { "@iarna/toml": "^2.2.5", @@ -19,6 +19,7 @@ "codexmate": "cli.js" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.2", "vitepress": "^1.6.4" }, "engines": { @@ -1238,6 +1239,223 @@ "dev": true, "license": "MIT" }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", diff --git a/package.json b/package.json index d810a16c..474bdcd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexmate", - "version": "0.0.35", + "version": "0.0.33", "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具", "main": "cli.js", "bin": { @@ -42,7 +42,10 @@ "test:e2e": "node tests/e2e/run.js", "setup:git": "git remote set-url origin https://github.com/SakuraByteCore/codexmate.git && gh auth setup-git", "reset:dev": "node tools/dev/reset-and-dev.js", - "pretest": "node tools/ci/ensure-test-deps.js" + "pretest": "node tools/ci/ensure-test-deps.js", + "desktop:prepare": "node tools/desktop/prepare-tauri-resources.js", + "desktop:dev": "tauri dev", + "desktop:build": "tauri build" }, "dependencies": { "@iarna/toml": "^2.2.5", @@ -72,6 +75,7 @@ "author": "ymkiux", "license": "Apache-2.0", "devDependencies": { + "@tauri-apps/cli": "^2.11.2", "vitepress": "^1.6.4" } } diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 00000000..502406b4 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 00000000..c58a34d9 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "codexmate-desktop" +version = "0.0.33" +description = "Codex Mate desktop shell" +authors = ["ymkiux"] +license = "Apache-2.0" +repository = "https://github.com/SakuraByteCore/codexmate" +edition = "2021" +rust-version = "1.77.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.6.2" } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4" +tauri = { version = "2.11.2" } +tauri-plugin-log = "2" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 00000000..795b9b7c --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 00000000..c135d7f1 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 00000000..77e7d233 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..0f7976f1 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 00000000..98fda06f Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..f35d84ff Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..1823bb26 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..dc2b22ce Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..0ed3984c Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..60bf0ead Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..c8ca0ad1 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..8756459b Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..2c8023cc Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..2c5e6034 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..17d142c0 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 00000000..a2993adc Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 00000000..06c23c82 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 00000000..d1756ce4 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 00000000..9108fc6d --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,167 @@ +use std::{ + io::{Read, Write}, + net::{SocketAddr, TcpStream}, + path::PathBuf, + process::{Child, Command, Stdio}, + sync::Mutex, + time::{Duration, Instant}, +}; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +use tauri::{Manager, WindowEvent}; + +struct BackendState(Mutex>); + +fn health_check_ready() -> bool { + let addr: SocketAddr = match "127.0.0.1:3737".parse() { + Ok(value) => value, + Err(_) => return false, + }; + let mut stream = match TcpStream::connect_timeout(&addr, Duration::from_millis(300)) { + Ok(value) => value, + Err(_) => return false, + }; + let _ = stream.set_read_timeout(Some(Duration::from_millis(500))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(500))); + + let body = r#"{"action":"health-check","params":{}}"#; + let request = format!( + "POST /api HTTP/1.1\r\nHost: 127.0.0.1:3737\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.as_bytes().len(), + body + ); + if stream.write_all(request.as_bytes()).is_err() { + return false; + } + + let mut response = String::new(); + if stream.read_to_string(&mut response).is_err() { + return false; + } + response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200") +} + +fn wait_for_backend(timeout: Duration) -> bool { + let started = Instant::now(); + while started.elapsed() < timeout { + if health_check_ready() { + return true; + } + std::thread::sleep(Duration::from_millis(200)); + } + false +} + +#[cfg(windows)] +fn configure_backend_process(command: &mut Command) { + const CREATE_NO_WINDOW: u32 = 0x08000000; + command.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(windows))] +fn configure_backend_process(_command: &mut Command) {} + +fn find_cli_path(app: &tauri::App) -> Result> { + let mut candidates = Vec::new(); + + if let Ok(resource_dir) = app.path().resource_dir() { + candidates.push(resource_dir.join("codexmate").join("cli.js")); + candidates.push(resource_dir.join("cli.js")); + } + + if let Ok(current_dir) = std::env::current_dir() { + candidates.push(current_dir.join("cli.js")); + } + + candidates + .into_iter() + .find(|candidate| candidate.is_file()) + .ok_or_else(|| "unable to locate bundled codexmate cli.js".into()) +} + +fn spawn_backend(app: &tauri::App) -> Result, Box> { + if std::env::var("CODEXMATE_DESKTOP_SKIP_BACKEND").ok().as_deref() == Some("1") { + return Ok(None); + } + + if health_check_ready() { + return Ok(None); + } + + let cli_path = find_cli_path(app)?; + let cli_dir = cli_path + .parent() + .ok_or_else(|| "unable to resolve codexmate cli directory")?; + let node_bin = std::env::var("CODEXMATE_NODE").unwrap_or_else(|_| "node".to_string()); + + let mut command = Command::new(node_bin); + command + .arg(&cli_path) + .arg("run") + .arg("--host") + .arg("127.0.0.1") + .arg("--no-browser") + .current_dir(cli_dir) + .env("CODEXMATE_NO_BROWSER", "1") + .env("CODEXMATE_HOST", "127.0.0.1") + .env("CODEXMATE_PORT", "3737") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + configure_backend_process(&mut command); + + let child = command + .spawn() + .map_err(|err| format!("unable to start codexmate backend with Node.js: {err}"))?; + + if !wait_for_backend(Duration::from_secs(15)) { + return Err("codexmate backend did not become ready on 127.0.0.1:3737".into()); + } + + Ok(Some(child)) +} + +fn stop_backend(window: &tauri::Window) { + let state = window.state::(); + let child = { + let mut guard = match state.0.lock() { + Ok(value) => value, + Err(_) => return, + }; + guard.take() + }; + + if let Some(mut child) = child { + let _ = child.kill(); + let _ = child.wait(); + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + app.manage(BackendState(Mutex::new(None))); + } else { + let child = spawn_backend(app)?; + app.manage(BackendState(Mutex::new(child))); + } + Ok(()) + }) + .on_window_event(|window, event| { + if window.label() == "main" && matches!(event, WindowEvent::Destroyed) { + stop_backend(window); + } + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 00000000..ad5fe839 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + app_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 00000000..ee46dbc9 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,55 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "Codex Mate", + "version": "0.0.33", + "identifier": "ai.codexmate.desktop", + "build": { + "frontendDist": "../web-ui", + "devUrl": "http://127.0.0.1:3737", + "beforeDevCommand": "npm run desktop:prepare && node cli.js run --host 127.0.0.1 --no-browser", + "beforeBuildCommand": "npm run desktop:prepare" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Codex Mate", + "width": 1280, + "height": 860, + "minWidth": 960, + "minHeight": 640, + "resizable": true, + "fullscreen": false, + "url": "http://127.0.0.1:3737" + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "android": { + "debugApplicationIdSuffix": ".debug" + }, + "resources": { + "../cli.js": "codexmate/cli.js", + "../package.json": "codexmate/package.json", + "../package-lock.json": "codexmate/package-lock.json", + "../cli": "codexmate/cli", + "../lib": "codexmate/lib", + "../plugins": "codexmate/plugins", + "../web-ui": "codexmate/web-ui", + "../web-ui.html": "codexmate/web-ui.html", + "../node_modules": "codexmate/node_modules" + } + } +} diff --git a/tools/desktop/prepare-tauri-resources.js b/tools/desktop/prepare-tauri-resources.js new file mode 100644 index 00000000..c0f20182 --- /dev/null +++ b/tools/desktop/prepare-tauri-resources.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const rootDir = path.resolve(__dirname, '..', '..'); +const packagePath = path.join(rootDir, 'package.json'); +const tauriConfigPath = path.join(rootDir, 'src-tauri', 'tauri.conf.json'); +const cargoTomlPath = path.join(rootDir, 'src-tauri', 'Cargo.toml'); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function writeJson(filePath, value) { + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function assertExists(relativePath) { + const resolved = path.join(rootDir, relativePath); + if (!fs.existsSync(resolved)) { + throw new Error(`desktop resource is missing: ${relativePath}`); + } +} + +const pkg = readJson(packagePath); +const config = readJson(tauriConfigPath); + +config.productName = 'Codex Mate'; +config.version = pkg.version; +config.identifier = config.identifier && config.identifier !== 'com.tauri.dev' + ? config.identifier + : 'ai.codexmate.desktop'; + +config.build = { + ...(config.build || {}), + devUrl: 'http://127.0.0.1:3737', + frontendDist: '../web-ui', + beforeDevCommand: 'npm run desktop:prepare && node cli.js run --host 127.0.0.1 --no-browser', + beforeBuildCommand: 'npm run desktop:prepare' +}; + +config.app = { + ...(config.app || {}), + windows: [ + { + label: 'main', + title: 'Codex Mate', + width: 1280, + height: 860, + minWidth: 960, + minHeight: 640, + resizable: true, + fullscreen: false, + url: 'http://127.0.0.1:3737' + } + ], + security: { + ...(config.app && config.app.security ? config.app.security : {}), + csp: null + } +}; + +config.bundle = { + ...(config.bundle || {}), + active: true, + targets: 'all', + resources: { + '../cli.js': 'codexmate/cli.js', + '../package.json': 'codexmate/package.json', + '../package-lock.json': 'codexmate/package-lock.json', + '../cli': 'codexmate/cli', + '../lib': 'codexmate/lib', + '../plugins': 'codexmate/plugins', + '../web-ui': 'codexmate/web-ui', + '../web-ui.html': 'codexmate/web-ui.html', + '../node_modules': 'codexmate/node_modules' + } +}; + +[ + 'cli.js', + 'package.json', + 'package-lock.json', + 'cli', + 'lib', + 'plugins', + 'web-ui', + 'node_modules' +].forEach(assertExists); + +writeJson(tauriConfigPath, config); + +if (fs.existsSync(cargoTomlPath)) { + const cargoToml = fs.readFileSync(cargoTomlPath, 'utf8'); + const nextCargoToml = cargoToml.replace( + /(\[package\][\s\S]*?\nversion\s*=\s*")([^"]+)(")/, + `$1${pkg.version}$3` + ); + fs.writeFileSync(cargoTomlPath, nextCargoToml); +} + +console.log(`desktop config prepared for Codex Mate ${pkg.version}`);