diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 00000000..815af464 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,80 @@ +name: Build - Linux + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-linux: + name: Build Linux (Ubuntu) + runs-on: ubuntu-latest + + strategy: + matrix: + arch: [x64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y libarchive-tools + + - name: Build Electron app for Linux + run: npm run electron:build:linux + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify build artifacts + run: | + echo "=== Checking deb ===" + shopt -s nullglob + DEBS=(release/*.deb) + if [ ${#DEBS[@]} -eq 0 ]; then echo "ERROR: No .deb found"; exit 1; fi + for DEB in "${DEBS[@]}"; do + echo "deb: $DEB ($(stat --format=%s "$DEB") bytes)" + + echo "=== Checking deb metadata ===" + dpkg-deb --info "$DEB" + + echo "=== Checking deb contains dorothy binary ===" + MAIN_BIN=$(dpkg-deb --contents "$DEB" | grep -m1 "opt/Dorothy/dorothy$") + if [ -z "$MAIN_BIN" ]; then echo "ERROR: dorothy binary not found in deb"; exit 1; fi + BIN_SIZE=$(echo "$MAIN_BIN" | awk '{print $3}') + echo "Main binary: $(echo "$MAIN_BIN" | awk '{print $NF}') ($BIN_SIZE bytes)" + if [ "$BIN_SIZE" -eq 0 ]; then echo "ERROR: dorothy binary is zero-size"; exit 1; fi + done + + echo "=== All checks passed ===" + + - name: Upload deb artifact + uses: actions/upload-artifact@v4 + with: + name: Dorothy-Linux-deb-${{ matrix.arch }} + path: release/*.deb + if-no-files-found: warn + + - name: Upload to GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: | + release/*.deb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 14634310..1ab2a2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,9 @@ /.next/ /out/ -# production -/build +# production (Next.js output) +/build/* +!/build/linux/ # misc .DS_Store diff --git a/build/linux/after-install.sh b/build/linux/after-install.sh new file mode 100644 index 00000000..7619bbcf --- /dev/null +++ b/build/linux/after-install.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Set SUID bit on chrome-sandbox for Electron's sandbox to work. +# Required for Chromium's SUID sandbox on Linux (avoids --no-sandbox). +# Idempotent: safe to run multiple times. +chown root:root /opt/Dorothy/chrome-sandbox +chmod 4755 /opt/Dorothy/chrome-sandbox + +# Warn if SUID bit didn't stick (e.g. /opt mounted with nosuid) +if [ ! -u /opt/Dorothy/chrome-sandbox ]; then + echo "WARNING: Failed to set SUID bit on /opt/Dorothy/chrome-sandbox." >&2 + echo "If /opt is mounted with 'nosuid', the Electron sandbox will not work." >&2 + echo "You may need to run Dorothy with --no-sandbox as a workaround." >&2 +fi diff --git a/build/linux/before-remove.sh b/build/linux/before-remove.sh new file mode 100644 index 00000000..5f886997 --- /dev/null +++ b/build/linux/before-remove.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Remove SUID bit from chrome-sandbox before package removal. +# Ensures no world-executable SUID root binary is left behind. +if [ -f /opt/Dorothy/chrome-sandbox ]; then + chmod 0755 /opt/Dorothy/chrome-sandbox +fi diff --git a/electron/main.ts b/electron/main.ts index 2b20d1de..79b94b01 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -15,6 +15,18 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +if (process.platform === 'linux') { + // Ignore EPIPE errors on stdout/stderr (happens when launched from desktop entries) + process.stdout.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EPIPE') return; + throw err; + }); + process.stderr.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EPIPE') return; + throw err; + }); +} + // Types import type { AppSettings, AgentStatus } from './types'; diff --git a/package.json b/package.json index b37afb6c..2f06caef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,8 @@ { "name": "dorothy", "version": "1.2.7", + "description": "Agent Control Center - manage AI coding agents from a single desktop app", + "author": "Charlie85270", "private": true, "main": "electron/dist/main.js", "scripts": { @@ -14,8 +16,11 @@ "test:coverage": "vitest run --coverage", "electron:dev": "concurrently \"npm run dev\" \"npm run electron:start\"", "electron:start": "wait-on http://localhost:3000 && tsc -p electron/tsconfig.json && NODE_ENV=development electron .", - "electron:build": "bash -c 'set -e; mv src/app/api src/app/_api_backup; mv src/app/icon.tsx src/app/_icon_backup.tsx 2>/dev/null || true; trap \"mv src/app/_api_backup src/app/api 2>/dev/null; mv src/app/_icon_backup.tsx src/app/icon.tsx 2>/dev/null\" EXIT; ELECTRON_BUILD=1 next build; tsc -p electron/tsconfig.json; cd mcp-orchestrator && npm install && npm run build && cd ..; cd mcp-telegram && npm install && npm run build && cd ..; cd mcp-kanban && npm install && npm run build && cd ..; cd mcp-vault && npm install && npm run build && cd ..; cd mcp-socialdata && npm install && npm run build && cd ..; cd mcp-x && npm install && npm run build && cd ..; cd mcp-world && npm install && npm run build && cd ..; electron-builder --mac'", - "electron:pack": "tsc -p electron/tsconfig.json && cd mcp-orchestrator && npm install && npm run build && cd .. && cd mcp-telegram && npm install && npm run build && cd .. && cd mcp-kanban && npm install && npm run build && cd .. && cd mcp-vault && npm install && npm run build && cd .. && cd mcp-socialdata && npm install && npm run build && cd .. && cd mcp-x && npm install && npm run build && cd .. && cd mcp-world && npm install && npm run build && cd .. && electron-builder --dir --mac" + "electron:prepare": "bash scripts/electron-prepare.sh", + "electron:build": "npm run electron:prepare && electron-builder --mac", + "electron:build:linux": "npm run electron:prepare && electron-builder --linux", + "electron:pack": "npm run electron:prepare && electron-builder --dir --mac", + "electron:pack:linux": "npm run electron:prepare && electron-builder --dir --linux" }, "build": { "appId": "io.dorothy.app", @@ -116,6 +121,33 @@ "zip" ] }, + "linux": { + "category": "Development", + "icon": "public/icon-512.png", + "target": [ + "deb" + ], + "desktop": { + "StartupNotify": "true", + "Categories": "Development;IDE;" + } + }, + "deb": { + "maintainer": "Charlie85270", + "fpm": ["--after-install", "build/linux/after-install.sh", "--before-remove", "build/linux/before-remove.sh"], + "depends": [ + "libgtk-3-0 | libgtk-3-0t64", + "libnotify4", + "libnss3", + "libxss1", + "libxtst6", + "xdg-utils", + "libatspi2.0-0 | libatspi2.0-0t64", + "libuuid1", + "libsecret-1-0" + ], + "artifactName": "Dorothy-${version}-${arch}.deb" + }, "dmg": { "title": "Dorothy", "contents": [ diff --git a/scripts/electron-prepare.sh b/scripts/electron-prepare.sh new file mode 100755 index 00000000..1aa4dc70 --- /dev/null +++ b/scripts/electron-prepare.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Shared preparation steps for all Electron build/pack scripts. +# Builds Next.js (with API route backup), compiles TypeScript, and builds all MCP sub-packages. +set -e + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Backup API routes and icon that are incompatible with static export +mv "$REPO_ROOT/src/app/api" "$REPO_ROOT/src/app/_api_backup" +mv "$REPO_ROOT/src/app/icon.tsx" "$REPO_ROOT/src/app/_icon_backup.tsx" 2>/dev/null || true + +cleanup() { + mv "$REPO_ROOT/src/app/_api_backup" "$REPO_ROOT/src/app/api" 2>/dev/null || true + mv "$REPO_ROOT/src/app/_icon_backup.tsx" "$REPO_ROOT/src/app/icon.tsx" 2>/dev/null || true +} +trap cleanup EXIT + +# Build Next.js static export +ELECTRON_BUILD=1 npx next build + +# Compile Electron TypeScript +tsc -p electron/tsconfig.json + +# Build all MCP sub-packages +for pkg in mcp-orchestrator mcp-telegram mcp-kanban mcp-vault mcp-socialdata mcp-x mcp-world; do + pushd "$pkg" > /dev/null + npm install && npm run build + popd > /dev/null +done