Initial extract: templates + methodology guide from Ideation
Public, version-controlled source of truth for TeamIDE's project templates, coding conventions, and the build-plan → PM scaffolding rules. Mounted read-only by TeamIDE's planner agent at runtime. Extracted from the personal Ideation repo as a precondition for workspace-projects-agent-spec.md §9.4. Personal Ideation stays private; this repo holds only the publishable methodology.
@@ -0,0 +1,348 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Desktop Build Script Template (Tauri)
|
||||
# Builds the Tauri desktop app for macOS
|
||||
#
|
||||
# Usage:
|
||||
# ./build.sh -dev # Development build (runs from build folder)
|
||||
# ./build.sh -dev -open # Development build and open app
|
||||
# ./build.sh -prod # Production build (builds to build/prod/)
|
||||
# ./build.sh -install # Build and open Finder for drag-to-Applications
|
||||
# ./build.sh clean # Clean build folder (preserves logs)
|
||||
# ./build.sh clean-logs # Clean all logs
|
||||
# ./build.sh logs # View latest console log
|
||||
#
|
||||
# ============================================================
|
||||
# PROJECT CONFIGURATION - Customize these for your project
|
||||
# ============================================================
|
||||
APP_NAME="TauriStarter" # Display name of the app
|
||||
BUNDLE_ID_BASE="com.tauristarter.app" # Base bundle ID
|
||||
VERSION="0.1.0" # App version
|
||||
BINARY_NAME="TauriStarter" # Rust binary name (matches productName in tauri.conf.json)
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[Build]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[Build]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[Build]${NC} $1"; }
|
||||
error() { echo -e "${RED}[Build]${NC} $1"; exit 1; }
|
||||
|
||||
# Paths
|
||||
TAURI_DIR="$SCRIPT_DIR/src-tauri"
|
||||
|
||||
# Build folder structure
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
DEV_BUILD_DIR="$BUILD_DIR/dev"
|
||||
PROD_BUILD_DIR="$BUILD_DIR/prod"
|
||||
LOGS_DIR="$BUILD_DIR/logs"
|
||||
CONSOLE_DIR="$BUILD_DIR/console"
|
||||
PROD_INSTALL_DIR="$HOME/Applications"
|
||||
|
||||
# Timestamp for log files
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
|
||||
# Parse arguments
|
||||
BUILD_MODE=""
|
||||
OPEN_APP=false
|
||||
SHOW_INSTALL=false
|
||||
CLEAN_BUILD=false
|
||||
CLEAN_LOGS=false
|
||||
SHOW_LOGS=false
|
||||
SECURE_BUILD=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-prod|--prod|--production)
|
||||
BUILD_MODE="prod"
|
||||
shift
|
||||
;;
|
||||
-dev|--dev|--development)
|
||||
BUILD_MODE="dev"
|
||||
shift
|
||||
;;
|
||||
-open|--open)
|
||||
OPEN_APP=true
|
||||
shift
|
||||
;;
|
||||
-install|--install)
|
||||
SHOW_INSTALL=true
|
||||
BUILD_MODE="prod"
|
||||
shift
|
||||
;;
|
||||
-secure|--secure)
|
||||
SECURE_BUILD=true
|
||||
shift
|
||||
;;
|
||||
clean|--clean)
|
||||
CLEAN_BUILD=true
|
||||
shift
|
||||
;;
|
||||
clean-logs|--clean-logs)
|
||||
CLEAN_LOGS=true
|
||||
shift
|
||||
;;
|
||||
logs|--logs)
|
||||
SHOW_LOGS=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "$APP_NAME Build Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [-dev|-prod] [-open] [-secure] [clean|clean-logs|logs]"
|
||||
echo ""
|
||||
echo "Build Commands:"
|
||||
echo " -dev, --dev Development build (runs from build folder)"
|
||||
echo " -prod, --prod Production build (builds to build/prod/)"
|
||||
echo " -install Build prod and open Finder for drag-to-Applications"
|
||||
echo " -open, --open Open the app after building"
|
||||
echo " -secure Build without automation (no HTTP server on port 9876)"
|
||||
echo ""
|
||||
echo "Other Commands:"
|
||||
echo " clean Clean build artifacts (preserves logs)"
|
||||
echo " clean-logs Remove all logs"
|
||||
echo " logs Show latest console log (last 100 lines)"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Build folders:"
|
||||
echo " Dev: $DEV_BUILD_DIR"
|
||||
echo " Prod: $PROD_BUILD_DIR (also installs to $PROD_INSTALL_DIR)"
|
||||
echo " Logs: $LOGS_DIR"
|
||||
echo " Console: $CONSOLE_DIR"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ./build.sh -dev -open # Build dev and open app"
|
||||
echo " ./build.sh -prod # Build prod to build/prod/"
|
||||
echo " ./build.sh -prod -secure # Build prod without automation"
|
||||
echo " ./build.sh -install # Build and open Finder to drag to Applications"
|
||||
echo " ./build.sh logs # View latest console output"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error "Unknown option: $1. Use --help for usage."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Handle logs command
|
||||
if [ "$SHOW_LOGS" = true ]; then
|
||||
if [ -f "$CONSOLE_DIR/console_latest.log" ]; then
|
||||
log "Latest console log:"
|
||||
echo "----------------------------------------"
|
||||
tail -100 "$CONSOLE_DIR/console_latest.log"
|
||||
else
|
||||
warn "No console logs found at $CONSOLE_DIR/console_latest.log"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle clean-logs command
|
||||
if [ "$CLEAN_LOGS" = true ]; then
|
||||
log "Cleaning logs..."
|
||||
rm -rf "$LOGS_DIR" "$CONSOLE_DIR"
|
||||
success "Logs cleaned!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle clean command
|
||||
if [ "$CLEAN_BUILD" = true ]; then
|
||||
log "Cleaning build folder (preserving logs)..."
|
||||
|
||||
# Backup logs
|
||||
if [ -d "$LOGS_DIR" ]; then
|
||||
mv "$LOGS_DIR" "$SCRIPT_DIR/.logs_backup" 2>/dev/null || true
|
||||
fi
|
||||
if [ -d "$CONSOLE_DIR" ]; then
|
||||
mv "$CONSOLE_DIR" "$SCRIPT_DIR/.console_backup" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Clean build dir
|
||||
rm -rf "$BUILD_DIR"
|
||||
|
||||
# Clean other build artifacts
|
||||
rm -rf "$SCRIPT_DIR/dist"
|
||||
rm -rf "$SCRIPT_DIR/.quasar"
|
||||
rm -rf "$TAURI_DIR/target"
|
||||
|
||||
# Restore logs
|
||||
mkdir -p "$BUILD_DIR"
|
||||
if [ -d "$SCRIPT_DIR/.logs_backup" ]; then
|
||||
mv "$SCRIPT_DIR/.logs_backup" "$LOGS_DIR"
|
||||
fi
|
||||
if [ -d "$SCRIPT_DIR/.console_backup" ]; then
|
||||
mv "$SCRIPT_DIR/.console_backup" "$CONSOLE_DIR"
|
||||
fi
|
||||
|
||||
success "Build folder cleaned!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Require a build mode if not running a command
|
||||
if [ -z "$BUILD_MODE" ]; then
|
||||
error "No build mode specified. Use -dev or -prod. See --help for usage."
|
||||
fi
|
||||
|
||||
# Create log directories
|
||||
mkdir -p "$LOGS_DIR"
|
||||
mkdir -p "$CONSOLE_DIR"
|
||||
|
||||
# Set up logging
|
||||
BUILD_LOG="$LOGS_DIR/build_${BUILD_MODE}_$TIMESTAMP.log"
|
||||
CONSOLE_LOG="$CONSOLE_DIR/console_$TIMESTAMP.log"
|
||||
|
||||
# Create symlinks to latest logs
|
||||
ln -sf "build_${BUILD_MODE}_$TIMESTAMP.log" "$LOGS_DIR/build_latest.log"
|
||||
ln -sf "console_$TIMESTAMP.log" "$CONSOLE_DIR/console_latest.log"
|
||||
|
||||
# Logging function that writes to both console and file
|
||||
log_both() {
|
||||
echo -e "${BLUE}[Build]${NC} $1"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$BUILD_LOG"
|
||||
}
|
||||
|
||||
# Set paths based on build mode
|
||||
if [ "$BUILD_MODE" = "prod" ]; then
|
||||
APP_BUNDLE="$PROD_BUILD_DIR/$APP_NAME.app"
|
||||
INSTALL_BUNDLE="$PROD_INSTALL_DIR/$APP_NAME.app"
|
||||
BUNDLE_ID="$BUNDLE_ID_BASE"
|
||||
else
|
||||
APP_BUNDLE="$DEV_BUILD_DIR/$APP_NAME.app"
|
||||
INSTALL_BUNDLE=""
|
||||
BUNDLE_ID="${BUNDLE_ID_BASE}.dev"
|
||||
fi
|
||||
|
||||
log_both "=========================================="
|
||||
log_both "Building $APP_NAME ($BUILD_MODE mode)"
|
||||
log_both "=========================================="
|
||||
log_both "Build log: $BUILD_LOG"
|
||||
log_both "Console log: $CONSOLE_LOG"
|
||||
|
||||
# Create build directories
|
||||
mkdir -p "$DEV_BUILD_DIR"
|
||||
mkdir -p "$PROD_BUILD_DIR"
|
||||
|
||||
# Kill existing instances before building
|
||||
log_both "Stopping existing instances..."
|
||||
pkill -f "$DEV_BUILD_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
|
||||
pkill -f "$PROD_BUILD_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
|
||||
pkill -f "$PROD_INSTALL_DIR/$APP_NAME.app/Contents/MacOS" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Check dependencies
|
||||
command -v node >/dev/null 2>&1 || error "Node.js is required but not installed"
|
||||
command -v cargo >/dev/null 2>&1 || error "Rust/Cargo is required but not installed"
|
||||
|
||||
# Step 1: Install npm dependencies if needed
|
||||
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
|
||||
log_both "Installing npm dependencies..."
|
||||
npm install 2>&1 | tee -a "$BUILD_LOG"
|
||||
fi
|
||||
|
||||
# Step 2: Build with Tauri (builds frontend + Rust + creates bundle)
|
||||
log_both "Building with Tauri (this embeds frontend into binary)..."
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Build cargo args for secure mode (no automation)
|
||||
CARGO_ARGS=""
|
||||
if [ "$SECURE_BUILD" = true ]; then
|
||||
log_both "Secure build: disabling automation feature..."
|
||||
CARGO_ARGS="-- --no-default-features --features custom-protocol"
|
||||
fi
|
||||
|
||||
if [ "$SHOW_INSTALL" = true ]; then
|
||||
# No --ci flag: opens Finder window for drag-to-Applications
|
||||
npx tauri build $CARGO_ARGS 2>&1 | tee -a "$BUILD_LOG"
|
||||
else
|
||||
# --ci flag: suppress Finder window
|
||||
npx tauri build --ci $CARGO_ARGS 2>&1 | tee -a "$BUILD_LOG"
|
||||
fi
|
||||
|
||||
# Find the generated app bundle
|
||||
TAURI_BUNDLE_DIR="$TAURI_DIR/target/release/bundle/macos"
|
||||
TAURI_APP_BUNDLE="$TAURI_BUNDLE_DIR/$APP_NAME.app"
|
||||
|
||||
if [ ! -d "$TAURI_APP_BUNDLE" ]; then
|
||||
error "Tauri build failed - app bundle not found at $TAURI_APP_BUNDLE"
|
||||
fi
|
||||
|
||||
# Step 3: Copy the Tauri-built bundle to our build directory
|
||||
log_both "Copying app bundle to $APP_BUNDLE..."
|
||||
rm -rf "$APP_BUNDLE"
|
||||
cp -R "$TAURI_APP_BUNDLE" "$APP_BUNDLE"
|
||||
|
||||
# Step 4: Update Info.plist with our bundle ID (dev vs prod)
|
||||
log_both "Updating Info.plist with bundle ID: $BUNDLE_ID..."
|
||||
if [ "$BUILD_MODE" = "dev" ]; then
|
||||
DISPLAY_NAME="$APP_NAME (Dev)"
|
||||
else
|
||||
DISPLAY_NAME="$APP_NAME"
|
||||
fi
|
||||
|
||||
# Use PlistBuddy to update the plist
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$APP_BUNDLE/Contents/Info.plist"
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $DISPLAY_NAME" "$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $DISPLAY_NAME" "$APP_BUNDLE/Contents/Info.plist"
|
||||
|
||||
# Step 5: For prod, also install to ~/Applications
|
||||
if [ "$BUILD_MODE" = "prod" ] && [ -n "$INSTALL_BUNDLE" ]; then
|
||||
log_both "Installing to $INSTALL_BUNDLE..."
|
||||
pkill -f "$INSTALL_BUNDLE/Contents/MacOS" 2>/dev/null || true
|
||||
sleep 1
|
||||
rm -rf "$INSTALL_BUNDLE"
|
||||
mkdir -p "$PROD_INSTALL_DIR"
|
||||
cp -R "$APP_BUNDLE" "$INSTALL_BUNDLE"
|
||||
touch "$INSTALL_BUNDLE"
|
||||
fi
|
||||
|
||||
# Touch the app to update Finder cache
|
||||
touch "$APP_BUNDLE"
|
||||
|
||||
# Build complete
|
||||
log_both "=========================================="
|
||||
success "Build Complete!"
|
||||
log_both "=========================================="
|
||||
echo ""
|
||||
echo " Build folder: $BUILD_DIR"
|
||||
echo " App bundle: $APP_BUNDLE"
|
||||
echo " Mode: $BUILD_MODE"
|
||||
echo " Bundle ID: $BUNDLE_ID"
|
||||
if [ "$SECURE_BUILD" = true ]; then
|
||||
echo " Automation: DISABLED (secure build)"
|
||||
else
|
||||
echo " Automation: enabled (port 9876)"
|
||||
fi
|
||||
if [ "$BUILD_MODE" = "prod" ] && [ -n "$INSTALL_BUNDLE" ]; then
|
||||
echo " Installed to: $INSTALL_BUNDLE"
|
||||
fi
|
||||
echo ""
|
||||
echo " Build log: $BUILD_LOG"
|
||||
echo " Console log: $CONSOLE_LOG"
|
||||
echo ""
|
||||
|
||||
# Open the app and capture console output
|
||||
if [ "$OPEN_APP" = true ]; then
|
||||
log_both "Opening app (console output will be logged)..."
|
||||
|
||||
# Run the app in background with output to console log
|
||||
"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" >> "$CONSOLE_LOG" 2>&1 &
|
||||
APP_PID=$!
|
||||
|
||||
echo " App started (PID: $APP_PID)"
|
||||
echo " Console log: $CONSOLE_LOG"
|
||||
echo ""
|
||||
echo " View logs with: ./build.sh logs"
|
||||
else
|
||||
echo " To run: open \"$APP_BUNDLE\""
|
||||
echo " Or with logging: \"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME\" 2>&1 | tee -a \"$CONSOLE_LOG\""
|
||||
fi
|
||||
echo ""
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri Starter</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="q-app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "tauri-starter",
|
||||
"version": "0.1.0",
|
||||
"description": "Quasar + Vite + Tauri starter template",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.9",
|
||||
"pinia": "^2.1.7",
|
||||
"quasar": "^2.14.2",
|
||||
"vue": "^3.4.18",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/vite-plugin": "^1.10.0",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"@tauri-apps/cli": "^1.5.11",
|
||||
"@types/node": "^20.11.16",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "tauri-starter"
|
||||
version = "0.1.0"
|
||||
description = "Tauri Starter App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.6", features = ["shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4"
|
||||
tiny_http = "0.12" # For automation HTTP server
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol", "automation"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
automation = [] # Enable automation HTTP server on port 9876
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
After Width: | Height: | Size: 299 B |
|
After Width: | Height: | Size: 855 B |
|
After Width: | Height: | Size: 104 B |
|
After Width: | Height: | Size: 239 B |
|
After Width: | Height: | Size: 331 B |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 975 B |
|
After Width: | Height: | Size: 103 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 171 B |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,166 @@
|
||||
# Tauri Automation Plugin
|
||||
|
||||
A lightweight, reusable automation layer for Tauri applications. Enables external tools to interact with your app via HTTP API.
|
||||
|
||||
## Features
|
||||
|
||||
- **HTTP API** on `localhost:9876` for external automation
|
||||
- **Navigation**: Route to pages
|
||||
- **Element interaction**: Click, type, clear, select
|
||||
- **Reading**: Get text, values, attributes, check existence
|
||||
- **Waiting**: Wait for elements with timeout
|
||||
- **Screenshots**: Capture page as PNG
|
||||
- **Debug mode only**: Automatically disabled in release builds
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ HTTP ┌──────────────────┐
|
||||
│ External Tool │ ───────────▶ │ Rust HTTP Server │
|
||||
│ (curl, tests) │ │ (port 9876) │
|
||||
└─────────────────┘ └────────┬─────────┘
|
||||
│
|
||||
│ Tauri Command
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ JavaScript │
|
||||
│ Automation │
|
||||
│ Service │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
│ DOM APIs
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ WebView │
|
||||
│ (Your App) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## HTTP API
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/automation/execute` | Execute a command |
|
||||
| GET | `/automation/screenshot` | Capture screenshot (PNG) |
|
||||
| GET | `/automation/health` | Health check |
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Navigate to a route
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "navigate", "args": {"path": "/profile"}}'
|
||||
|
||||
# Click an element
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "click", "args": {"selector": "#submit-btn"}}'
|
||||
|
||||
# Type into an input
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "type", "args": {"selector": "#email", "text": "test@example.com"}}'
|
||||
|
||||
# Get element text
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "getText", "args": {"selector": ".heading"}}'
|
||||
|
||||
# Wait for element
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "waitFor", "args": {"selector": ".loaded", "timeout": 5000}}'
|
||||
|
||||
# Take screenshot
|
||||
curl http://localhost:9876/automation/screenshot --output screenshot.png
|
||||
|
||||
# Health check
|
||||
curl http://localhost:9876/automation/health
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
| Command | Args | Returns | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `navigate` | `{path}` | `null` | Navigate to route |
|
||||
| `click` | `{selector}` | `null` | Click element |
|
||||
| `type` | `{selector, text}` | `null` | Type into input |
|
||||
| `clear` | `{selector}` | `null` | Clear input value |
|
||||
| `getText` | `{selector}` | `string` | Get element text |
|
||||
| `getValue` | `{selector}` | `string` | Get input value |
|
||||
| `getAttribute` | `{selector, attribute}` | `string` | Get attribute |
|
||||
| `exists` | `{selector}` | `boolean` | Check if element exists |
|
||||
| `waitFor` | `{selector, timeout?}` | `boolean` | Wait for element (default 5s) |
|
||||
| `select` | `{selector, value}` | `null` | Select dropdown option |
|
||||
| `check` | `{selector}` | `null` | Check checkbox |
|
||||
| `uncheck` | `{selector}` | `null` | Uncheck checkbox |
|
||||
| `getUrl` | `{}` | `string` | Get current URL |
|
||||
| `getTitle` | `{}` | `string` | Get page title |
|
||||
| `eval` | `{script}` | `any` | Evaluate JavaScript |
|
||||
|
||||
## Installation (Adding to Another Project)
|
||||
|
||||
### 1. Copy Rust files
|
||||
|
||||
Copy `src-tauri/src/automation/` to your project's `src-tauri/src/`.
|
||||
|
||||
### 2. Update Cargo.toml
|
||||
|
||||
Add dependencies:
|
||||
```toml
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
warp = "0.3"
|
||||
serde_json = "1.0"
|
||||
```
|
||||
|
||||
### 3. Update main.rs
|
||||
|
||||
```rust
|
||||
#[cfg(debug_assertions)]
|
||||
mod automation;
|
||||
|
||||
fn main() {
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
builder = builder
|
||||
.plugin(automation::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
automation::commands::execute_automation
|
||||
]);
|
||||
}
|
||||
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Copy JS files
|
||||
|
||||
Copy `src/automation/` to your project's `src/`.
|
||||
|
||||
### 5. Initialize in App.vue
|
||||
|
||||
```typescript
|
||||
import { initAutomation } from './automation'
|
||||
|
||||
onMounted(() => {
|
||||
initAutomation()
|
||||
})
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- **Debug builds only**: The HTTP server only runs in debug builds (`#[cfg(debug_assertions)]`)
|
||||
- **Localhost only**: Server binds to `127.0.0.1`, not accessible externally
|
||||
- **No auth**: Designed for local development/testing only
|
||||
|
||||
## License
|
||||
|
||||
MIT - Use freely in your projects.
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Tauri Automation Plugin
|
||||
//!
|
||||
//! Provides HTTP API for external automation of the application.
|
||||
//! Enabled via the "automation" feature flag.
|
||||
|
||||
pub mod server;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime, Window,
|
||||
};
|
||||
|
||||
/// Shared state for automation
|
||||
pub struct AutomationState {
|
||||
pub window: Arc<Mutex<Option<Window<tauri::Wry>>>>,
|
||||
}
|
||||
|
||||
impl Default for AutomationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
window: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the automation plugin
|
||||
pub fn init() -> TauriPlugin<tauri::Wry> {
|
||||
Builder::new("automation")
|
||||
.setup(|app| {
|
||||
let state = AutomationState::default();
|
||||
app.manage(state);
|
||||
|
||||
// Start HTTP server in background thread
|
||||
let app_handle = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
server::start_server(app_handle);
|
||||
});
|
||||
|
||||
println!("[Automation] Plugin initialized - HTTP server starting on port 9876");
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Execute an automation command via JavaScript evaluation
|
||||
pub fn execute_command(window: &Window<tauri::Wry>, command: &str, args: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let args_json = serde_json::to_string(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let script = format!(
|
||||
r#"
|
||||
(async function() {{
|
||||
try {{
|
||||
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {{
|
||||
return JSON.stringify({{ error: 'Automation not initialized. Call initAutomation() first.' }});
|
||||
}}
|
||||
const result = await window.__TAURI_AUTOMATION__.execute('{}', {});
|
||||
return JSON.stringify({{ success: true, result: result }});
|
||||
}} catch (e) {{
|
||||
return JSON.stringify({{ error: e.message || String(e) }});
|
||||
}}
|
||||
}})()
|
||||
"#,
|
||||
command,
|
||||
args_json
|
||||
);
|
||||
|
||||
// Execute JavaScript and get result
|
||||
let result = window.eval(&script);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// For Tauri 1.x, eval doesn't return a value directly
|
||||
Ok(serde_json::json!({ "success": true, "note": "Command sent to webview" }))
|
||||
}
|
||||
Err(e) => Err(format!("Failed to execute script: {}", e))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
//! HTTP server for automation commands
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tiny_http::{Server, Response, Header, Method};
|
||||
|
||||
const PORT: u16 = 9876;
|
||||
|
||||
// Global screenshot buffer (simple approach without lazy_static)
|
||||
static SCREENSHOT_DATA: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
pub fn set_screenshot_data(data: String) {
|
||||
if let Ok(mut guard) = SCREENSHOT_DATA.lock() {
|
||||
*guard = Some(data);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_screenshot_data() -> Option<String> {
|
||||
if let Ok(mut guard) = SCREENSHOT_DATA.lock() {
|
||||
guard.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the HTTP server
|
||||
pub fn start_server(app_handle: AppHandle<tauri::Wry>) {
|
||||
let addr = format!("127.0.0.1:{}", PORT);
|
||||
|
||||
let server = match Server::http(&addr) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("[Automation] Failed to start server on {}: {}", addr, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("[Automation] HTTP server listening on http://{}", addr);
|
||||
|
||||
for mut request in server.incoming_requests() {
|
||||
let method = request.method().clone();
|
||||
let url = request.url().to_string();
|
||||
|
||||
println!("[Automation] {} {}", method, url);
|
||||
|
||||
let response = match (&method, url.as_str()) {
|
||||
// Health check
|
||||
(&Method::Get, "/automation/health") => {
|
||||
json_response(serde_json::json!({
|
||||
"status": "ok",
|
||||
"port": PORT,
|
||||
"version": "1.0.0"
|
||||
}))
|
||||
}
|
||||
|
||||
// Execute command
|
||||
(&Method::Post, "/automation/execute") => {
|
||||
handle_execute(&app_handle, &mut request)
|
||||
}
|
||||
|
||||
// Screenshot
|
||||
(&Method::Get, "/automation/screenshot") => {
|
||||
handle_screenshot(&app_handle)
|
||||
}
|
||||
|
||||
// CORS preflight
|
||||
(&Method::Options, _) => {
|
||||
cors_response()
|
||||
}
|
||||
|
||||
// 404
|
||||
_ => {
|
||||
json_response_with_status(
|
||||
serde_json::json!({ "error": "Not found" }),
|
||||
404
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = request.respond(response) {
|
||||
eprintln!("[Automation] Failed to send response: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle execute command request
|
||||
fn handle_execute(
|
||||
app_handle: &AppHandle<tauri::Wry>,
|
||||
request: &mut tiny_http::Request,
|
||||
) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
// Read body
|
||||
let mut body = String::new();
|
||||
if let Err(e) = request.as_reader().read_to_string(&mut body) {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": format!("Failed to read body: {}", e) }),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
let payload: serde_json::Value = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": format!("Invalid JSON: {}", e) }),
|
||||
400
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let command = match payload.get("command").and_then(|v| v.as_str()) {
|
||||
Some(c) => c.to_string(),
|
||||
None => {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": "Missing 'command' field" }),
|
||||
400
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let args = payload.get("args").cloned().unwrap_or(serde_json::json!({}));
|
||||
|
||||
// Get the main window
|
||||
let window = match app_handle.get_window("main") {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": "Main window not found" }),
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Build JavaScript to execute
|
||||
let args_json = serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string());
|
||||
let script = format!(
|
||||
r#"
|
||||
(async function() {{
|
||||
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {{
|
||||
console.error('[Automation] Not initialized');
|
||||
return;
|
||||
}}
|
||||
try {{
|
||||
const result = await window.__TAURI_AUTOMATION__.execute('{}', {});
|
||||
window.__TAURI_AUTOMATION__._lastResult = {{ success: true, result: result }};
|
||||
}} catch (e) {{
|
||||
window.__TAURI_AUTOMATION__._lastResult = {{ success: false, error: e.message || String(e) }};
|
||||
}}
|
||||
}})();
|
||||
"#,
|
||||
command,
|
||||
args_json
|
||||
);
|
||||
|
||||
// Execute the script
|
||||
if let Err(e) = window.eval(&script) {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": format!("Script execution failed: {}", e) }),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// Wait a bit for async commands to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// Return success - the result is stored in the webview for debugging
|
||||
json_response(serde_json::json!({
|
||||
"success": true,
|
||||
"message": "Command executed",
|
||||
"command": command
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle screenshot request
|
||||
fn handle_screenshot(app_handle: &AppHandle<tauri::Wry>) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let window = match app_handle.get_window("main") {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": "Main window not found" }),
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Request screenshot from JS
|
||||
let script = r#"
|
||||
(async function() {
|
||||
if (typeof window.__TAURI_AUTOMATION__ === 'undefined') {
|
||||
console.error('[Automation] Not initialized');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await window.__TAURI_AUTOMATION__.captureAndSend();
|
||||
} catch (e) {
|
||||
console.error('[Automation] Screenshot failed:', e);
|
||||
}
|
||||
})();
|
||||
"#;
|
||||
|
||||
if let Err(e) = window.eval(script) {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": format!("Screenshot request failed: {}", e) }),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for JS to send the screenshot data
|
||||
// html2canvas can take a while, especially on first load
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
|
||||
// Check if we have screenshot data
|
||||
if let Some(data_url) = take_screenshot_data() {
|
||||
// Parse data URL: data:image/png;base64,....
|
||||
if let Some(base64_data) = data_url.strip_prefix("data:image/png;base64,") {
|
||||
match base64_decode(base64_data) {
|
||||
Ok(bytes) => {
|
||||
return png_response(bytes);
|
||||
}
|
||||
Err(e) => {
|
||||
return json_response_with_status(
|
||||
serde_json::json!({ "error": format!("Base64 decode failed: {}", e) }),
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_response_with_status(
|
||||
serde_json::json!({ "error": "Screenshot not available. Make sure html2canvas is loaded." }),
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
/// Simple base64 decoder
|
||||
fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
|
||||
let input = input.trim();
|
||||
let chars: Vec<char> = input.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
|
||||
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
let mut output = Vec::new();
|
||||
let mut buffer: u32 = 0;
|
||||
let mut bits_collected = 0;
|
||||
|
||||
for c in chars {
|
||||
if c == '=' {
|
||||
break;
|
||||
}
|
||||
|
||||
let value = ALPHABET.iter().position(|&x| x == c as u8)
|
||||
.ok_or_else(|| format!("Invalid base64 character: {}", c))? as u32;
|
||||
|
||||
buffer = (buffer << 6) | value;
|
||||
bits_collected += 6;
|
||||
|
||||
if bits_collected >= 8 {
|
||||
bits_collected -= 8;
|
||||
output.push((buffer >> bits_collected) as u8);
|
||||
buffer &= (1 << bits_collected) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Create a JSON response
|
||||
fn json_response(data: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
json_response_with_status(data, 200)
|
||||
}
|
||||
|
||||
/// Create a JSON response with status
|
||||
fn json_response_with_status(data: serde_json::Value, status: u16) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let body = serde_json::to_vec(&data).unwrap_or_else(|_| b"{}".to_vec());
|
||||
let len = body.len();
|
||||
let cursor = std::io::Cursor::new(body);
|
||||
|
||||
Response::new(
|
||||
tiny_http::StatusCode(status),
|
||||
vec![
|
||||
Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
|
||||
],
|
||||
cursor,
|
||||
Some(len),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a PNG response
|
||||
fn png_response(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let len = data.len();
|
||||
let cursor = std::io::Cursor::new(data);
|
||||
|
||||
Response::new(
|
||||
tiny_http::StatusCode(200),
|
||||
vec![
|
||||
Header::from_bytes(&b"Content-Type"[..], &b"image/png"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
|
||||
],
|
||||
cursor,
|
||||
Some(len),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// CORS preflight response
|
||||
fn cors_response() -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let cursor = std::io::Cursor::new(Vec::new());
|
||||
|
||||
Response::new(
|
||||
tiny_http::StatusCode(204),
|
||||
vec![
|
||||
Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Methods"[..], &b"GET, POST, OPTIONS"[..]).unwrap(),
|
||||
Header::from_bytes(&b"Access-Control-Allow-Headers"[..], &b"Content-Type"[..]).unwrap(),
|
||||
],
|
||||
cursor,
|
||||
Some(0),
|
||||
None,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Prevents additional console window on Windows in release
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use chrono::Local;
|
||||
|
||||
// Automation module - enabled via feature flag
|
||||
#[cfg(feature = "automation")]
|
||||
mod automation;
|
||||
|
||||
/// Log frontend console messages to stdout with timestamp
|
||||
#[tauri::command]
|
||||
fn log_to_stdout(level: String, message: String) {
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
println!("[{}] [{}] {}", timestamp, level, message);
|
||||
}
|
||||
|
||||
/// Example command - replace with your own
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! Welcome to Tauri.", name)
|
||||
}
|
||||
|
||||
/// Receive screenshot data from JS automation
|
||||
#[cfg(feature = "automation")]
|
||||
#[tauri::command]
|
||||
fn automation_screenshot_data(data: String) {
|
||||
automation::server::set_screenshot_data(data);
|
||||
}
|
||||
|
||||
#[cfg(feature = "automation")]
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(automation::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
log_to_stdout,
|
||||
greet,
|
||||
automation_screenshot_data
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "automation"))]
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
log_to_stdout,
|
||||
greet
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:9000",
|
||||
"distDir": "../dist"
|
||||
},
|
||||
"package": {
|
||||
"productName": "TauriStarter",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.tauristarter.app",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "Tauri Starter",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// App root component
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
# Automation Service (JavaScript)
|
||||
|
||||
Frontend automation service that handles DOM manipulation commands from the Rust backend.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.ts` | Main service initialization and screenshot capture |
|
||||
| `commands.ts` | All DOM automation commands |
|
||||
|
||||
## Setup
|
||||
|
||||
Initialize in your app's entry point (App.vue or main.ts):
|
||||
|
||||
```typescript
|
||||
import { initAutomation } from './automation'
|
||||
|
||||
// In your app setup (only runs in dev)
|
||||
initAutomation()
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Navigation
|
||||
- `navigate` - Navigate to a Vue Router path
|
||||
|
||||
### Element Interaction
|
||||
- `click` - Click an element
|
||||
- `type` - Type text into an input
|
||||
- `clear` - Clear an input value
|
||||
- `select` - Select dropdown option
|
||||
- `check` - Check a checkbox
|
||||
- `uncheck` - Uncheck a checkbox
|
||||
- `focus` - Focus an element
|
||||
- `blur` - Blur an element
|
||||
- `pressKey` - Simulate key press
|
||||
- `submit` - Submit a form
|
||||
- `scrollTo` - Scroll to element
|
||||
|
||||
### Reading
|
||||
- `getText` - Get element text content
|
||||
- `getValue` - Get input value
|
||||
- `getAttribute` - Get element attribute
|
||||
- `exists` - Check if element exists
|
||||
- `getElements` - Get all matching elements
|
||||
- `getHtml` - Get element HTML
|
||||
- `getUrl` - Get current URL
|
||||
- `getTitle` - Get page title
|
||||
|
||||
### Waiting
|
||||
- `waitFor` - Wait for element to appear
|
||||
- `wait` - Wait for specified milliseconds
|
||||
|
||||
### Advanced
|
||||
- `eval` - Execute arbitrary JavaScript
|
||||
|
||||
## Adding Custom Commands
|
||||
|
||||
Add new commands to `commands.ts`:
|
||||
|
||||
```typescript
|
||||
export const commands: Record<string, CommandHandler> = {
|
||||
// ... existing commands
|
||||
|
||||
async myCustomCommand(args): Promise<void> {
|
||||
const value = args.value as string
|
||||
// Your implementation
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Screenshot Support
|
||||
|
||||
Screenshots use html2canvas, loaded dynamically from CDN:
|
||||
- https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
|
||||
|
||||
For offline use, bundle html2canvas:
|
||||
```bash
|
||||
npm install html2canvas
|
||||
```
|
||||
|
||||
Then update `index.ts` to import directly instead of CDN loading.
|
||||
|
||||
## Vue Router Integration
|
||||
|
||||
The `navigate` command tries to use Vue Router. For this to work, you need to expose the router on the window object. Update your `main.ts`:
|
||||
|
||||
```typescript
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
|
||||
// Expose for automation
|
||||
if (import.meta.env.DEV) {
|
||||
(window as any).__VUE_APP__ = app
|
||||
}
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
All selector arguments use standard CSS selectors:
|
||||
|
||||
```bash
|
||||
# By ID
|
||||
{"selector": "#my-id"}
|
||||
|
||||
# By class
|
||||
{"selector": ".my-class"}
|
||||
|
||||
# By attribute
|
||||
{"selector": "[data-testid='submit']"}
|
||||
|
||||
# Combined
|
||||
{"selector": "form#login .btn-primary"}
|
||||
|
||||
# Quasar components often need to target inner elements
|
||||
{"selector": ".q-input input"}
|
||||
{"selector": ".q-btn"}
|
||||
```
|
||||
|
||||
## Tips for Quasar
|
||||
|
||||
Quasar components have specific DOM structures:
|
||||
|
||||
| Component | Selector for inner element |
|
||||
|-----------|---------------------------|
|
||||
| q-input | `.q-input input` or `.q-input textarea` |
|
||||
| q-select | `.q-select` (click to open) |
|
||||
| q-btn | `.q-btn` |
|
||||
| q-checkbox | `.q-checkbox` |
|
||||
| q-tab | `.q-tab` |
|
||||
|
||||
Use `data-testid` attributes for reliable selectors:
|
||||
```vue
|
||||
<q-btn data-testid="submit-btn" label="Submit" />
|
||||
```
|
||||
```bash
|
||||
{"selector": "[data-testid='submit-btn']"}
|
||||
```
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Automation Commands
|
||||
*
|
||||
* DOM manipulation commands for automated testing.
|
||||
*/
|
||||
|
||||
export type CommandArgs = Record<string, unknown>
|
||||
export type CommandResult = unknown
|
||||
|
||||
type CommandHandler = (args: CommandArgs) => Promise<CommandResult>
|
||||
|
||||
// Router type for type safety
|
||||
interface VueRouter {
|
||||
push: (path: string) => Promise<unknown>
|
||||
currentRoute: { value: { path: string } }
|
||||
}
|
||||
|
||||
// Get Vue Router instance from the app
|
||||
function getRouter(): VueRouter | undefined {
|
||||
// Access the router from the app instance stored on window
|
||||
const app = (window as unknown as { __VUE_APP__?: { config: { globalProperties: { $router: VueRouter } } } }).__VUE_APP__
|
||||
return app?.config.globalProperties.$router
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an element to appear in the DOM
|
||||
*/
|
||||
async function waitForElement(selector: string, timeout = 5000): Promise<Element | null> {
|
||||
const start = Date.now()
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
const element = document.querySelector(selector)
|
||||
if (element) return element
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element or throw
|
||||
*/
|
||||
function getElement(selector: string): Element {
|
||||
const element = document.querySelector(selector)
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${selector}`)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate realistic typing into an input
|
||||
*/
|
||||
function simulateTyping(element: HTMLInputElement | HTMLTextAreaElement, text: string): void {
|
||||
element.focus()
|
||||
element.value = text
|
||||
|
||||
// Dispatch events to trigger Vue reactivity
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Available automation commands
|
||||
*/
|
||||
export const commands: Record<string, CommandHandler> = {
|
||||
/**
|
||||
* Navigate to a route
|
||||
*/
|
||||
async navigate(args): Promise<void> {
|
||||
const path = args.path as string
|
||||
if (!path) throw new Error('Missing path argument')
|
||||
|
||||
const router = getRouter()
|
||||
if (router) {
|
||||
await router.push(path)
|
||||
} else {
|
||||
// Fallback: use window.location
|
||||
window.location.hash = path
|
||||
}
|
||||
|
||||
// Wait for navigation to complete
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
},
|
||||
|
||||
/**
|
||||
* Click an element
|
||||
*/
|
||||
async click(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = await waitForElement(selector, 5000)
|
||||
if (!element) throw new Error(`Element not found: ${selector}`)
|
||||
|
||||
// Scroll into view
|
||||
element.scrollIntoView({ behavior: 'instant', block: 'center' })
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
|
||||
// Click
|
||||
;(element as HTMLElement).click()
|
||||
|
||||
// Wait for any resulting actions
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
},
|
||||
|
||||
/**
|
||||
* Type text into an input
|
||||
*/
|
||||
async type(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
const text = args.text as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
if (text === undefined) throw new Error('Missing text argument')
|
||||
|
||||
const element = await waitForElement(selector, 5000)
|
||||
if (!element) throw new Error(`Element not found: ${selector}`)
|
||||
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||
simulateTyping(element, text)
|
||||
} else {
|
||||
throw new Error(`Element is not an input: ${selector}`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear an input
|
||||
*/
|
||||
async clear(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||
simulateTyping(element, '')
|
||||
} else {
|
||||
throw new Error(`Element is not an input: ${selector}`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get element text content
|
||||
*/
|
||||
async getText(args): Promise<string> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = await waitForElement(selector, 5000)
|
||||
if (!element) throw new Error(`Element not found: ${selector}`)
|
||||
|
||||
return element.textContent?.trim() || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* Get input value
|
||||
*/
|
||||
async getValue(args): Promise<string> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
||||
return element.value
|
||||
}
|
||||
|
||||
throw new Error(`Element is not an input: ${selector}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get element attribute
|
||||
*/
|
||||
async getAttribute(args): Promise<string | null> {
|
||||
const selector = args.selector as string
|
||||
const attribute = args.attribute as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
if (!attribute) throw new Error('Missing attribute argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
return element.getAttribute(attribute)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if element exists
|
||||
*/
|
||||
async exists(args): Promise<boolean> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
return document.querySelector(selector) !== null
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for element to appear
|
||||
*/
|
||||
async waitFor(args): Promise<boolean> {
|
||||
const selector = args.selector as string
|
||||
const timeout = (args.timeout as number) || 5000
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = await waitForElement(selector, timeout)
|
||||
return element !== null
|
||||
},
|
||||
|
||||
/**
|
||||
* Select dropdown option
|
||||
*/
|
||||
async select(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
const value = args.value as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
if (value === undefined) throw new Error('Missing value argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLSelectElement) {
|
||||
element.value = value
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
} else {
|
||||
throw new Error(`Element is not a select: ${selector}`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check a checkbox
|
||||
*/
|
||||
async check(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLInputElement && element.type === 'checkbox') {
|
||||
if (!element.checked) {
|
||||
element.click()
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Element is not a checkbox: ${selector}`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uncheck a checkbox
|
||||
*/
|
||||
async uncheck(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLInputElement && element.type === 'checkbox') {
|
||||
if (element.checked) {
|
||||
element.click()
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Element is not a checkbox: ${selector}`)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current URL/route
|
||||
*/
|
||||
async getUrl(): Promise<string> {
|
||||
return window.location.href
|
||||
},
|
||||
|
||||
/**
|
||||
* Get page title
|
||||
*/
|
||||
async getTitle(): Promise<string> {
|
||||
return document.title
|
||||
},
|
||||
|
||||
/**
|
||||
* Evaluate arbitrary JavaScript
|
||||
*/
|
||||
async eval(args): Promise<unknown> {
|
||||
const script = args.script as string
|
||||
if (!script) throw new Error('Missing script argument')
|
||||
|
||||
// Use Function constructor to evaluate in global scope
|
||||
const fn = new Function(`return (async () => { ${script} })()`)
|
||||
return await fn()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all elements matching selector
|
||||
*/
|
||||
async getElements(args): Promise<{ count: number; texts: string[] }> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const elements = document.querySelectorAll(selector)
|
||||
const texts = Array.from(elements).map(el => el.textContent?.trim() || '')
|
||||
|
||||
return { count: elements.length, texts }
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll to element
|
||||
*/
|
||||
async scrollTo(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
},
|
||||
|
||||
/**
|
||||
* Get page HTML
|
||||
*/
|
||||
async getHtml(args): Promise<string> {
|
||||
const selector = (args.selector as string) || 'body'
|
||||
const element = getElement(selector)
|
||||
return element.innerHTML
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for a specified time
|
||||
*/
|
||||
async wait(args): Promise<void> {
|
||||
const ms = (args.ms as number) || 1000
|
||||
await new Promise(r => setTimeout(r, ms))
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus an element
|
||||
*/
|
||||
async focus(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector) as HTMLElement
|
||||
element.focus()
|
||||
},
|
||||
|
||||
/**
|
||||
* Blur (unfocus) an element
|
||||
*/
|
||||
async blur(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector) as HTMLElement
|
||||
element.blur()
|
||||
},
|
||||
|
||||
/**
|
||||
* Press a key
|
||||
*/
|
||||
async pressKey(args): Promise<void> {
|
||||
const key = args.key as string
|
||||
const selector = args.selector as string
|
||||
if (!key) throw new Error('Missing key argument')
|
||||
|
||||
const target = selector ? getElement(selector) : document.activeElement || document.body
|
||||
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
|
||||
target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }))
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit a form
|
||||
*/
|
||||
async submit(args): Promise<void> {
|
||||
const selector = args.selector as string
|
||||
if (!selector) throw new Error('Missing selector argument')
|
||||
|
||||
const element = getElement(selector)
|
||||
|
||||
if (element instanceof HTMLFormElement) {
|
||||
element.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
} else {
|
||||
// Find parent form
|
||||
const form = element.closest('form')
|
||||
if (form) {
|
||||
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
} else {
|
||||
throw new Error('No form found')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Tauri Automation Service
|
||||
*
|
||||
* Provides automation capabilities for external testing tools.
|
||||
* Only active in development builds.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { commands, type CommandArgs, type CommandResult } from './commands'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_AUTOMATION__: AutomationService
|
||||
}
|
||||
}
|
||||
|
||||
export interface AutomationService {
|
||||
execute: (command: string, args: CommandArgs) => Promise<CommandResult>
|
||||
screenshot: () => Promise<string>
|
||||
captureAndSend: () => Promise<void>
|
||||
_lastResult: { success: boolean; result?: unknown; error?: string } | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the automation service
|
||||
* Call this once when your app starts
|
||||
* Note: Automation is controlled by Rust feature flag, JS side always initializes
|
||||
*/
|
||||
export function initAutomation(): void {
|
||||
console.log('[Automation] Initializing...')
|
||||
|
||||
const service: AutomationService = {
|
||||
_lastResult: null,
|
||||
|
||||
/**
|
||||
* Execute an automation command
|
||||
*/
|
||||
async execute(command: string, args: CommandArgs = {}): Promise<CommandResult> {
|
||||
console.log(`[Automation] Executing: ${command}`, args)
|
||||
|
||||
const handler = commands[command]
|
||||
if (!handler) {
|
||||
throw new Error(`Unknown command: ${command}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(args)
|
||||
console.log(`[Automation] Result:`, result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`[Automation] Error:`, error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Capture screenshot and return as data URL
|
||||
*/
|
||||
async screenshot(): Promise<string> {
|
||||
// Dynamically import html2canvas
|
||||
const html2canvas = await loadHtml2Canvas()
|
||||
const canvas = await html2canvas(document.body, {
|
||||
backgroundColor: '#121212',
|
||||
scale: 1,
|
||||
logging: false,
|
||||
useCORS: true,
|
||||
})
|
||||
return canvas.toDataURL('image/png')
|
||||
},
|
||||
|
||||
/**
|
||||
* Capture screenshot and send to Rust backend
|
||||
*/
|
||||
async captureAndSend(): Promise<void> {
|
||||
try {
|
||||
const dataUrl = await this.screenshot()
|
||||
await invoke('automation_screenshot_data', { data: dataUrl })
|
||||
console.log('[Automation] Screenshot sent to backend')
|
||||
} catch (error) {
|
||||
console.error('[Automation] Screenshot capture failed:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Expose globally for Rust to call via eval
|
||||
window.__TAURI_AUTOMATION__ = service
|
||||
|
||||
console.log('[Automation] Ready. HTTP API available at http://localhost:9876')
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load html2canvas from CDN
|
||||
*/
|
||||
async function loadHtml2Canvas(): Promise<typeof import('html2canvas').default> {
|
||||
if ((window as unknown as { html2canvas?: unknown }).html2canvas) {
|
||||
return (window as unknown as { html2canvas: typeof import('html2canvas').default }).html2canvas
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'
|
||||
script.onload = () => {
|
||||
const h2c = (window as unknown as { html2canvas: typeof import('html2canvas').default }).html2canvas
|
||||
if (h2c) {
|
||||
resolve(h2c)
|
||||
} else {
|
||||
reject(new Error('html2canvas failed to load'))
|
||||
}
|
||||
}
|
||||
script.onerror = () => reject(new Error('Failed to load html2canvas'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
export { commands } from './commands'
|
||||
@@ -0,0 +1,52 @@
|
||||
// Console logger for Tauri - forwards console.log to Rust for file logging
|
||||
// This allows us to capture frontend logs when running as a desktop app
|
||||
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
// Check if running in Tauri environment
|
||||
function isTauri (): boolean {
|
||||
return typeof window !== 'undefined' && '__TAURI__' in window
|
||||
}
|
||||
|
||||
export function setupConsoleLogger () {
|
||||
// Only activate in Tauri environment
|
||||
if (!isTauri()) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalLog = console.log
|
||||
const originalWarn = console.warn
|
||||
const originalError = console.error
|
||||
|
||||
const sendToRust = (level: string, args: unknown[]) => {
|
||||
const message = args.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg)
|
||||
} catch {
|
||||
return String(arg)
|
||||
}
|
||||
}
|
||||
return String(arg)
|
||||
}).join(' ')
|
||||
|
||||
invoke('log_to_stdout', { level, message }).catch(() => {
|
||||
// Silently fail if Rust command not available
|
||||
})
|
||||
}
|
||||
|
||||
console.log = (...args: unknown[]) => {
|
||||
originalLog.apply(console, args)
|
||||
sendToRust('INFO', args)
|
||||
}
|
||||
|
||||
console.warn = (...args: unknown[]) => {
|
||||
originalWarn.apply(console, args)
|
||||
sendToRust('WARN', args)
|
||||
}
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
originalError.apply(console, args)
|
||||
sendToRust('ERROR', args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// App-specific styles
|
||||
// Add your custom styles here
|
||||
@@ -0,0 +1,14 @@
|
||||
// Quasar SCSS variables
|
||||
// Customize colors, breakpoints, etc.
|
||||
|
||||
$primary : #1976D2;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
$dark : #1D1D1D;
|
||||
$dark-page : #121212;
|
||||
|
||||
$positive : #21BA45;
|
||||
$negative : #C10015;
|
||||
$info : #31CCEC;
|
||||
$warning : #F2C037;
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
icon="menu"
|
||||
aria-label="Menu"
|
||||
@click="toggleLeftDrawer"
|
||||
/>
|
||||
<q-toolbar-title>
|
||||
Tauri Starter
|
||||
</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
bordered
|
||||
>
|
||||
<q-list>
|
||||
<q-item-label header>
|
||||
Navigation
|
||||
</q-item-label>
|
||||
|
||||
<q-item clickable v-ripple to="/">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="home" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Home
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const leftDrawerOpen = ref(false)
|
||||
|
||||
function toggleLeftDrawer () {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { Quasar, Notify, Dialog, Loading } from 'quasar'
|
||||
import router from './router'
|
||||
import { setupConsoleLogger } from './boot/console-logger'
|
||||
|
||||
// Import Quasar css
|
||||
import '@quasar/extras/roboto-font/roboto-font.css'
|
||||
import '@quasar/extras/material-icons/material-icons.css'
|
||||
import 'quasar/src/css/index.sass'
|
||||
|
||||
// Import app styles
|
||||
import './css/app.scss'
|
||||
|
||||
// Import root component
|
||||
import App from './App.vue'
|
||||
|
||||
// Setup console logger for Tauri
|
||||
setupConsoleLogger()
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Quasar, {
|
||||
plugins: {
|
||||
Notify,
|
||||
Dialog,
|
||||
Loading
|
||||
},
|
||||
config: {
|
||||
dark: 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
// Mount the app
|
||||
app.mount('#q-app')
|
||||
|
||||
// Expose Vue app for automation
|
||||
;(window as unknown as { __VUE_APP__: typeof app }).__VUE_APP__ = app
|
||||
|
||||
// Initialize automation service after app is mounted
|
||||
setTimeout(() => {
|
||||
import('./automation').then(({ initAutomation }) => {
|
||||
initAutomation()
|
||||
}).catch(err => {
|
||||
console.warn('[Automation] Failed to load:', err)
|
||||
})
|
||||
}, 100)
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-h1 text-weight-thin q-mb-md">404</h1>
|
||||
<p class="text-grey-6 q-mb-lg">Page not found</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Go Home"
|
||||
to="/"
|
||||
/>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 404 page
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<div class="text-center">
|
||||
<h4 class="q-mb-md">Tauri Starter</h4>
|
||||
<p class="text-grey-6">Quasar + Vite + Tauri template</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Get Started"
|
||||
class="q-mt-md"
|
||||
@click="showMessage"
|
||||
/>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
onMounted(() => {
|
||||
console.log('App initialized')
|
||||
})
|
||||
|
||||
function showMessage () {
|
||||
console.log('Get Started clicked')
|
||||
$q.notify({
|
||||
message: 'Ready to build!',
|
||||
color: 'positive',
|
||||
icon: 'check'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('pages/IndexPage.vue') }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useExampleStore = defineStore('example', () => {
|
||||
// State
|
||||
const count = ref(0)
|
||||
|
||||
// Getters
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
|
||||
// Actions
|
||||
function increment () {
|
||||
count.value++
|
||||
}
|
||||
|
||||
function reset () {
|
||||
count.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
doubleCount,
|
||||
increment,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": ["src/*"],
|
||||
"components/*": ["src/components/*"],
|
||||
"layouts/*": ["src/layouts/*"],
|
||||
"pages/*": ["src/pages/*"],
|
||||
"stores/*": ["src/stores/*"],
|
||||
"boot/*": ["src/boot/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "dist", "src-tauri"]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls }
|
||||
}),
|
||||
quasar({
|
||||
sassVariables: 'src/css/quasar-variables.scss'
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
src: fileURLToPath(new URL('./src', import.meta.url)),
|
||||
components: fileURLToPath(new URL('./src/components', import.meta.url)),
|
||||
layouts: fileURLToPath(new URL('./src/layouts', import.meta.url)),
|
||||
pages: fileURLToPath(new URL('./src/pages', import.meta.url)),
|
||||
stores: fileURLToPath(new URL('./src/stores', import.meta.url)),
|
||||
boot: fileURLToPath(new URL('./src/boot', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 9000,
|
||||
strictPort: false
|
||||
},
|
||||
clearScreen: false,
|
||||
envPrefix: ['VITE_', 'TAURI_']
|
||||
})
|
||||