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.
This commit is contained in:
2026-05-16 10:01:25 -05:00
commit 83c52bc011
95 changed files with 13727 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -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()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

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
}
]
}
}