diff --git a/.gitignore b/.gitignore index 33b20b217..74f5e8881 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ test-results/ *.AppImage *.deb *.rpm +*.apk resources/bin @@ -88,3 +89,13 @@ artifacts/** !artifacts/releases/**/*.AppImage !artifacts/releases/**/*.deb !artifacts/releases/**/*.rpm + +# Allow committing release binaries when explicitly staged under installers/. +!installers/ +installers/** +!installers/**/*.exe +!installers/**/*.apk +!installers/**/*.aab +!installers/**/*.zip +!installers/**/*.yml +!installers/**/*.blockmap diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 000000000..2b496e92a --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,18 @@ +# DeskClaw Android (APK) + +This is a Capacitor wrapper around the DeskClaw web UI. + +It produces an APK that bundles the UI for offline rendering. It does not embed the full Electron main-process features. + +## Build (Debug APK) + +```bash +cd mobile +pnpm run web:build +pnpm run web:sync +pnpm install +pnpm exec cap sync android +cd android +./gradlew assembleDebug +``` + diff --git a/mobile/capacitor.config.ts b/mobile/capacitor.config.ts new file mode 100644 index 000000000..e9b1cdaba --- /dev/null +++ b/mobile/capacitor.config.ts @@ -0,0 +1,14 @@ +import type { CapacitorConfig } from '@capacitor/cli' + +const config: CapacitorConfig = { + appId: 'dev.deskclaw.app', + appName: 'DeskClaw', + webDir: 'www', + bundledWebRuntime: false, + server: { + androidScheme: 'https', + }, +} + +export default config + diff --git a/mobile/package.json b/mobile/package.json new file mode 100644 index 000000000..5276a000f --- /dev/null +++ b/mobile/package.json @@ -0,0 +1,24 @@ +{ + "name": "deskclaw-mobile", + "private": true, + "version": "0.0.0", + "type": "module", + "packageManager": "pnpm@10.31.0", + "scripts": { + "web:build": "cd .. && pnpm run build:vite", + "web:sync": "node scripts/sync-web.mjs", + "android:init": "pnpm install && pnpm exec cap init DeskClaw dev.deskclaw.app --web-dir=www --npm-client=pnpm", + "android:add": "pnpm exec cap add android", + "android:sync": "pnpm exec cap sync android", + "android:apk:debug": "cd android && ./gradlew assembleDebug", + "android:apk:release": "cd android && ./gradlew assembleRelease" + }, + "dependencies": { + "@capacitor/android": "^7.4.3", + "@capacitor/core": "^7.4.3" + }, + "devDependencies": { + "@capacitor/cli": "^7.4.3" + } +} + diff --git a/mobile/scripts/sync-web.mjs b/mobile/scripts/sync-web.mjs new file mode 100644 index 000000000..e7788e590 --- /dev/null +++ b/mobile/scripts/sync-web.mjs @@ -0,0 +1,38 @@ +import { mkdir, readdir, rm, copyFile, stat } from 'node:fs/promises' +import { join, resolve } from 'node:path' + +async function copyDir(srcDir, dstDir) { + await mkdir(dstDir, { recursive: true }) + const entries = await readdir(srcDir, { withFileTypes: true }) + for (const entry of entries) { + const src = join(srcDir, entry.name) + const dst = join(dstDir, entry.name) + if (entry.isDirectory()) { + await copyDir(src, dst) + continue + } + if (entry.isFile()) { + await copyFile(src, dst) + } + } +} + +async function main() { + const repoRoot = resolve(process.cwd(), '..') + const fromDir = join(repoRoot, 'dist') + const toDir = resolve(process.cwd(), 'www') + + const st = await stat(fromDir).catch(() => null) + if (!st || !st.isDirectory()) { + throw new Error(`Missing dist/ at ${fromDir}. Run: pnpm -C .. run build:vite`) + } + + await rm(toDir, { recursive: true, force: true }) + await copyDir(fromDir, toDir) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) +