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,36 @@
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Nhost secrets
|
||||
app/nhost/.secrets
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
# Note: Never ignore lock files (package-lock.json, yarn.lock, pnpm-lock.yaml)
|
||||
# They are required for reproducible builds
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
.quasar/
|
||||
build/
|
||||
|
||||
# Tauri / Rust
|
||||
app/src-tauri/target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
@@ -0,0 +1,537 @@
|
||||
# Project Guidelines
|
||||
|
||||
## Protecting This File
|
||||
|
||||
**IMPORTANT**: This file (CLAUDE.md) can be lost during branch operations!
|
||||
|
||||
Git doesn't protect untracked/ignored files during `git checkout` or branch switches. Before major git operations:
|
||||
```bash
|
||||
cp CLAUDE.md CLAUDE.md.backup
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
This project follows a standardized structure for Tauri desktop apps.
|
||||
|
||||
```
|
||||
ProjectRoot/
|
||||
├── CLAUDE.md # AI assistant guidelines
|
||||
├── README.md # Project documentation
|
||||
├── build_spec.md # Full project specification
|
||||
├── build_plan.md # Phased task checklist
|
||||
├── .gitignore
|
||||
├── docs/ # Technical documentation
|
||||
│ ├── README.md # Docs index with table of contents
|
||||
│ ├── getting-started.md
|
||||
│ ├── architecture.md
|
||||
│ ├── development.md
|
||||
│ ├── deployment.md
|
||||
│ └── feature-*.md # Feature docs (added per phase)
|
||||
└── app/ # ALL project code lives here
|
||||
├── build.sh # Build script
|
||||
├── package.json
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
├── index.html
|
||||
├── src/
|
||||
│ ├── main.ts
|
||||
│ ├── App.vue
|
||||
│ ├── boot/
|
||||
│ │ └── console-logger.ts
|
||||
│ ├── components/
|
||||
│ ├── composables/
|
||||
│ ├── css/
|
||||
│ ├── layouts/
|
||||
│ ├── pages/
|
||||
│ ├── router/
|
||||
│ ├── stores/
|
||||
│ ├── services/
|
||||
│ └── types/
|
||||
└── src-tauri/
|
||||
├── Cargo.toml
|
||||
├── tauri.conf.json
|
||||
├── icons/
|
||||
└── src/
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
## TeamIDE Project Management
|
||||
|
||||
If this project was scaffolded from Ideation, `.teamide/` PM files (plans, tasks, project.yml) should already exist — generated from `build_plan.md` during scaffolding. See the Ideation workspace's `claude.md` for the PM generation workflow and format details.
|
||||
|
||||
## Terminology
|
||||
|
||||
- **"build spec"** or **"spec"** refers to `build_spec.md` - the full project description
|
||||
- **"build plan"** or **"plan"** refers to `build_plan.md` - the phased task checklist
|
||||
|
||||
## Onboarding Questions (Ask at Project Start)
|
||||
|
||||
At the start of every new project conversation, gather these details before writing any code:
|
||||
|
||||
1. **Brand colors & fonts** - Primary, secondary, accent colors. Typography preferences. Any existing brand guidelines or color palette?
|
||||
2. **App personality & tone** - Formal or casual? Dark or light default theme? Dense or spacious layout? Any design inspiration or reference apps?
|
||||
3. **Target platform** - Desktop-first, mobile-first, or responsive? Specific OS targets (macOS, Windows, Linux)? Minimum screen sizes?
|
||||
4. **Auth requirements** - Login method (email/password, OAuth, magic link)? User roles and permissions? Public or private app?
|
||||
|
||||
Don't assume defaults for any of these. Ask, get answers, then document them in the Project-Specific Configuration section at the bottom of this file.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: Quasar Framework (Vue 3) + Vite
|
||||
- **Desktop**: Tauri (Rust) - Cross-platform builds (macOS, Windows, Linux)
|
||||
- **State**: Pinia
|
||||
- **Routing**: Vue Router
|
||||
- **Backend** (optional): Nhost (self-hosted or cloud)
|
||||
- PostgreSQL - Database
|
||||
- Hasura - GraphQL API
|
||||
- Auth - Authentication service
|
||||
- Storage - File storage
|
||||
- Functions - Serverless functions (Node.js)
|
||||
|
||||
## Testing Changes (CRITICAL)
|
||||
|
||||
**After every code editing session, you MUST rebuild the app so the user can test your changes.**
|
||||
|
||||
```bash
|
||||
cd app
|
||||
./build.sh -dev -open
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
- This uses a **build-and-run** approach, not hot reload
|
||||
- Your code changes are NOT visible until you rebuild
|
||||
- Always rebuild before asking the user to test
|
||||
- If the user reports issues, check the logs first: `./build.sh logs`
|
||||
|
||||
## Build Script
|
||||
|
||||
**Always use `build.sh` for building the app.** This ensures consistent builds and logging for collaborative debugging.
|
||||
|
||||
```bash
|
||||
cd app
|
||||
|
||||
./build.sh -dev -open # Build dev and open app
|
||||
./build.sh -prod # Build prod to build/prod/
|
||||
./build.sh -install # Build prod and open Finder for drag-to-Applications
|
||||
./build.sh logs # View latest console output (last 100 lines)
|
||||
./build.sh clean # Clean build artifacts (preserves logs)
|
||||
./build.sh clean-logs # Remove all logs
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `-dev` | Development build. Bundle ID: `com.[app].dev` |
|
||||
| `-prod` | Production build. Builds to `build/prod/`. |
|
||||
| `-install` | Build prod and open Finder for drag-to-Applications. |
|
||||
| `-open` | Open the app after building (use with `-dev` or `-prod`) |
|
||||
| `-secure` | Build without automation (no HTTP server on port 9876) |
|
||||
| `logs` | Show last 100 lines of console output |
|
||||
| `clean` | Remove build artifacts (preserves logs) |
|
||||
| `clean-logs` | Remove all log files |
|
||||
|
||||
### Why Build-and-Run (No Hot Reload)
|
||||
|
||||
This project uses a **build-and-run** approach instead of hot reload:
|
||||
|
||||
| Aspect | Hot Reload | Build-and-Run |
|
||||
|--------|------------|---------------|
|
||||
| Speed | Instant updates | Full rebuild required |
|
||||
| Dev/Prod parity | Different environments | Identical to production |
|
||||
| Debugging | Dev-only issues possible | What you test = what ships |
|
||||
| Logging | Console only | Full logs to files |
|
||||
|
||||
The tradeoff is slower iteration, but you always test exactly what will ship.
|
||||
|
||||
### What the Build Does
|
||||
|
||||
1. **Build with Tauri** - `npx tauri build` does all of the following:
|
||||
- Build frontend (`npm run build` → `dist/`)
|
||||
- Build Rust binary with embedded frontend
|
||||
- Create .app bundle in `src-tauri/target/release/bundle/macos/`
|
||||
2. **Copy to build folder** - Copies bundle to `build/dev/` or `build/prod/`
|
||||
3. **Update Info.plist** - Sets correct bundle ID for dev/prod
|
||||
4. **Install (prod only)** - Copies to `~/Applications`
|
||||
|
||||
**Important**: Using `npx tauri build` ensures the frontend is properly embedded in the binary. Manual `cargo build --release` does NOT embed the frontend.
|
||||
|
||||
### Build Output
|
||||
|
||||
Builds are organized in separate folders:
|
||||
|
||||
```
|
||||
app/build/
|
||||
├── dev/
|
||||
│ └── AppName.app # Dev build (bundle ID: .dev)
|
||||
├── prod/
|
||||
│ └── AppName.app # Prod build
|
||||
├── logs/
|
||||
│ ├── build_dev_*.log # Build output logs
|
||||
│ └── build_latest.log # Symlink to latest
|
||||
└── console/
|
||||
├── console_*.log # App runtime console output
|
||||
└── console_latest.log # Symlink to latest
|
||||
```
|
||||
|
||||
Dev and prod have different bundle IDs, so both can run simultaneously.
|
||||
|
||||
### Logging
|
||||
|
||||
All build output and app console output is logged:
|
||||
|
||||
- **Build logs**: `build/logs/build_latest.log` - compilation output, errors
|
||||
- **Console logs**: `build/console/console_latest.log` - runtime `console.log()`, errors
|
||||
|
||||
When debugging, run `./build.sh logs` to see the latest console output.
|
||||
|
||||
### Console Logging (Already Configured)
|
||||
|
||||
This template includes console logging out of the box. Frontend `console.log()` calls are forwarded to Rust and captured in log files.
|
||||
|
||||
**How it works:**
|
||||
```
|
||||
console.log() → console-logger.ts → invoke('log_to_stdout') → Rust println!() → stdout → build.sh logs
|
||||
```
|
||||
|
||||
## Automation Plugin (Already Configured)
|
||||
|
||||
This template includes an automation HTTP server for external testing and AI-assisted development.
|
||||
|
||||
**How it works:**
|
||||
```
|
||||
External Tool (curl) → HTTP Server (port 9876) → Rust → JavaScript → DOM
|
||||
```
|
||||
|
||||
### Automation API
|
||||
|
||||
When the app is running, these endpoints are available:
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:9876/automation/health
|
||||
|
||||
# Execute a command
|
||||
curl -X POST http://localhost:9876/automation/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command": "click", "args": {"selector": "#my-button"}}'
|
||||
|
||||
# Take a screenshot
|
||||
curl http://localhost:9876/automation/screenshot --output screenshot.png
|
||||
```
|
||||
|
||||
### Available Commands
|
||||
|
||||
| Command | Args | Description |
|
||||
|---------|------|-------------|
|
||||
| `navigate` | `{path}` | Navigate to route |
|
||||
| `click` | `{selector}` | Click element |
|
||||
| `type` | `{selector, text}` | Type into input |
|
||||
| `getText` | `{selector}` | Get element text |
|
||||
| `getValue` | `{selector}` | Get input value |
|
||||
| `exists` | `{selector}` | Check if element exists |
|
||||
| `waitFor` | `{selector, timeout?}` | Wait for element |
|
||||
| `getUrl` | `{}` | Get current URL |
|
||||
| `screenshot` | - | Capture page (via endpoint) |
|
||||
|
||||
Full command list in `src/automation/README.md`.
|
||||
|
||||
### Disabling Automation
|
||||
|
||||
**Option 1: Build flag (recommended)**
|
||||
|
||||
Use the `-secure` flag to build without automation:
|
||||
|
||||
```bash
|
||||
./build.sh -prod -secure # Production build without automation
|
||||
```
|
||||
|
||||
**Option 2: Permanently disable**
|
||||
|
||||
Remove `automation` from the default features in `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
default = ["custom-protocol"] # Remove "automation"
|
||||
```
|
||||
|
||||
### When to Clean
|
||||
|
||||
Run `./build.sh clean` when:
|
||||
- Switching between major dependency versions
|
||||
- Build errors reference stale/cached files
|
||||
- Rust compilation issues after Tauri config changes
|
||||
- Before creating a fresh production build
|
||||
|
||||
## Node.js Environment
|
||||
|
||||
**Node.js version**: Always match Debian stable (currently Node.js 18.x for Debian 12 Bookworm)
|
||||
|
||||
Check current Debian stable Node version: https://packages.debian.org/stable/nodejs
|
||||
|
||||
**Package manager**: Use npm. Commit lock files for reproducible builds.
|
||||
|
||||
Rules:
|
||||
- Always commit `package-lock.json` - required for deterministic installs
|
||||
- Never ignore lock files (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`)
|
||||
- Use `npm install` to install dependencies
|
||||
- Use `npm run <script>` to run package scripts
|
||||
|
||||
## Communication Rules
|
||||
|
||||
**Always explain before acting.** Before running any command, SSH, database query, or making changes:
|
||||
- Give a short, concise explanation of WHAT you're about to do and WHY
|
||||
- Wait for approval on anything that modifies production data or server state
|
||||
- Don't chain multiple server commands without explaining each step
|
||||
|
||||
## Database Rules (If Using Nhost Backend)
|
||||
|
||||
**NEVER make direct database changes via Hasura MCP or Hasura Console.**
|
||||
|
||||
All database changes MUST be done through migrations so they:
|
||||
1. Are tracked in version control
|
||||
2. Persist across restarts (`nhost down` / `nhost up`)
|
||||
3. Can be reproduced in production
|
||||
|
||||
### Hasura MCP Usage
|
||||
|
||||
- **READ-ONLY**: Use Hasura MCP only to inspect schema, check what tables exist, verify tracking status
|
||||
- **NO MUTATIONS**: Never use it to create tables, add columns, track tables, or modify permissions
|
||||
- **MIGRATIONS ONLY**: All schema changes go in `app/nhost/migrations/default/`
|
||||
- **METADATA ONLY**: All Hasura config (tracking, permissions, relationships) go in `app/nhost/metadata/`
|
||||
|
||||
### How to Make Database Changes
|
||||
|
||||
1. Write SQL migration in `app/nhost/migrations/default/<N>_<name>/up.sql` (N = next version number)
|
||||
2. Write rollback in `app/nhost/migrations/default/<N>_<name>/down.sql`
|
||||
3. Add Hasura metadata YAML in `app/nhost/metadata/databases/default/tables/`
|
||||
4. Restart dev environment to apply migrations
|
||||
5. Commit and push
|
||||
|
||||
### Hasura Role Permissions (CRITICAL)
|
||||
|
||||
**NEVER define permissions for the `admin` role in Hasura metadata.**
|
||||
|
||||
Hasura reserves `admin` as a special role with **full unrestricted access**. Defining explicit permissions for `admin` causes metadata errors.
|
||||
|
||||
Define permissions only for `user`, `public`, and custom roles.
|
||||
|
||||
## Development Approach
|
||||
|
||||
**Integrate-as-you-go** - Connect real backend services as you build. Don't build elaborate mock systems.
|
||||
|
||||
## Persistence Rules (CRITICAL)
|
||||
|
||||
**All persistence requires an explicit user action. No exceptions.**
|
||||
|
||||
Never auto-save. Never sync state in the background. Never write to the database on a timer or watcher. Every single write to a database, file, or storage must be traceable to a deliberate user action.
|
||||
|
||||
### The Rule
|
||||
|
||||
Data is only persisted when the user deliberately triggers it — clicking "Save", submitting a form, confirming a dialog, toggling a setting, etc.
|
||||
|
||||
### WRONG — Never Do This
|
||||
|
||||
- Auto-saving form fields as the user types
|
||||
- Syncing component state to the database on mount/unmount
|
||||
- Watchers that write to storage when reactive data changes
|
||||
- Debounced save on input change
|
||||
- Persisting UI state automatically (sidebar position, tab selection, scroll position)
|
||||
- Writing to the database during navigation or route changes
|
||||
- Background sync intervals or polling that writes data
|
||||
|
||||
### RIGHT — Always Do This
|
||||
|
||||
- User clicks "Save" → write to database
|
||||
- User submits a form → persist the data
|
||||
- User explicitly toggles a setting → save that setting
|
||||
- User clicks "Delete" → confirm, then remove from database
|
||||
- User drags an item to reorder → save on drop (explicit action)
|
||||
|
||||
### Why This Matters
|
||||
|
||||
Auto-save creates unpredictable behavior, race conditions, and persists data the user didn't intend to save. Every write should be traceable to a conscious user decision. If you can't point to the button or action the user took to trigger the save, it shouldn't be saving.
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```
|
||||
User Action → Service Call → Database Write → Confirm Success to User
|
||||
```
|
||||
|
||||
1. User triggers action (click, submit, confirm)
|
||||
2. Call the appropriate service/API
|
||||
3. Write to database
|
||||
4. Show success feedback (Quasar Notify or UI update)
|
||||
5. If it fails, show an error — don't silently retry
|
||||
|
||||
## Code Quality
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Use TypeScript strict mode
|
||||
- Define interfaces/types for all data structures in `src/types/`
|
||||
- No `any` types unless absolutely unavoidable — use `unknown` and narrow instead
|
||||
- Type all function parameters and return values
|
||||
|
||||
### Vue / Quasar Conventions
|
||||
|
||||
- Use Composition API with `<script setup lang="ts">` for all components
|
||||
- One component per file, named in PascalCase (e.g., `UserProfile.vue`)
|
||||
- Composables go in `src/composables/`, prefixed with `use` (e.g., `useAuth.ts`)
|
||||
- Keep components focused — if it's doing too much, split it
|
||||
- Props down, events up — don't mutate props, emit events to parent
|
||||
|
||||
### Naming
|
||||
|
||||
- **Files**: PascalCase for components (`UserCard.vue`), camelCase for everything else (`userService.ts`)
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE for true constants, camelCase for derived/computed values
|
||||
- **Types/Interfaces**: PascalCase, no `I` prefix (use `User` not `IUser`)
|
||||
- **CSS classes**: kebab-case
|
||||
|
||||
### General
|
||||
|
||||
- No magic numbers or strings — use named constants
|
||||
- Keep functions short and single-purpose
|
||||
- Colocate related code — don't scatter logic across files unnecessarily
|
||||
- Delete dead code, don't comment it out
|
||||
|
||||
## Error Handling
|
||||
|
||||
### User-Facing Errors
|
||||
|
||||
- Always show human-readable messages — never expose raw error objects, stack traces, or technical details to the user
|
||||
- Use Quasar's `Notify` plugin for transient feedback (success, warning, error)
|
||||
- Use inline error messages for form validation
|
||||
|
||||
### Technical Errors
|
||||
|
||||
- Log technical details to console for debugging (`console.error` with context)
|
||||
- Include the operation that failed and relevant IDs in log messages
|
||||
- Never swallow errors silently — if you catch it, either handle it or log it
|
||||
|
||||
### Pattern
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await userService.updateProfile(data)
|
||||
Notify.create({ type: 'positive', message: 'Profile updated' })
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error)
|
||||
Notify.create({ type: 'negative', message: 'Could not update profile. Try again.' })
|
||||
}
|
||||
```
|
||||
|
||||
### Where to Catch
|
||||
|
||||
- Catch at **service boundaries** (API calls, database operations), not deep in utility functions
|
||||
- Let errors propagate up to where they can be meaningfully handled
|
||||
- Don't wrap every line in try/catch — catch at the level where you can show the user feedback
|
||||
|
||||
## Documentation
|
||||
|
||||
**Every phase ends with documentation.** After completing a feature or phase:
|
||||
|
||||
1. **Create feature docs** - Copy `docs/TEMPLATE-feature.md` to `docs/feature-[name].md`
|
||||
2. **Update the index** - Add links to `docs/README.md` table of contents
|
||||
3. **Update core docs** - Keep `architecture.md`, `development.md` current
|
||||
|
||||
### Docs Structure
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/README.md` | Index with table of contents |
|
||||
| `docs/getting-started.md` | Installation & setup |
|
||||
| `docs/architecture.md` | Tech stack & design decisions |
|
||||
| `docs/development.md` | Development workflow |
|
||||
| `docs/deployment.md` | Build & deployment |
|
||||
| `docs/feature-*.md` | Feature-specific documentation |
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- `feature-[name].md` - Feature docs (e.g., `feature-authentication.md`)
|
||||
- `component-[name].md` - Component docs
|
||||
- `integration-[name].md` - Integration docs
|
||||
- `guide-[topic].md` - How-to guides
|
||||
|
||||
## Commit Rules
|
||||
|
||||
- **Never mention Claude, AI, or "Generated with Claude Code" in commits**
|
||||
- All commits are authored by the developer - no co-author tags
|
||||
- Write commit messages in first person as the developer
|
||||
- Keep commit messages concise and descriptive of the changes
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
- **main** - Production branch
|
||||
- **test** - Staging/testing branch
|
||||
|
||||
**Workflow:**
|
||||
1. Develop and test locally
|
||||
2. Push to `test` branch for staging
|
||||
3. Verify on staging
|
||||
4. Merge `test` → `main` for production
|
||||
|
||||
## Customization Checklist
|
||||
|
||||
When using this template for a new project:
|
||||
|
||||
1. Update `app/src-tauri/tauri.conf.json`:
|
||||
- `productName`
|
||||
- `identifier` (e.g., `com.yourapp.app`)
|
||||
- `title`
|
||||
|
||||
2. Update `app/package.json`:
|
||||
- `name`
|
||||
- `description`
|
||||
|
||||
3. Update `app/build.sh`:
|
||||
- `APP_NAME`
|
||||
- `BINARY_NAME` (must match productName)
|
||||
- `BUNDLE_ID_DEV`
|
||||
- `BUNDLE_ID_PROD`
|
||||
|
||||
4. Replace placeholder icons in `app/src-tauri/icons/`
|
||||
|
||||
5. Update documentation:
|
||||
- Replace `[Project Name]` in all `docs/*.md` files
|
||||
- Update `docs/architecture.md` with project-specific tech decisions
|
||||
- Update `docs/getting-started.md` with project-specific setup
|
||||
|
||||
6. Add your pages, components, and stores
|
||||
|
||||
7. Add Tauri commands to `src-tauri/src/main.rs`
|
||||
|
||||
---
|
||||
|
||||
# Project-Specific Configuration
|
||||
|
||||
<!-- Everything below this line is customized per project -->
|
||||
|
||||
## Project Name
|
||||
|
||||
[PROJECT_NAME]
|
||||
|
||||
## Description
|
||||
|
||||
[Brief description from build_spec.md]
|
||||
|
||||
## Backend (If Applicable)
|
||||
|
||||
| Service | URL |
|
||||
|---------|-----|
|
||||
| GraphQL | |
|
||||
| Auth | |
|
||||
| Storage | |
|
||||
|
||||
## Database Schema (If Applicable)
|
||||
|
||||
[Tables and relationships]
|
||||
|
||||
## TeamIDE Hosting (If Applicable)
|
||||
|
||||
- **Workspace ID**: `your-workspace-id-here`
|
||||
- Use `teamide_deploy` to deploy changes
|
||||
- Use `teamide_set_env_vars` to configure environment variables
|
||||
- Use `teamide_get_logs` to check build status
|
||||
@@ -0,0 +1,77 @@
|
||||
# TauriStarter
|
||||
|
||||
> Starter template for building desktop apps with Quasar (Vue 3), Vite, and Tauri
|
||||
|
||||
## Overview
|
||||
|
||||
TauriStarter is a ready-to-use template for cross-platform desktop applications. Combines the power of Vue 3 with Quasar's component library and Tauri's lightweight native runtime. Includes a build script for consistent dev/prod builds with logging.
|
||||
|
||||
## Features
|
||||
|
||||
- **Quasar Framework** - Vue 3 with Quasar components and utilities
|
||||
- **Tauri Runtime** - Lightweight Rust-based native app shell
|
||||
- **Build Script** - Consistent builds with dev/prod separation
|
||||
- **Console Logging** - Frontend logs captured to files for debugging
|
||||
- **Hot-Swappable Structure** - Standardized layout for component reuse
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: Quasar Framework (Vue 3) + Vite
|
||||
- **Desktop**: Tauri (Rust)
|
||||
- **State**: Pinia
|
||||
- **Routing**: Vue Router
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- Rust (for Tauri)
|
||||
- Xcode Command Line Tools (macOS)
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
cd app
|
||||
npm install
|
||||
|
||||
# Web dev server
|
||||
npm run dev
|
||||
|
||||
# Native app dev
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cd app
|
||||
|
||||
# Dev build - opens app
|
||||
./build.sh -dev -open
|
||||
|
||||
# Prod build - installs to ~/Applications
|
||||
./build.sh -prod
|
||||
|
||||
# View console logs
|
||||
./build.sh logs
|
||||
|
||||
# Clean build artifacts
|
||||
./build.sh clean
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
When using this template for a new project, update:
|
||||
|
||||
1. `app/src-tauri/tauri.conf.json` - productName, identifier, title
|
||||
2. `app/package.json` - name, description
|
||||
3. `app/build.sh` - APP_NAME, BUNDLE_ID_DEV, BUNDLE_ID_PROD
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Add screenshots to the screenshots/ folder -->
|
||||
|
||||
## Status
|
||||
|
||||
**Template** - Ready for use
|
||||
@@ -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_']
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
# Documentation
|
||||
|
||||
> Technical documentation for [Project Name]
|
||||
|
||||
## Table of Contents
|
||||
|
||||
### Getting Started
|
||||
- [Installation & Setup](./getting-started.md)
|
||||
|
||||
### Features
|
||||
<!-- Add feature docs as they're built. Format: [Feature Name](./feature-name.md) -->
|
||||
|
||||
### Architecture
|
||||
- [Tech Stack & Architecture](./architecture.md)
|
||||
|
||||
### Development
|
||||
- [Development Guide](./development.md)
|
||||
- [Build & Deployment](./deployment.md)
|
||||
|
||||
---
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Adding New Documentation
|
||||
|
||||
After completing a feature or phase, add documentation:
|
||||
|
||||
1. Create a new `.md` file in this folder using the naming convention below
|
||||
2. Add a link to the Table of Contents above
|
||||
3. Use the appropriate template as a starting point:
|
||||
- [Feature template](./TEMPLATE-feature.md) - For documenting features
|
||||
- [Build plan template](./TEMPLATE-build-plan.md) - For creating structured build plans
|
||||
|
||||
### Naming Convention
|
||||
|
||||
| Type | Pattern | Example |
|
||||
|------|---------|---------|
|
||||
| Feature docs | `feature-[name].md` | `feature-authentication.md` |
|
||||
| Component docs | `component-[name].md` | `component-sidebar.md` |
|
||||
| Integration docs | `integration-[name].md` | `integration-nhost.md` |
|
||||
| Guide docs | `guide-[topic].md` | `guide-testing.md` |
|
||||
| Build plans | `[name]-build-plan.md` | `auth-build-plan.md` |
|
||||
|
||||
### Documentation Structure
|
||||
|
||||
Each feature doc should include:
|
||||
- **Overview** - What the feature does
|
||||
- **Usage** - How to use it (with code examples)
|
||||
- **Configuration** - Available options/settings
|
||||
- **Related** - Links to related features/components
|
||||
@@ -0,0 +1,121 @@
|
||||
# [Project Name] - Build Plan
|
||||
|
||||
## Executor Config
|
||||
```yaml
|
||||
default_branch_prefix: feat/phase
|
||||
test_before_commit: true
|
||||
require_passing_tests: true
|
||||
max_test_retries: 3
|
||||
```
|
||||
|
||||
## Phase 1: [Phase Name]
|
||||
**repo**: [repository-name]
|
||||
**branch**: feat/phase-1-[slug]
|
||||
|
||||
### 1.1 [Task Name]
|
||||
- **task**: [Description of what to do. Be specific and actionable.
|
||||
Include context about existing code to use or patterns to follow.]
|
||||
- **test**: `[command to verify success]`
|
||||
- **commit**: "[Commit message]"
|
||||
|
||||
### 1.2 [Task Name]
|
||||
- **task**: [Description]
|
||||
- **test**: `[test command]`
|
||||
- **commit**: "[Commit message]"
|
||||
|
||||
### 1.3 Documentation
|
||||
- **task**: Update documentation for phase 1 changes.
|
||||
Add any new files to docs/ folder.
|
||||
- **test**: none
|
||||
- **commit**: "Document phase 1"
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: [Phase Name]
|
||||
**repo**: [repository-name]
|
||||
**branch**: feat/phase-2-[slug]
|
||||
**depends_on**: Phase 1
|
||||
|
||||
### 2.1 [Task Name]
|
||||
- **task**: [Description]
|
||||
- **test**: `[test command]`
|
||||
- **commit**: "[Commit message]"
|
||||
|
||||
### 2.2 Documentation
|
||||
- **task**: Update documentation for phase 2 changes.
|
||||
- **test**: none
|
||||
- **commit**: "Document phase 2"
|
||||
|
||||
---
|
||||
|
||||
## Current Status
|
||||
|
||||
**Phase**: 1 - [Phase Name]
|
||||
**Task**: [Current task]
|
||||
|
||||
---
|
||||
|
||||
## Template Instructions
|
||||
|
||||
> Remove this section when using this template
|
||||
|
||||
### Plan Format
|
||||
|
||||
Each plan consists of:
|
||||
|
||||
1. **Executor Config** (optional) - YAML block with settings
|
||||
2. **Phases** - Major milestones (`## Phase N: Name`)
|
||||
3. **Tasks** - Individual work items (`### N.N Task Name`)
|
||||
|
||||
### Phase Metadata
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `**repo**` | Yes | Repository to work in |
|
||||
| `**branch**` | No | Branch to create (default: `{prefix}-{N}`) |
|
||||
| `**depends_on**` | No | Phase that must complete first |
|
||||
|
||||
### Task Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `- **task**` | Yes | What the agent should do |
|
||||
| `- **test**` | No | Command to verify (`none` to skip) |
|
||||
| `- **commit**` | No | Commit message (agent decides if omitted) |
|
||||
|
||||
### Writing Good Tasks
|
||||
|
||||
**Be specific:**
|
||||
```markdown
|
||||
# Bad
|
||||
- **task**: Add authentication
|
||||
|
||||
# Good
|
||||
- **task**: Add JWT authentication with /api/login and /api/logout endpoints.
|
||||
Use bcrypt for password hashing. Store tokens in httpOnly cookies.
|
||||
```
|
||||
|
||||
**Include context:**
|
||||
```markdown
|
||||
- **task**: Create dashboard component at /dashboard showing:
|
||||
- User's recent activity (last 10 items)
|
||||
- Account settings link
|
||||
Use the existing UserCard component for the header.
|
||||
```
|
||||
|
||||
**Use testable tests:**
|
||||
```markdown
|
||||
# Bad
|
||||
- **test**: Check that it works
|
||||
|
||||
# Good
|
||||
- **test**: `pnpm test && pnpm build`
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
1. **One thing per task** - Don't combine unrelated work
|
||||
2. **Tests are checkpoints** - Use them to verify progress
|
||||
3. **Commits are savepoints** - Small, focused commits
|
||||
4. **Phases are milestones** - Each should be independently useful
|
||||
5. **End phases with docs** - Document as you go
|
||||
@@ -0,0 +1,60 @@
|
||||
# Feature Name
|
||||
|
||||
> Brief one-line description of what this feature does
|
||||
|
||||
## Overview
|
||||
|
||||
Explain what this feature does and why it exists. Include:
|
||||
- The problem it solves
|
||||
- Key capabilities
|
||||
- When to use it
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```typescript
|
||||
// Code example showing basic usage
|
||||
```
|
||||
|
||||
### Advanced Example
|
||||
|
||||
```typescript
|
||||
// Code example showing advanced/common patterns
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `option1` | `string` | `""` | Description of option |
|
||||
| `option2` | `boolean` | `false` | Description of option |
|
||||
|
||||
## Components
|
||||
|
||||
List the main components involved in this feature:
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `ComponentName` | `src/components/ComponentName.vue` | Brief description |
|
||||
|
||||
## Data Flow
|
||||
|
||||
Describe how data flows through this feature (if applicable):
|
||||
|
||||
```
|
||||
User Action → Component → Store → API → Database
|
||||
↑ ↓
|
||||
UI Update ← ← ← ← Response
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Related Feature](./feature-related.md)
|
||||
- [Related Component](./component-related.md)
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| YYYY-MM-DD | Initial implementation |
|
||||
@@ -0,0 +1,61 @@
|
||||
# Architecture
|
||||
|
||||
> Technical architecture and design decisions for [Project Name]
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology | Purpose |
|
||||
|-------|------------|---------|
|
||||
| Frontend | Vue 3 + Quasar | UI framework |
|
||||
| State | Pinia | State management |
|
||||
| Routing | Vue Router | Client-side routing |
|
||||
| Desktop | Tauri | Native desktop wrapper |
|
||||
| Backend | Nhost | Auth, database, storage |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── src/
|
||||
│ ├── boot/ # Initialization scripts
|
||||
│ ├── components/ # Reusable Vue components
|
||||
│ ├── composables/ # Vue composables (shared logic)
|
||||
│ ├── layouts/ # Page layouts
|
||||
│ ├── pages/ # Route pages
|
||||
│ ├── router/ # Route definitions
|
||||
│ ├── stores/ # Pinia stores
|
||||
│ ├── services/ # API services
|
||||
│ └── types/ # TypeScript types
|
||||
├── src-tauri/ # Tauri/Rust code
|
||||
└── build.sh # Build script
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### [Decision 1]
|
||||
|
||||
**Context**: [Why this decision was needed]
|
||||
|
||||
**Decision**: [What was decided]
|
||||
|
||||
**Consequences**: [Trade-offs and implications]
|
||||
|
||||
### [Decision 2]
|
||||
|
||||
...
|
||||
|
||||
## Data Model
|
||||
|
||||
[Describe main entities and relationships]
|
||||
|
||||
```
|
||||
Entity1 ──────< Entity2
|
||||
│
|
||||
└──────< Entity3
|
||||
```
|
||||
|
||||
## External Integrations
|
||||
|
||||
| Service | Purpose | Documentation |
|
||||
|---------|---------|---------------|
|
||||
| [Service] | [Purpose] | [Link] |
|
||||
@@ -0,0 +1,78 @@
|
||||
# Build & Deployment
|
||||
|
||||
> Build process and deployment instructions for [Project Name]
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
cd app
|
||||
|
||||
# Development build
|
||||
./build.sh -dev -open
|
||||
|
||||
# Production build (installs to ~/Applications)
|
||||
./build.sh -prod
|
||||
|
||||
# Clean build artifacts
|
||||
./build.sh clean
|
||||
```
|
||||
|
||||
## Build Output
|
||||
|
||||
```
|
||||
app/build/
|
||||
├── dev/
|
||||
│ └── [AppName].app # Development build
|
||||
├── prod/
|
||||
│ └── [AppName].app # Production build
|
||||
├── logs/
|
||||
│ └── build_latest.log # Build output
|
||||
└── console/
|
||||
└── console_latest.log # Runtime logs
|
||||
```
|
||||
|
||||
## Production Build
|
||||
|
||||
### macOS
|
||||
|
||||
1. Build the production version:
|
||||
```bash
|
||||
./build.sh -prod
|
||||
```
|
||||
|
||||
2. The app is automatically installed to `~/Applications/`
|
||||
|
||||
### Distribution
|
||||
|
||||
[Add distribution instructions - notarization, signing, etc.]
|
||||
|
||||
## TeamIDE Deployment
|
||||
|
||||
If using TeamIDE for hosting:
|
||||
|
||||
```bash
|
||||
# Deploy changes
|
||||
teamide_deploy
|
||||
|
||||
# Set environment variables
|
||||
teamide_set_env_vars
|
||||
|
||||
# Check logs
|
||||
teamide_get_logs
|
||||
```
|
||||
|
||||
**Workspace ID**: `[your-workspace-id]`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required for Production
|
||||
|
||||
```env
|
||||
# List production environment variables
|
||||
```
|
||||
|
||||
### Optional
|
||||
|
||||
```env
|
||||
# List optional environment variables
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# Development Guide
|
||||
|
||||
> Development workflow and practices for [Project Name]
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Build and Run
|
||||
|
||||
This project uses a **build-and-run** approach (not hot reload):
|
||||
|
||||
```bash
|
||||
cd app
|
||||
./build.sh -dev -open # Build and launch
|
||||
./build.sh logs # View console output
|
||||
```
|
||||
|
||||
### Why Build-and-Run?
|
||||
|
||||
- **Dev = Prod** - What you test is what ships
|
||||
- **Full logging** - Console output captured to files
|
||||
- **No dev-only bugs** - Identical to production environment
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. Create components in `src/components/`
|
||||
2. Add pages in `src/pages/`
|
||||
3. Register routes in `src/router/`
|
||||
4. Add state management in `src/stores/`
|
||||
5. **Rebuild to test**: `./build.sh -dev -open`
|
||||
6. **Document the feature** in `docs/`
|
||||
|
||||
### Adding a Tauri Command
|
||||
|
||||
1. Add the command in `src-tauri/src/main.rs`
|
||||
2. Register it in the `invoke_handler`
|
||||
3. Call from frontend with `invoke('command_name', { args })`
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logs
|
||||
|
||||
```bash
|
||||
./build.sh logs # Last 100 lines
|
||||
cat build/console/console_latest.log # Full log
|
||||
```
|
||||
|
||||
### Build Errors
|
||||
|
||||
```bash
|
||||
cat build/logs/build_latest.log
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Build fails with cache errors | `./build.sh clean && ./build.sh -dev -open` |
|
||||
| Changes not appearing | Rebuild - this is build-and-run, not hot reload |
|
||||
| Console logs missing | Check `console-logger.ts` is in boot files |
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Components**: PascalCase (`UserProfile.vue`)
|
||||
- **Composables**: camelCase with `use` prefix (`useAuth.ts`)
|
||||
- **Stores**: camelCase (`userStore.ts`)
|
||||
- **Types**: PascalCase (`UserProfile.ts`)
|
||||
|
||||
## Testing
|
||||
|
||||
[Add testing instructions when implemented]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Getting Started
|
||||
|
||||
> Installation and initial setup guide for [Project Name]
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- [Additional prerequisites]
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/dcherrera/[ProjectName].git
|
||||
cd [ProjectName]/app
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Copy the environment template:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Configure required environment variables:
|
||||
```env
|
||||
# Required
|
||||
VARIABLE_NAME=value
|
||||
```
|
||||
|
||||
## Running the App
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
./build.sh -dev -open
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
./build.sh -prod
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Architecture Overview](./architecture.md) - Understand the codebase structure
|
||||
- [Development Guide](./development.md) - Learn the development workflow
|
||||