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 @@
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
MIT Transparency License
|
||||||
|
|
||||||
|
Copyright (c) Corey Herrera
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software without any modification,
|
||||||
|
exemption or additional provision. All operational distributions, including
|
||||||
|
executables, embedded installations, package repositories, container images,
|
||||||
|
and network deployments must preserve their users' access to original source,
|
||||||
|
documentation, build scripts, configuration, and modifications necessary to
|
||||||
|
reproduce the intended implementation, in order to promote operational
|
||||||
|
transparency for the betterment of humanity.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# TeamIDE Methodology
|
||||||
|
|
||||||
|
Project templates, coding conventions, and the build-plan → PM scaffolding rules that [TeamIDE](https://teamide.dev)'s planner agent reads at run time.
|
||||||
|
|
||||||
|
This repo is the public, version-controlled source of truth for **how a TeamIDE project gets shaped**:
|
||||||
|
|
||||||
|
- `claude.md` — the methodology guide: how to plan a project, the spec/plan/PM-file pipeline, naming conventions, the file structure every project follows, and the docs/InternalDocs split.
|
||||||
|
- `templates/coding-best-practices.md` — universal coding rules (descriptive names, small functions, library-first design, file-header convention, no auto-save).
|
||||||
|
- `templates/claude.md` — the standard project CLAUDE.md template (copied and customized into every new project).
|
||||||
|
- `templates/TauriStarter/` — a working Tauri + Quasar + Vite desktop-app starter.
|
||||||
|
- `templates/ResearchStarter/` — a hypothesis-first R&D starter.
|
||||||
|
- `templates/teamide/` — `.teamide/project.yml` and `.teamide/plans/` / `.teamide/tasks/` scaffolding.
|
||||||
|
- `templates/docs/` — documentation templates (`feature-*.md`, `getting-started.md`, etc.).
|
||||||
|
|
||||||
|
## How TeamIDE consumes this
|
||||||
|
|
||||||
|
TeamIDE clones this repo to `~/TeamIDE/.data/methodology/` on first launch and refreshes it on demand (manual button in the planner UI, or `git pull` on app start — TBD). The planner agent has **read-only** access to this directory. Writes outside its working dir are forbidden by the prompt.
|
||||||
|
|
||||||
|
If you want to customize TeamIDE's planner with your own templates or conventions, fork this repo and point TeamIDE at your fork (configurable in the planner settings).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Living document — extracted from an earlier private repo on 2026-05-16 to support TeamIDE's planner/conductor agent pipeline (see TeamIDE's `workspace-projects-agent-spec.md` §9.4).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT Transparency License](./LICENSE) — same as TeamIDE itself.
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
MIT Transparency License
|
||||||
|
|
||||||
|
Copyright (c) Corey Herrera
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software without any modification,
|
||||||
|
exemption or additional provision. All operational distributions, including
|
||||||
|
executables, embedded installations, package repositories, container images,
|
||||||
|
and network deployments must preserve their users' access to original source,
|
||||||
|
documentation, build scripts, configuration, and modifications necessary to
|
||||||
|
reproduce the intended implementation, in order to promote operational
|
||||||
|
transparency for the betterment of humanity.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Project Name
|
||||||
|
|
||||||
|
> One-line tagline describing what this project does
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Brief description of what the project does and the problem it solves. Keep this to 2-3 paragraphs max.
|
||||||
|
|
||||||
|
Explain the motivation - why does this exist? What pain point does it address?
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Feature 1** - Brief description
|
||||||
|
- **Feature 2** - Brief description
|
||||||
|
- **Feature 3** - Brief description
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: Vue 3, TypeScript, Quasar
|
||||||
|
- **Backend**: Nhost (PostgreSQL, Hasura, Auth)
|
||||||
|
- **Desktop**: Tauri (if applicable)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dcherrera/ProjectName.git
|
||||||
|
cd ProjectName/app
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Active** - Currently in development
|
||||||
|
|
||||||
|
<!-- Status options: Active | Maintained | Paused | Archived | Concept -->
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT Transparency License](LICENSE)
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Data files (large, not committed)
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
*.whl
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Model weights & checkpoints
|
||||||
|
*.pt
|
||||||
|
*.pth
|
||||||
|
*.ckpt
|
||||||
|
*.safetensors
|
||||||
|
*.bin
|
||||||
|
*.onnx
|
||||||
|
*.h5
|
||||||
|
checkpoints/
|
||||||
|
weights/
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
*.ipynb_metadata/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.a
|
||||||
|
*.out
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
# Research Project Guidelines
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ProjectName/
|
||||||
|
├── README.md # Project overview (public-facing)
|
||||||
|
├── LICENSE # MIT Transparency License
|
||||||
|
├── CLAUDE.md # AI assistant guidelines + project config
|
||||||
|
├── goals.md # Success criteria (what does "done" look like?)
|
||||||
|
├── spec.md # Technical specification (optional)
|
||||||
|
├── conclusions.md # Living findings document
|
||||||
|
├── open_questions.md # Unresolved questions tracker
|
||||||
|
├── .gitignore
|
||||||
|
├── experiments/ # Experiment code (numbered runs)
|
||||||
|
│ ├── run1-baseline.ext
|
||||||
|
│ ├── run2-variable-change.ext
|
||||||
|
│ └── ...
|
||||||
|
├── results/ # Experiment results documentation
|
||||||
|
│ ├── TEMPLATE-results.md # Template for new results docs
|
||||||
|
│ └── experiment_N_results.md # One per experiment group
|
||||||
|
├── data/ # Datasets (gitignored — large files)
|
||||||
|
├── literature/ # Paper notes & prior art
|
||||||
|
│ ├── TEMPLATE-paper.md # Template for paper reviews
|
||||||
|
│ └── paper-name.md
|
||||||
|
├── src/ # Implementation code (when research produces software)
|
||||||
|
├── tests/ # Test suite
|
||||||
|
├── docs/ # Technical documentation
|
||||||
|
│ ├── README.md # Docs index
|
||||||
|
│ ├── TEMPLATE-experiment-plan.md
|
||||||
|
│ ├── TEMPLATE-milestone.md
|
||||||
|
│ ├── getting-started.md
|
||||||
|
│ └── architecture.md
|
||||||
|
└── screenshots/ # Visuals for README / portfolio
|
||||||
|
```
|
||||||
|
|
||||||
|
## TeamIDE Project Management
|
||||||
|
|
||||||
|
If this project was scaffolded from Ideation, `.teamide/` PM files (plans, tasks, project.yml) should already exist — generated from milestone definitions during scaffolding. See the Ideation workspace's `claude.md` for the PM generation workflow and format details.
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
| Term | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| **goals.md** | What success looks like — pass/fail criteria, anti-goals, kill gates |
|
||||||
|
| **spec.md** | Technical specification for the system being built or studied (optional) |
|
||||||
|
| **conclusions.md** | Living findings — updated after each experiment phase |
|
||||||
|
| **open_questions.md** | Unresolved questions with context and leads |
|
||||||
|
| **experiment** | A numbered run in `experiments/` with a hypothesis |
|
||||||
|
| **results doc** | Structured write-up in `results/` documenting what happened |
|
||||||
|
| **milestone** | A dependency-tracked unit of work with success criteria |
|
||||||
|
|
||||||
|
## The Research Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
Vision doc --> Math/tradeoff analysis --> Milestone plan --> Implementation --> Tests pass
|
||||||
|
(speculative) (constrained) (prescriptive) (code) (done)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each stage has a different document type and level of commitment. Ideas flow left to right and can be killed at any stage.
|
||||||
|
|
||||||
|
## Document Layers
|
||||||
|
|
||||||
|
### Layer 1: Vision Documents
|
||||||
|
|
||||||
|
Thinking documents. Speculative, conversational, organized around questions and hypotheses. These live at the repo root or in topic-specific folders.
|
||||||
|
|
||||||
|
**Purpose:** Capture the frontier of thinking. These are allowed to be wrong.
|
||||||
|
|
||||||
|
**Examples:** Mathematical explorations, system architecture visions, training/data blueprints, goal documents.
|
||||||
|
|
||||||
|
### Layer 2: Specs & Plans
|
||||||
|
|
||||||
|
Prescriptive. Designed to be turned directly into implementation tasks. Live in `docs/` or as top-level files (`spec.md`).
|
||||||
|
|
||||||
|
**Purpose:** Bridge between "what if" and "what to build." Every spec includes tasks, API surface, success criteria, and test cases.
|
||||||
|
|
||||||
|
### Layer 3: Research Subprojects
|
||||||
|
|
||||||
|
Active investigations with clear experiment plans and validation gates. Each gets its own directory or subfolder within `experiments/` with a README explaining the hypothesis, experiment design, and expected outcomes.
|
||||||
|
|
||||||
|
### Layer 4: Architecture Docs
|
||||||
|
|
||||||
|
Written after implementation. Describe how things actually work, not how they were planned. Live in `docs/architecture.md`.
|
||||||
|
|
||||||
|
**Purpose:** Let a new developer understand the system without reading source code.
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### 1. Hypothesis First (Non-Negotiable)
|
||||||
|
|
||||||
|
**Every experiment requires a written hypothesis before any code runs.** No hypothesis, no experiment.
|
||||||
|
|
||||||
|
The hypothesis states what you expect to happen and why — it forces you to think before you compute.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Hypothesis
|
||||||
|
Increasing batch size from 1 to 32 will reduce gradient noise enough for
|
||||||
|
convergence, because individual gradient samples have high variance but
|
||||||
|
batch-averaged gradients should be stable.
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
Loss should decrease monotonically with batch=32, vs oscillating with batch=1.
|
||||||
|
If loss still oscillates with batch=32, the variance problem is fundamental,
|
||||||
|
not just a sample-size issue.
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a hypothesis, you don't know what the experiment is testing. You'll see a number and not know if it's good or bad.
|
||||||
|
|
||||||
|
**The scientific method, applied:**
|
||||||
|
1. **Observe** — notice something in your data, results, or literature
|
||||||
|
2. **Question** — formulate a specific question about what you observed
|
||||||
|
3. **Hypothesize** — predict an answer and state WHY you expect it
|
||||||
|
4. **Experiment** — design a run that can confirm or reject the hypothesis
|
||||||
|
5. **Analyze** — compare results to the prediction
|
||||||
|
6. **Conclude** — state what was learned, update your mental model
|
||||||
|
7. **Repeat** — the conclusion feeds the next observation
|
||||||
|
|
||||||
|
### 2. Kill Gates
|
||||||
|
|
||||||
|
Research questions get structured investigation with kill gates:
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: Literature review (what exists?)
|
||||||
|
Phase 2: Build minimal viable module
|
||||||
|
Phase 3: Integration experiments (defined matrix of configs)
|
||||||
|
Phase 4: Investigate properties (hypothesis-based, only if Phase 3 passes)
|
||||||
|
Phase 5: Optimization (only if Phase 4 is promising)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each phase is a gate. If Phase 3 fails, Phase 4 doesn't happen. This prevents sunk-cost fallacy on dead-end research.
|
||||||
|
|
||||||
|
### 3. Constraint-Driven Design
|
||||||
|
|
||||||
|
Start with hard limits. Let them drive architecture.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Constraints
|
||||||
|
- [Your hardware/budget/time limit here]
|
||||||
|
- [Your performance requirement here]
|
||||||
|
- [Your compatibility requirement here]
|
||||||
|
|
||||||
|
## Design (driven by above)
|
||||||
|
- [Architecture decision that follows from constraints]
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't design the ideal system then optimize. Design around your actual constraints from day one.
|
||||||
|
|
||||||
|
### 4. Explicit Deferral
|
||||||
|
|
||||||
|
Every milestone says what it's NOT doing and why:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Deferred:** [Feature/capability].
|
||||||
|
**Reason:** [Why it's not worth doing now].
|
||||||
|
```
|
||||||
|
|
||||||
|
Three categories for all work items:
|
||||||
|
- **Active/Concrete** — being built now, with tests
|
||||||
|
- **Outlined** — architectural sketch, no code yet
|
||||||
|
- **Deferred** — recognized need, explicitly postponed with rationale
|
||||||
|
|
||||||
|
This prevents scope creep while preserving ideas for later.
|
||||||
|
|
||||||
|
### 5. Tradeoff Tables
|
||||||
|
|
||||||
|
Every complex decision gets a table showing the option space:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| Option | Pros | Cons | When to pick |
|
||||||
|
|-----------|------------------|-----------------------|----------------|
|
||||||
|
| Option A | Simple, fast | Doesn't scale past N | Prototyping |
|
||||||
|
| Option B | Scales well | Complex to implement | Production |
|
||||||
|
| Option C | Best performance | Vendor lock-in | If budget > $X |
|
||||||
|
```
|
||||||
|
|
||||||
|
No "we chose X because it's better." Instead: "here's the tradeoff space, here's what we picked and why."
|
||||||
|
|
||||||
|
### 6. Testable Success Criteria
|
||||||
|
|
||||||
|
Every milestone ends with a concrete, falsifiable statement:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Success Criteria:**
|
||||||
|
- [System] does [specific thing] in [specific time/resource bound]
|
||||||
|
- [N] tests pass with zero warnings
|
||||||
|
- [Specific scenario] produces [specific measurable result]
|
||||||
|
```
|
||||||
|
|
||||||
|
Not "it should work" — specific numbers, specific scenarios.
|
||||||
|
|
||||||
|
### 7. Math First, Code Second
|
||||||
|
|
||||||
|
Abstract ideas get mathematical or formal framing before any code is written:
|
||||||
|
|
||||||
|
```
|
||||||
|
Idea (what if we tried X?)
|
||||||
|
--> Formalization (what does X actually mean mathematically?)
|
||||||
|
--> Existing work (has anyone done this? what libraries exist?)
|
||||||
|
--> Experiment plan (how would we test this?)
|
||||||
|
--> Validation gate (what result would make us continue vs abandon?)
|
||||||
|
```
|
||||||
|
|
||||||
|
This filters out ideas that sound good but don't survive formalization.
|
||||||
|
|
||||||
|
### 8. Documentation as Specification
|
||||||
|
|
||||||
|
Write implementation-ready docs before coding:
|
||||||
|
|
||||||
|
1. Write detailed spec in markdown (API signatures, wire formats, data structures)
|
||||||
|
2. Implement against the spec
|
||||||
|
3. Test against the spec's success criteria
|
||||||
|
4. Update the spec if reality diverges
|
||||||
|
|
||||||
|
The spec IS the source of truth. Code follows docs, not the other way around.
|
||||||
|
|
||||||
|
## Experiment Tracking
|
||||||
|
|
||||||
|
All experiments live in the `experiments/` folder and follow strict naming and documentation conventions.
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
|
||||||
|
Experiments are numbered sequentially with a descriptive kebab-case suffix:
|
||||||
|
|
||||||
|
```
|
||||||
|
experiments/
|
||||||
|
├── run1-batch-gradient-baseline.c
|
||||||
|
├── run2-per-sample-online.c
|
||||||
|
├── run3-leaky-relu-activation.c
|
||||||
|
├── run4-temperature-annealing.c
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Prefix: `run<N>-` where N is sequential (run1, run2, run3...)
|
||||||
|
- Suffix: short kebab-case description of what this run changes vs the previous
|
||||||
|
- One variable changed per run when possible (isolation)
|
||||||
|
- If a run is a variant of another, use sub-numbering: `run9b-normalization-lower-temp`
|
||||||
|
- Run numbering stays globally sequential across the whole project — don't restart at run1 inside subfolders
|
||||||
|
|
||||||
|
When a single experiment direction spawns many runs, group them in a subfolder inside `experiments/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
experiments/
|
||||||
|
├── run1-baseline.ext
|
||||||
|
├── run2-different-approach.ext
|
||||||
|
├── scaling-study/ # Subfolder for related runs
|
||||||
|
│ ├── run3-small-scale.ext
|
||||||
|
│ ├── run4-medium-scale.ext
|
||||||
|
│ └── run5-large-scale.ext
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Results Documentation
|
||||||
|
|
||||||
|
Every experiment's results are documented in `results/`. One results file per experiment or per logical group:
|
||||||
|
|
||||||
|
```
|
||||||
|
results/
|
||||||
|
├── experiment_1_results.md
|
||||||
|
├── experiment_2_results.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `results/TEMPLATE-results.md` as the starting point for each results file.
|
||||||
|
|
||||||
|
### Key Rules for Results
|
||||||
|
|
||||||
|
1. **Every run gets a row in the summary table** — even failed runs. Especially failed runs.
|
||||||
|
2. **Link to source files** — reference experiment files as `experiments/run1-descriptive-name.ext`
|
||||||
|
3. **Include raw output** — paste actual terminal output. Don't just summarize.
|
||||||
|
4. **One finding per discovery** — each finding gets its own subsection with a bold **Verdict** line
|
||||||
|
5. **Comparison tables for pivots** — when switching approaches, add a side-by-side comparison table
|
||||||
|
6. **Strikethrough completed next steps** — mark done items with `~~strikethrough~~` and add **DONE**
|
||||||
|
7. **Decision points with kill gates** — define upfront what results would make you continue, pivot, or abandon
|
||||||
|
8. **Literature context** — compare your results to published work
|
||||||
|
|
||||||
|
### Running Experiments
|
||||||
|
|
||||||
|
**All experiments MUST be run in the background** so the user can continue chatting while they execute. Never run a build or training run in the foreground — it blocks the conversation. Use background execution and check on results later.
|
||||||
|
|
||||||
|
## Milestone Format
|
||||||
|
|
||||||
|
Every implementation milestone follows this structure:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Milestone X.Y: Title — [STATUS]
|
||||||
|
|
||||||
|
**What:** One-sentence summary of the deliverable.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Task with enough detail that someone else could do it
|
||||||
|
- [ ] Another task
|
||||||
|
|
||||||
|
**Completed.** [Summary paragraph when done.]
|
||||||
|
|
||||||
|
**Deferred:** [What was intentionally left out and why.]
|
||||||
|
|
||||||
|
**New Files:**
|
||||||
|
|
||||||
|
| File | What |
|
||||||
|
|------|------|
|
||||||
|
| path/to/file | One-line description |
|
||||||
|
|
||||||
|
**API Surface:**
|
||||||
|
[Function signatures or interface definitions]
|
||||||
|
|
||||||
|
**Depends on:** Milestone X.Y-1
|
||||||
|
|
||||||
|
**Success Criteria:** [Falsifiable statement with numbers]
|
||||||
|
|
||||||
|
**Test Cases:**
|
||||||
|
1. [Specific scenario] — [expected result]
|
||||||
|
2. [Specific scenario] — [expected result]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Graphs
|
||||||
|
|
||||||
|
Milestones form a dependency chain. Visualize it:
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: 1.1 --> 1.2 --> 1.3 --> 1.4 --> 1.5
|
||||||
|
| |
|
||||||
|
Phase 2: v v
|
||||||
|
2.1 --> 2.2 --> 2.3 --> 2.4
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes it obvious what can be parallelized and what blocks what.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
1. **Hypothesis before experiment** — no code runs without a written prediction of what will happen and why. This is the most important rule.
|
||||||
|
2. **Speculation is welcome but quarantined** — vision docs are clearly marked as exploratory
|
||||||
|
3. **Implementation follows specs** — not the reverse
|
||||||
|
4. **Deferral is first-class** — saying "not now" with a reason is as important as saying "yes"
|
||||||
|
5. **Constraints are features** — limits drive creative solutions rather than being obstacles
|
||||||
|
6. **Tests are the definition of done** — not "it compiles" but "N tests pass with zero warnings"
|
||||||
|
7. **Tradeoffs are documented** — future-you can understand why decisions were made
|
||||||
|
8. **Kill gates protect your time** — research that fails a gate gets abandoned, not propped up
|
||||||
|
9. **Every experiment is tracked** — numbered, named, with raw output in results files. Failed runs are documented just as thoroughly as successes.
|
||||||
|
10. **One variable per run** — isolate changes so you know what caused the result. Sub-number variants (run9b) when testing minor tweaks.
|
||||||
|
11. **Experiments run in the background** — never block the user's ability to chat. All builds, training runs, and long-running processes execute in the background.
|
||||||
|
|
||||||
|
## Commit Rules
|
||||||
|
|
||||||
|
- Never mention Claude, AI, or generated code in commits
|
||||||
|
- Write as the developer, first person
|
||||||
|
- Concise and descriptive
|
||||||
|
|
||||||
|
## Customization Checklist
|
||||||
|
|
||||||
|
When creating a new project from this template:
|
||||||
|
|
||||||
|
- [ ] Update `README.md` with project name, problem, idea, approaches
|
||||||
|
- [ ] Fill in `goals.md` with success criteria from the project plan
|
||||||
|
- [ ] Fill in `spec.md` if the project has a technical specification (otherwise delete it)
|
||||||
|
- [ ] Set up `open_questions.md` with initial research questions
|
||||||
|
- [ ] Create initial experiment plans in `docs/` using `TEMPLATE-experiment-plan.md`
|
||||||
|
- [ ] Add project-specific entries to `.gitignore` (language-specific, data formats, etc.)
|
||||||
|
- [ ] Update the "Project-Specific Configuration" section below
|
||||||
|
- [ ] Initialize git and create GitHub repo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project-Specific Configuration
|
||||||
|
|
||||||
|
<!-- Everything below this line is customized per project -->
|
||||||
|
|
||||||
|
## Project Name
|
||||||
|
|
||||||
|
[PROJECT_NAME]
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Brief description of the research project]
|
||||||
|
|
||||||
|
## Research Question
|
||||||
|
|
||||||
|
[The core question this project investigates]
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
[Languages, frameworks, libraries being used]
|
||||||
|
|
||||||
|
## TeamIDE Workspace ID
|
||||||
|
|
||||||
|
[Workspace ID if deployed via TeamIDE]
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
MIT Transparency License
|
||||||
|
|
||||||
|
Copyright (c) Corey Herrera
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software without any modification,
|
||||||
|
exemption or additional provision. All operational distributions, including
|
||||||
|
executables, embedded installations, package repositories, container images,
|
||||||
|
and network deployments must preserve their users' access to original source,
|
||||||
|
documentation, build scripts, configuration, and modifications necessary to
|
||||||
|
reproduce the intended implementation, in order to promote operational
|
||||||
|
transparency for the betterment of humanity.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
# [Project Name]
|
||||||
|
|
||||||
|
> One-line description of what this research investigates.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
[What problem exists. Why current solutions are insufficient. 2-3 paragraphs max.]
|
||||||
|
|
||||||
|
## The Idea
|
||||||
|
|
||||||
|
[Your proposed approach. What makes it different. Include a diagram or table if helpful.]
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
[What works, what doesn't, what's next. Update this as the project progresses.]
|
||||||
|
|
||||||
|
| Component | Status | Key Metric |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| ... | ... | ... |
|
||||||
|
|
||||||
|
## Research Approaches
|
||||||
|
|
||||||
|
[List each approach being investigated with pros/cons.]
|
||||||
|
|
||||||
|
### Approach 1: [Name]
|
||||||
|
- **Pro:** ...
|
||||||
|
- **Con:** ...
|
||||||
|
- **Open question:** ...
|
||||||
|
|
||||||
|
## Experiment Plan
|
||||||
|
|
||||||
|
[Numbered list of planned experiments with goals.]
|
||||||
|
|
||||||
|
### Experiment 1: [Name]
|
||||||
|
**Goal:** [One sentence.]
|
||||||
|
- [Details]
|
||||||
|
- [Key question this answers]
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
[What results would make this research succeed vs fail.]
|
||||||
|
|
||||||
|
1. [Criterion with specific number]
|
||||||
|
2. [Criterion with specific number]
|
||||||
|
|
||||||
|
If no approach meets these criteria: [fallback plan].
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ProjectName/
|
||||||
|
├── experiments/ # Numbered experiment runs
|
||||||
|
├── results/ # Experiment results documentation
|
||||||
|
├── data/ # Datasets (gitignored)
|
||||||
|
├── literature/ # Paper notes & prior art
|
||||||
|
├── src/ # Implementation code
|
||||||
|
├── tests/ # Test suite
|
||||||
|
└── docs/ # Technical documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Paper/resource](URL) — one-line note
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT Transparency License](LICENSE)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Conclusions: [Project Name]
|
||||||
|
|
||||||
|
## Status: In Progress
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
### Finding 1: [Title]
|
||||||
|
**Discovered in:** [Experiment N, Phase X]
|
||||||
|
**Summary:** [One paragraph]
|
||||||
|
**Evidence:** See `results/experiment_N_results.md`
|
||||||
|
**Impact:** [How this changes our approach]
|
||||||
|
|
||||||
|
## What Worked
|
||||||
|
|
||||||
|
1. [Approach/technique that succeeded and why]
|
||||||
|
|
||||||
|
## What Didn't Work
|
||||||
|
|
||||||
|
1. [Approach/technique that failed and why — this is equally valuable]
|
||||||
|
|
||||||
|
## Final Recommendation
|
||||||
|
|
||||||
|
[What to do with these findings. Ship it? Pivot? Integrate into another project?]
|
||||||
|
|
||||||
|
## Open Questions Remaining
|
||||||
|
|
||||||
|
1. [Question that this research didn't answer]
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Documentation
|
||||||
|
|
||||||
|
> Technical documentation for [Project Name]
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
- [Setup Guide](./getting-started.md)
|
||||||
|
|
||||||
|
### Research
|
||||||
|
<!-- Add experiment plans and milestone docs as the project progresses -->
|
||||||
|
<!-- Example: [Experiment 1 Plan](./experiment-1-plan.md) -->
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- [Architecture](./architecture.md) — Post-implementation technical design
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
- [Experiment Plan Template](./TEMPLATE-experiment-plan.md) — For planning new experiments
|
||||||
|
- [Milestone Template](./TEMPLATE-milestone.md) — For milestone-based planning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Guidelines
|
||||||
|
|
||||||
|
### Adding New Documentation
|
||||||
|
|
||||||
|
After completing an experiment phase or milestone:
|
||||||
|
|
||||||
|
1. Create a new `.md` file in this folder
|
||||||
|
2. Add a link to the Table of Contents above
|
||||||
|
3. Use the appropriate template:
|
||||||
|
- [Experiment plan template](./TEMPLATE-experiment-plan.md) — For planning experiments
|
||||||
|
- [Milestone template](./TEMPLATE-milestone.md) — For implementation milestones
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
|
||||||
|
| Type | Pattern | Example |
|
||||||
|
|------|---------|---------|
|
||||||
|
| Experiment plans | `experiment-N-plan.md` | `experiment-1-plan.md` |
|
||||||
|
| Milestone docs | `milestone-N-name.md` | `milestone-1-foundation.md` |
|
||||||
|
| Topic research | `research-[topic].md` | `research-gradient-methods.md` |
|
||||||
|
| Guide docs | `guide-[topic].md` | `guide-data-preparation.md` |
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Run N: [Descriptive Name]
|
||||||
|
|
||||||
|
## Hypothesis
|
||||||
|
|
||||||
|
[What we expect to happen and why.]
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- **Architecture:** [Model/system description]
|
||||||
|
- **Key change vs previous run:** [What's different]
|
||||||
|
- **Config:** [Hyperparameters, settings]
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
[What result would confirm or reject the hypothesis.]
|
||||||
|
|
||||||
|
## Run Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[How to build and run this experiment]
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Milestone Plan: [Project Name]
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: 1.1 --> 1.2 --> 1.3
|
||||||
|
|
|
||||||
|
Phase 2: v
|
||||||
|
2.1 --> 2.2 --> 2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: [Phase Name]
|
||||||
|
|
||||||
|
### Milestone 1.1: [Title] — PENDING
|
||||||
|
|
||||||
|
**What:** [One-sentence summary of the deliverable.]
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Task with enough detail that someone else could do it
|
||||||
|
- [ ] Another task
|
||||||
|
|
||||||
|
**Deferred:** [What was intentionally left out and why.]
|
||||||
|
|
||||||
|
**New Files:**
|
||||||
|
|
||||||
|
| File | What |
|
||||||
|
|------|------|
|
||||||
|
| path/to/file | One-line description |
|
||||||
|
|
||||||
|
**API Surface:**
|
||||||
|
[Function signatures or interface definitions, if applicable]
|
||||||
|
|
||||||
|
**Depends on:** None
|
||||||
|
|
||||||
|
**Success Criteria:** [Falsifiable statement with numbers]
|
||||||
|
|
||||||
|
**Test Cases:**
|
||||||
|
1. [Specific scenario] — [expected result]
|
||||||
|
2. [Specific scenario] — [expected result]
|
||||||
|
|
||||||
|
### Milestone 1.2: [Title] — PENDING
|
||||||
|
|
||||||
|
**What:** ...
|
||||||
|
|
||||||
|
**Depends on:** Milestone 1.1
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: [Phase Name]
|
||||||
|
|
||||||
|
### Milestone 2.1: [Title] — PENDING
|
||||||
|
|
||||||
|
**Depends on:** Milestone 1.2
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kill Gates
|
||||||
|
|
||||||
|
- **After Phase 1:** If [condition], abandon [approach] and [fallback action]
|
||||||
|
- **After Phase 2:** If [condition], pivot to [alternative]
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
> Technical architecture for [Project Name]
|
||||||
|
|
||||||
|
**Note:** This document is written after implementation. It describes how things actually work, not how they were planned. For the original design, see [spec.md](../spec.md).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Layer | Technology | Purpose |
|
||||||
|
|-------|------------|---------|
|
||||||
|
| [Layer] | [Technology] | [Purpose] |
|
||||||
|
|
||||||
|
## System Design
|
||||||
|
|
||||||
|
[Diagrams, data flow, component interactions.]
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
### [Decision 1]
|
||||||
|
|
||||||
|
**Context:** [Why this decision was needed]
|
||||||
|
|
||||||
|
**Decision:** [What was decided]
|
||||||
|
|
||||||
|
**Consequences:** [Trade-offs and implications]
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
[Describe main entities, formats, and relationships]
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
|
||||||
|
| Dependency | Purpose | Documentation |
|
||||||
|
|------------|---------|---------------|
|
||||||
|
| [Library] | [Purpose] | [Link] |
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
> Setup guide for [Project Name]
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Language runtime / compiler]
|
||||||
|
- [Package manager]
|
||||||
|
- [Additional prerequisites]
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/dcherrera/[ProjectName].git
|
||||||
|
cd [ProjectName]
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
[install command]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Experiments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a specific experiment
|
||||||
|
[command to run experiment]
|
||||||
|
|
||||||
|
# View results
|
||||||
|
[command to view output]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `experiments/` | Experiment code (numbered runs) |
|
||||||
|
| `results/` | Experiment results documentation |
|
||||||
|
| `data/` | Datasets (gitignored) |
|
||||||
|
| `literature/` | Paper notes & prior art |
|
||||||
|
| `src/` | Implementation code |
|
||||||
|
| `tests/` | Test suite |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read [goals.md](../goals.md) to understand success criteria
|
||||||
|
- Check [open_questions.md](../open_questions.md) for current research questions
|
||||||
|
- Review [Architecture](./architecture.md) for technical design
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Goals: [Project Name]
|
||||||
|
|
||||||
|
## Primary Goal
|
||||||
|
|
||||||
|
[One sentence: what does this research need to achieve to be worth the time?]
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
These are pass/fail — either you hit them or you don't.
|
||||||
|
|
||||||
|
1. [Specific measurable criterion]
|
||||||
|
2. [Specific measurable criterion]
|
||||||
|
3. [Specific measurable criterion]
|
||||||
|
|
||||||
|
## Stretch Goals
|
||||||
|
|
||||||
|
Nice to have, not required for success.
|
||||||
|
|
||||||
|
1. [Stretch criterion]
|
||||||
|
|
||||||
|
## Anti-Goals
|
||||||
|
|
||||||
|
What this project is NOT trying to do. Prevents scope creep.
|
||||||
|
|
||||||
|
1. [Explicit non-goal]
|
||||||
|
2. [Explicit non-goal]
|
||||||
|
|
||||||
|
## Fallback Plan
|
||||||
|
|
||||||
|
If no approach meets the success criteria:
|
||||||
|
- [What you'll do instead]
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
- **Phase 1 deadline:** [Date or "N weeks from start"]
|
||||||
|
- **Kill gate:** If Phase 1 doesn't pass by [deadline], [action]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# [Paper Title] ([Author Year])
|
||||||
|
|
||||||
|
**Link:** [URL]
|
||||||
|
|
||||||
|
**Key idea:** [One sentence]
|
||||||
|
|
||||||
|
**Relevance:** [How it relates to our research]
|
||||||
|
|
||||||
|
**What they got:** [Their results]
|
||||||
|
|
||||||
|
**What we can use:** [Actionable takeaway]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Open Questions: [Project Name]
|
||||||
|
|
||||||
|
## Active Questions
|
||||||
|
|
||||||
|
### Q1: [Question]
|
||||||
|
**Context:** [Why this matters]
|
||||||
|
**Blocking:** [What can't proceed until this is answered, if anything]
|
||||||
|
**Leads:** [Any partial answers or places to look]
|
||||||
|
|
||||||
|
## Answered Questions
|
||||||
|
|
||||||
|
<!-- Move questions here as they're resolved -->
|
||||||
|
<!--
|
||||||
|
### ~~Q0: [Question]~~
|
||||||
|
**Answer:** [What we found]
|
||||||
|
**Discovered in:** [Experiment N / Phase X / literature review]
|
||||||
|
-->
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Experiment N Results: [Short Title]
|
||||||
|
|
||||||
|
## Phase [A/B/C...] — [Phase Name]
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- **Architecture:** [Model/system description]
|
||||||
|
- **Source:** `experiments/run1-descriptive-name.ext`
|
||||||
|
- **Config:** [Key hyperparameters or settings]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of All Runs
|
||||||
|
|
||||||
|
| Run | Key Variable | Config | Best Metric | Key Finding |
|
||||||
|
|-----|-------------|--------|-------------|-------------|
|
||||||
|
| 1 | baseline | ... | ... | [One-line takeaway] |
|
||||||
|
| 2 | changed X | ... | ... | [One-line takeaway] |
|
||||||
|
| 3 | changed Y | ... | ... | [One-line takeaway] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Findings
|
||||||
|
|
||||||
|
### Finding 1: [Descriptive Title]
|
||||||
|
|
||||||
|
**Runs [N-M]** ([what was varied])
|
||||||
|
|
||||||
|
[Explanation of what happened and why. Include raw output.]
|
||||||
|
|
||||||
|
```
|
||||||
|
[Raw output from the run — config, per-epoch metrics, final results]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verdict:** [One-line conclusion. Bold the action item.]
|
||||||
|
|
||||||
|
### Finding 2: [Next Discovery]
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Best Result
|
||||||
|
|
||||||
|
**Run N: [Short description of the winning config]**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Full output of the best run]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusions
|
||||||
|
|
||||||
|
[What the phase proved or disproved. Structural insights, not just numbers.]
|
||||||
|
|
||||||
|
1. **[Finding]:** [Explanation]
|
||||||
|
2. **[Finding]:** [Explanation]
|
||||||
|
3. **[Finding]:** [Explanation]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Priority order:
|
||||||
|
|
||||||
|
1. ~~**Run N: [Description]**~~ — **DONE.** [Result summary.]
|
||||||
|
2. **Run N+1: [Description]** — *Next up.* [Why this is the next thing to try.]
|
||||||
|
3. **Run N+2: [Description]** — [Brief rationale.]
|
||||||
|
|
||||||
|
**Decision points:**
|
||||||
|
- **>[Target] achieved:** Proceed to [next phase]
|
||||||
|
- **Stuck at [range]:** [What that means and what to try]
|
||||||
|
- **Below [floor] after remaining runs:** [Kill gate — abandon or pivot]
|
||||||
|
|
||||||
|
### Literature Context
|
||||||
|
|
||||||
|
| Method | Result | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| [Existing work] | [Their result] | [How it compares] |
|
||||||
|
| **Us (Run N)** | **[Our result]** | **[Where we stand]** |
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Spec: [System/Component Name]
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[What this system does. One paragraph.]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- [Hard limit 1]
|
||||||
|
- [Hard limit 2]
|
||||||
|
- [Hard limit 3]
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
[System design. Diagrams, data flow, component interactions.]
|
||||||
|
|
||||||
|
## API Surface
|
||||||
|
|
||||||
|
[Function signatures, data formats, wire protocols — whatever the system exposes.]
|
||||||
|
|
||||||
|
```
|
||||||
|
// Example
|
||||||
|
void system_do_thing(input_t *in, output_t *out);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Formats
|
||||||
|
|
||||||
|
[File formats, serialization, storage layout.]
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
[How this system connects to other projects or systems.]
|
||||||
|
|
||||||
|
## Open Design Questions
|
||||||
|
|
||||||
|
- [Question that needs resolution before implementation]
|
||||||
|
- [Question]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
#!/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="MyApp" # Display name of the app
|
||||||
|
BUNDLE_ID_BASE="com.myapp.app" # Base bundle ID
|
||||||
|
VERSION="0.1.0" # App version
|
||||||
|
BINARY_NAME="my-app" # Rust binary name (usually lowercase with hyphens)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
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] [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 ""
|
||||||
|
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 -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"
|
||||||
|
if [ "$SHOW_INSTALL" = true ]; then
|
||||||
|
# No --ci flag: opens Finder window for drag-to-Applications
|
||||||
|
npx tauri build 2>&1 | tee -a "$BUILD_LOG"
|
||||||
|
else
|
||||||
|
# --ci flag: suppress Finder window
|
||||||
|
npx tauri build --ci 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 [ "$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 and capture output to console log
|
||||||
|
"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" 2>&1 | while IFS= read -r line; do
|
||||||
|
echo "$line"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line" >> "$CONSOLE_LOG"
|
||||||
|
done &
|
||||||
|
|
||||||
|
echo " App is running. Console output logged to:"
|
||||||
|
echo " $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,623 @@
|
||||||
|
# Project Guidelines
|
||||||
|
|
||||||
|
## Protecting This File
|
||||||
|
|
||||||
|
**IMPORTANT**: This file (claude.md) is gitignored and 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Best Practices (CRITICAL)
|
||||||
|
|
||||||
|
**Read `coding-best-practices.md` before writing any code.** It contains personal coding conventions that apply to every project — naming rules, file/function size limits, folder nesting patterns, persistence rules, and language-specific structure guides. Those rules override defaults and must be followed.
|
||||||
|
|
||||||
|
### File Header Check Rule
|
||||||
|
|
||||||
|
**Every time you open a file to work on it, check the file header first.** Look for outstanding `@todo` items, unresolved `@issues`, or outdated `@status` notes. If any are completed or resolved, clean them up before doing other work. If a TODO is relevant to the current task, address it. This ensures nothing falls through the cracks.
|
||||||
|
|
||||||
|
## Project Structure (CRITICAL)
|
||||||
|
|
||||||
|
This project follows a standardized structure for future modularity. The long-term goal is to enable drag-and-drop of components between projects. **This structure must be followed.**
|
||||||
|
|
||||||
|
```
|
||||||
|
ProjectRoot/ # git root
|
||||||
|
├── claude.md # (gitignored) AI assistant guidelines
|
||||||
|
├── coding-best-practices.md # Coding conventions (read before Phase 1)
|
||||||
|
├── build_spec.md # (gitignored) Full project specification
|
||||||
|
├── build_plan.md # (gitignored) Phased task checklist
|
||||||
|
├── .gitignore
|
||||||
|
├── docs/ # Public-facing documentation (parsed to HTML for website)
|
||||||
|
│ ├── README.md # Docs index with table of contents
|
||||||
|
│ ├── getting-started.md
|
||||||
|
│ ├── architecture.md
|
||||||
|
│ ├── development.md
|
||||||
|
│ ├── deployment.md
|
||||||
|
│ └── feature-*.md # Feature docs (added per phase)
|
||||||
|
├── InternalDocs/ # Internal development documentation (not public)
|
||||||
|
│ └── ... # Design decisions, technical debt, internal API docs
|
||||||
|
└── app/ # ALL project code lives here
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/ # Static assets
|
||||||
|
│ ├── boot/ # Quasar boot files (nhost init, etc.)
|
||||||
|
│ ├── components/ # Vue components
|
||||||
|
│ ├── composables/ # Vue composables
|
||||||
|
│ ├── layouts/ # Page layouts
|
||||||
|
│ ├── pages/ # Route pages
|
||||||
|
│ ├── router/ # Vue Router config
|
||||||
|
│ ├── stores/ # Pinia stores
|
||||||
|
│ ├── services/ # API services
|
||||||
|
│ └── types/ # TypeScript types
|
||||||
|
├── nhost/ # Nhost configuration
|
||||||
|
│ ├── functions/ # Serverless functions
|
||||||
|
│ ├── migrations/ # Database migrations
|
||||||
|
│ └── metadata/ # Hasura metadata
|
||||||
|
├── public/ # Public static files
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .env # Dev environment (gitignored)
|
||||||
|
└── .env.prod # Prod environment (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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) - SPA mode
|
||||||
|
- **Desktop** (optional): Tauri (Rust) - Cross-platform builds (macOS, Windows, Linux)
|
||||||
|
- **Backend**: Nhost (self-hosted via Docker)
|
||||||
|
- PostgreSQL - Database
|
||||||
|
- Hasura - GraphQL API
|
||||||
|
- Auth - Authentication service
|
||||||
|
- Storage - File storage
|
||||||
|
- Functions - Serverless functions (Node.js)
|
||||||
|
- **Containerization**: Docker / Docker Compose
|
||||||
|
|
||||||
|
### Tauri Desktop Apps
|
||||||
|
|
||||||
|
When building desktop apps with Tauri:
|
||||||
|
- Add `src-tauri/` folder inside `app/`
|
||||||
|
- Use the build-and-run approach (no hot reload) for consistent dev/prod builds
|
||||||
|
- Separate bundle IDs for dev and prod (can run both simultaneously)
|
||||||
|
|
||||||
|
## 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`) |
|
||||||
|
| `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/spa/`)
|
||||||
|
- 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 Setup (Tauri)
|
||||||
|
|
||||||
|
For frontend `console.log()` to appear in the logs, you need:
|
||||||
|
|
||||||
|
1. **Boot file** - Copy `templates/scripts/console-logger.ts` to `app/src/boot/`
|
||||||
|
2. **Register boot** - Add `'console-logger'` to `boot` array in `quasar.config.js`
|
||||||
|
3. **Rust command** - Add `log_to_stdout` command to `src-tauri/src/commands.rs`
|
||||||
|
4. **Cargo dep** - Add `chrono = "0.4"` to `Cargo.toml`
|
||||||
|
|
||||||
|
The console-logger.ts template includes the Rust code you need to add.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
```
|
||||||
|
console.log() → console-logger.ts → invoke('log_to_stdout') → Rust println!() → stdout → build.sh logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 (CRITICAL)
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
**Migration folder naming:**
|
||||||
|
```
|
||||||
|
app/nhost/migrations/default/
|
||||||
|
├── 1_init_schema/
|
||||||
|
├── 2_add_users/
|
||||||
|
├── 3_add_settings/
|
||||||
|
└── ...next version number...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
```
|
||||||
|
"cannot define permission for admin role"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct approach:**
|
||||||
|
- Define permissions only for `user`, `public`, and custom roles
|
||||||
|
- Users with `X-Hasura-Role: admin` automatically bypass all permission checks
|
||||||
|
|
||||||
|
**Example - WRONG:**
|
||||||
|
```yaml
|
||||||
|
select_permissions:
|
||||||
|
- role: admin # DON'T DO THIS
|
||||||
|
permission:
|
||||||
|
columns: [id, name]
|
||||||
|
filter: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example - CORRECT:**
|
||||||
|
```yaml
|
||||||
|
select_permissions:
|
||||||
|
- role: user # Only define for non-admin roles
|
||||||
|
permission:
|
||||||
|
columns: [id, name]
|
||||||
|
filter: { created_by: { _eq: X-Hasura-User-Id } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Approach
|
||||||
|
|
||||||
|
**Integrate-as-you-go** - Connect real backend services as you build. Don't build elaborate mock systems. The Docker infrastructure provides:
|
||||||
|
- Database & Hasura - Real tables and GraphQL
|
||||||
|
- Nhost Auth - Real authentication
|
||||||
|
- Real data from the start
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### docs/ vs InternalDocs/
|
||||||
|
|
||||||
|
This project has two documentation directories with different audiences:
|
||||||
|
|
||||||
|
| Directory | Audience | Purpose | Parsed for website? |
|
||||||
|
|-----------|----------|---------|---------------------|
|
||||||
|
| `docs/` | Public | User-facing app documentation — getting started, features, architecture | Yes |
|
||||||
|
| `InternalDocs/` | Developers | Internal dev context — design decisions, technical debt, internal API contracts, Claude context | No |
|
||||||
|
|
||||||
|
**`docs/`** contains ONLY what the public needs. It gets parsed into HTML and served on the app website. No internal implementation details, no dev-only context.
|
||||||
|
|
||||||
|
**`InternalDocs/`** contains everything internal — design rationale, known technical debt, internal API documentation, integration notes, debugging guides, anything that helps the development team but doesn't belong in public docs.
|
||||||
|
|
||||||
|
Both directories are committed to git.
|
||||||
|
|
||||||
|
### Phase 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
|
||||||
|
4. **Update internal docs** - Add design decisions, technical debt notes, or internal API docs to `InternalDocs/` as needed
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Nhost
|
||||||
|
NHOST_ADMIN_SECRET=
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=
|
||||||
|
|
||||||
|
# App
|
||||||
|
APP_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project-Specific Configuration
|
||||||
|
|
||||||
|
<!-- Everything below this line is customized per project -->
|
||||||
|
|
||||||
|
## Project Name
|
||||||
|
|
||||||
|
[PROJECT_NAME]
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Brief description from build_spec.md]
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
[Tables and relationships]
|
||||||
|
|
||||||
|
## URLs
|
||||||
|
|
||||||
|
| Service | Local | Production |
|
||||||
|
|---------|-------|------------|
|
||||||
|
| App | http://localhost:[dynamic] | |
|
||||||
|
| GraphQL | http://localhost:8080 | |
|
||||||
|
|
||||||
|
**Note**: For Tauri desktop apps, use dynamic port allocation. The dev server will find the first available port.
|
||||||
|
|
||||||
|
## MCP Servers
|
||||||
|
|
||||||
|
[Configure in .mcp.json if needed]
|
||||||
|
|
||||||
|
## Servers
|
||||||
|
|
||||||
|
[Production server details if applicable]
|
||||||
|
|
||||||
|
## Dockerized Frontend for Production
|
||||||
|
|
||||||
|
When deploying a fully dockerized project, add a frontend service to docker-compose:
|
||||||
|
|
||||||
|
### 1. Create `app/Dockerfile.frontend`
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create `app/nginx.conf`
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add frontend service to `docker-compose.yml`
|
||||||
|
|
||||||
|
All Traefik hostnames use hardcoded localhost values. TeamIDE's proxy rewrites the Host header to localhost, so this works both locally and in production.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.frontend
|
||||||
|
depends_on:
|
||||||
|
graphql:
|
||||||
|
condition: service_healthy
|
||||||
|
auth:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
traefik.enable: "true"
|
||||||
|
traefik.http.routers.frontend.rule: Host(`localhost`)
|
||||||
|
traefik.http.routers.frontend.entrypoints: web
|
||||||
|
traefik.http.services.frontend.loadbalancer.server.port: "80"
|
||||||
|
extra_hosts:
|
||||||
|
- host.docker.internal:host-gateway
|
||||||
|
- localhost:host-gateway
|
||||||
|
- auth.localhost:host-gateway
|
||||||
|
- hasura.localhost:host-gateway
|
||||||
|
- storage.localhost:host-gateway
|
||||||
|
restart: always
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Traefik hostname pattern
|
||||||
|
|
||||||
|
All services use hardcoded localhost hostnames:
|
||||||
|
- Frontend: `Host(\`localhost\`)`
|
||||||
|
- Auth: `Host(\`auth.localhost\`)`
|
||||||
|
- Hasura: `Host(\`hasura.localhost\`)`
|
||||||
|
- Storage: `Host(\`storage.localhost\`)`
|
||||||
|
- Functions: `Host(\`functions.localhost\`)`
|
||||||
|
|
||||||
|
## TeamIDE Hosting
|
||||||
|
|
||||||
|
This project is deployed via TeamIDE.
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
To get your workspace ID, run:
|
||||||
|
```bash
|
||||||
|
teamide workspace list
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,702 @@
|
||||||
|
# Coding Best Practices
|
||||||
|
|
||||||
|
**Read this file at the start of any coding phase.** These are personal coding conventions that apply to every project, every language, every framework. They override defaults and must be followed unless explicitly told otherwise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
Code is navigated spatially. The folder tree is a mental map — every directory, file, and function name is a landmark. When the structure is clean and names are descriptive, you can find what you need without searching. When it's not, you're lost in your own codebase.
|
||||||
|
|
||||||
|
These rules exist to keep code **navigable, scannable, and small enough to hold in your head**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Universal Rules
|
||||||
|
|
||||||
|
These apply to ALL code in ALL languages. No exceptions.
|
||||||
|
|
||||||
|
### 1. Descriptive Names — Everything
|
||||||
|
|
||||||
|
Every name must tell you what it is at a glance. Files, folders, functions, variables, types, classes, routes, components, database tables, columns — everything.
|
||||||
|
|
||||||
|
**The test:** Would you know what this is from the file tree alone, without opening it?
|
||||||
|
|
||||||
|
| Bad | Good |
|
||||||
|
|-----|------|
|
||||||
|
| `utils.ts` | `dateFormatUtils.ts` |
|
||||||
|
| `helpers.ts` | `pricingCalculationHelpers.ts` |
|
||||||
|
| `data.ts` | `userSessionData.ts` |
|
||||||
|
| `handler()` | `handleFileUploadComplete()` |
|
||||||
|
| `process()` | `processIncomingWebhookPayload()` |
|
||||||
|
| `calc()` | `calculateMonthlySubscriptionCost()` |
|
||||||
|
| `Item` | `InventoryLineItem` |
|
||||||
|
| `manage()` | `manageConnectionPool()` |
|
||||||
|
| `Service` | `NotificationDeliveryService` |
|
||||||
|
| `tmp` | `pendingUploadBuffer` |
|
||||||
|
|
||||||
|
Prefer longer, descriptive names over short, ambiguous ones. Abbreviations are acceptable only when universally understood (`id`, `url`, `http`, `db`, `api`, `auth`).
|
||||||
|
|
||||||
|
### 2. Small Functions — ~100 Lines of Code Max
|
||||||
|
|
||||||
|
The ~100-line limit applies to the **function body only** — the executable logic. Documentation comments above the function do NOT count toward this limit.
|
||||||
|
|
||||||
|
- If a function body is growing past ~100 lines, look for logical extraction points
|
||||||
|
- Each function should do one thing and do it well
|
||||||
|
- Aim for concise, precise code — fewer lines that do more, not sprawling logic
|
||||||
|
- Use one blank line to separate logical chunks within a function for readability — but no more than one
|
||||||
|
- Don't add unnecessary blank lines just for spacing — simplify the logic instead
|
||||||
|
|
||||||
|
### 3. Small Files — Under 1,000 Lines of Code
|
||||||
|
|
||||||
|
The 1,000-line limit applies to **code only**. Documentation comments, file headers, and inline documentation do NOT count toward this limit. This limit exists to enforce efficient, concise code — not to punish well-documented code.
|
||||||
|
|
||||||
|
- Prefer more files with fewer functions each
|
||||||
|
- Every function in a file must logically belong there — don't put camera rotation functions in a text editor file
|
||||||
|
- Split where it makes semantic sense, not just to hit a number
|
||||||
|
- The folder nesting structure exists to support these smaller units
|
||||||
|
|
||||||
|
### 4. Library-First Design
|
||||||
|
|
||||||
|
Write all code as if every function could be extracted into a standalone library. The end goal is maximum reusability across projects.
|
||||||
|
|
||||||
|
**Principles:**
|
||||||
|
- Functions should be self-contained with minimal external dependencies
|
||||||
|
- Clean, well-defined interfaces — inputs and outputs should be obvious
|
||||||
|
- Avoid hard-coded project-specific values inside reusable logic — pass them as parameters
|
||||||
|
- Group related functions that could form a library into their own files/modules
|
||||||
|
- In any language that supports libraries (C, Rust, JS/TS, Swift, etc.), structure code so extraction into a library requires moving files, not rewriting them
|
||||||
|
|
||||||
|
**For C specifically:** This is the primary target. Every utility, algorithm, data structure, and system abstraction should be written as a reusable library from day one. Headers define the public API contract, source files contain the implementation. A well-written C library can be linked into any project.
|
||||||
|
|
||||||
|
### 5. No Auto-Save / No Background Persistence
|
||||||
|
|
||||||
|
**Every write to a database, file, or storage must trace back to a deliberate user action.** No exceptions unless explicitly requested.
|
||||||
|
|
||||||
|
Never implement:
|
||||||
|
- Auto-saving as the user types
|
||||||
|
- Debounced save on input change
|
||||||
|
- Watchers that persist reactive state changes
|
||||||
|
- Background sync intervals
|
||||||
|
- Persisting UI state automatically (sidebar position, tab selection, scroll)
|
||||||
|
- Writing to storage on mount/unmount/navigation
|
||||||
|
|
||||||
|
Always require:
|
||||||
|
- User clicks "Save" → write
|
||||||
|
- User submits form → persist
|
||||||
|
- User explicitly toggles setting → save
|
||||||
|
- User confirms action → execute
|
||||||
|
|
||||||
|
**Pattern:** `User Action → Service Call → Write → Confirm Success`
|
||||||
|
|
||||||
|
### 6. Folder Nesting as Navigation
|
||||||
|
|
||||||
|
Directories are not just organization — they're how you navigate. The tree structure should let you drill down to the right file by following a logical path, not by guessing or searching.
|
||||||
|
|
||||||
|
**Principles:**
|
||||||
|
- Group by **domain/feature**, not by file type (prefer `auth/authService.ts` over `services/auth.ts` when the module is complex enough)
|
||||||
|
- Each directory should have a clear, single purpose
|
||||||
|
- Nesting depth should match conceptual depth — don't nest just for nesting, don't flatten when there's real hierarchy
|
||||||
|
- A directory with 1 file is usually wrong — either the file belongs in the parent or more files belong in the directory
|
||||||
|
|
||||||
|
### 7. Error Transparency
|
||||||
|
|
||||||
|
- Never swallow errors silently
|
||||||
|
- Catch at service boundaries where you can show user feedback
|
||||||
|
- Log technical details (`console.error` with context)
|
||||||
|
- Show human-readable messages to users — never raw errors, stack traces, or technical details
|
||||||
|
- If you catch it, either handle it meaningfully or log it. Never empty catch blocks.
|
||||||
|
|
||||||
|
### 8. Delete Dead Code
|
||||||
|
|
||||||
|
Don't comment out code. Don't leave unused functions, imports, or variables. If it's not being used, delete it. Git has the history if you ever need it back.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inline Code Documentation
|
||||||
|
|
||||||
|
Every source file carries its own documentation. This gives both the developer and Claude immediate context the moment a file is opened — no hunting through separate docs.
|
||||||
|
|
||||||
|
### File Header — Always Present
|
||||||
|
|
||||||
|
Every source file MUST have a header block at the top with three sections: **status**, **issues**, and **todo**. All three sections are always present, even when empty. The empty slots serve as a reminder that documentation can go there — if the sections aren't present, they get forgotten.
|
||||||
|
|
||||||
|
**Template (JS/TS/Vue):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* @file DescriptiveFileName
|
||||||
|
* @description What this file is for and what it does (1-3 concise lines)
|
||||||
|
*
|
||||||
|
* @status None
|
||||||
|
* @issues None
|
||||||
|
* @todo None
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template (Rust):**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! # DescriptiveModuleName
|
||||||
|
//! What this module is for and what it does (1-3 concise lines)
|
||||||
|
//!
|
||||||
|
//! ## Status
|
||||||
|
//! None
|
||||||
|
//!
|
||||||
|
//! ## Issues
|
||||||
|
//! None
|
||||||
|
//!
|
||||||
|
//! ## Todo
|
||||||
|
//! None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template (C — header files):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* @file descriptive_name.h
|
||||||
|
* @brief What this header defines and its purpose (1-3 concise lines)
|
||||||
|
*
|
||||||
|
* @status None
|
||||||
|
* @issues None
|
||||||
|
* @todo None
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template (C — source files):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* @file descriptive_name.c
|
||||||
|
* @brief Implementation of [what this file implements]
|
||||||
|
*
|
||||||
|
* @status None
|
||||||
|
* @issues None
|
||||||
|
* @todo None
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template (Swift):**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
/// # DescriptiveFileName
|
||||||
|
/// What this file is for and what it does (1-3 concise lines)
|
||||||
|
///
|
||||||
|
/// ## Status
|
||||||
|
/// None
|
||||||
|
///
|
||||||
|
/// ## Issues
|
||||||
|
/// None
|
||||||
|
///
|
||||||
|
/// ## Todo
|
||||||
|
/// None
|
||||||
|
```
|
||||||
|
|
||||||
|
**When there IS something to document:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* @file AvatarUploadService
|
||||||
|
* @description Handles user avatar upload, validation, and storage.
|
||||||
|
* Supports JPEG, PNG, and WebP formats up to 10MB.
|
||||||
|
*
|
||||||
|
* @status Avatar upload works but no progress indicator yet
|
||||||
|
* @issues Timeout on large files (>10MB) — needs chunked upload
|
||||||
|
* @todo
|
||||||
|
* - [ ] Add upload progress bar (blocked on chunked upload)
|
||||||
|
* - [ ] Validate image dimensions before upload
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
**What each section means:**
|
||||||
|
|
||||||
|
| Section | Purpose | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `@status` | Current state of the code — what works, what's partial | "Core CRUD works, batch operations not yet implemented" |
|
||||||
|
| `@issues` | Known bugs, performance problems, fragile areas | "Race condition when two users edit simultaneously" |
|
||||||
|
| `@todo` | Incomplete work left during a session — breadcrumbs | "- [ ] Add input validation for email field" |
|
||||||
|
|
||||||
|
### File Header Cleanup Rule
|
||||||
|
|
||||||
|
**When working in any file, always check the header first.** If there are completed TODOs, resolved issues, or outdated status notes — clean them up. Reset sections to "None" when everything is addressed. This prevents documentation rot.
|
||||||
|
|
||||||
|
### File-Level TODOs Are Breadcrumbs
|
||||||
|
|
||||||
|
The `@todo` section in file headers is NOT the project plan. The real task tracking lives in plan files and `.teamide/tasks/`. File-level TODOs exist for the "mid-session, had to step away" situation:
|
||||||
|
|
||||||
|
- You're deep in a function, a priority comes in — drop a TODO so you don't lose your place
|
||||||
|
- You notice something needs doing but it's not the current task — note it so it doesn't get forgotten
|
||||||
|
- If you come back to that file a month later, the TODO catches what fell through the cracks
|
||||||
|
|
||||||
|
**Cleanup context:** When a file-level TODO is completed, remove it from the header. When all TODOs are done, reset to "None". If a TODO turns into a real task, move it to the project plan and remove it from the header.
|
||||||
|
|
||||||
|
### Function Documentation
|
||||||
|
|
||||||
|
Document every public/exported function. Internal helper functions need docs only when the logic isn't obvious from the name and signature.
|
||||||
|
|
||||||
|
**JS/TS pattern:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Calculates the prorated subscription cost for a partial billing period.
|
||||||
|
* Uses daily rate × remaining days, rounded to 2 decimal places.
|
||||||
|
*
|
||||||
|
* @param planMonthlyRate - The full monthly rate in dollars
|
||||||
|
* @param startDate - When the subscription started (mid-cycle)
|
||||||
|
* @param billingCycleEnd - End date of the current billing cycle
|
||||||
|
* @returns Prorated cost in dollars (e.g., 14.52)
|
||||||
|
*/
|
||||||
|
function calculateProratedSubscriptionCost(
|
||||||
|
planMonthlyRate: number,
|
||||||
|
startDate: Date,
|
||||||
|
billingCycleEnd: Date
|
||||||
|
): number {
|
||||||
|
// function body...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C header pattern (API contract):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* Allocates a block from the thread-local memory pool.
|
||||||
|
* Falls back to system malloc if pool is exhausted.
|
||||||
|
*
|
||||||
|
* @param size Bytes to allocate (rounded up to pool alignment)
|
||||||
|
* @return Pointer to allocated block, or NULL on failure
|
||||||
|
*
|
||||||
|
* Thread-safe: yes (per-thread pools, no locks needed)
|
||||||
|
* Reentrant: no (modifies pool state)
|
||||||
|
*/
|
||||||
|
void *pool_alloc(size_t size);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rust pattern:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Calculates the prorated subscription cost for a partial billing period.
|
||||||
|
/// Uses daily rate × remaining days, rounded to 2 decimal places.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `plan_monthly_rate` - The full monthly rate in dollars
|
||||||
|
/// * `start_date` - When the subscription started (mid-cycle)
|
||||||
|
/// * `billing_cycle_end` - End date of the current billing cycle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Prorated cost in dollars (e.g., 14.52)
|
||||||
|
fn calculate_prorated_subscription_cost(
|
||||||
|
plan_monthly_rate: f64,
|
||||||
|
start_date: NaiveDate,
|
||||||
|
billing_cycle_end: NaiveDate,
|
||||||
|
) -> f64 {
|
||||||
|
// function body...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inline Comments
|
||||||
|
|
||||||
|
Use inline comments to explain **why**, not **what**. The code shows what it does — comments explain the reasoning that isn't obvious.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Offset by 1 because the API uses 1-based page indexing
|
||||||
|
const apiPage = displayPage + 1;
|
||||||
|
|
||||||
|
// Must check auth before loading data — the API returns 200 with empty
|
||||||
|
// results for unauthorized users instead of 401
|
||||||
|
await verifyAuthStatus();
|
||||||
|
const results = await loadDashboardData();
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't write comments that just restate the code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD — restates the code
|
||||||
|
const total = price * quantity; // multiply price by quantity
|
||||||
|
|
||||||
|
// GOOD — explains the business rule
|
||||||
|
const total = price * quantity; // pre-tax total, tax applied at checkout
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder Structure Patterns by Language
|
||||||
|
|
||||||
|
The core principle is the same everywhere: **categorical nesting by domain**, with descriptive names, keeping files small and focused. Here's how that maps to each language ecosystem.
|
||||||
|
|
||||||
|
### JavaScript / TypeScript (Vue, Quasar, Node)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── modules/ # Feature modules (self-contained)
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── index.ts # Module entry, re-exports
|
||||||
|
│ │ ├── store.ts # Pinia store (composition API)
|
||||||
|
│ │ ├── api.ts # API calls for this module
|
||||||
|
│ │ ├── types.ts # Types/interfaces for this module
|
||||||
|
│ │ ├── AuthLogin.vue # Components named descriptively
|
||||||
|
│ │ ├── AuthRegister.vue
|
||||||
|
│ │ └── components/ # Sub-components if needed
|
||||||
|
│ │ └── AuthFormField.vue
|
||||||
|
│ └── dashboard/
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── store.ts
|
||||||
|
│ ├── api.ts
|
||||||
|
│ └── DashboardOverview.vue
|
||||||
|
├── services/ # Shared services (API client, DB, etc.)
|
||||||
|
│ ├── apiClient.ts # Centralized fetch wrapper
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── client.ts
|
||||||
|
│ │ └── tables/
|
||||||
|
│ │ ├── userSettings.ts
|
||||||
|
│ │ └── sessionHistory.ts
|
||||||
|
│ └── fileWatcher.ts
|
||||||
|
├── stores/ # Global stores (app-wide state)
|
||||||
|
│ └── globalAppState.ts
|
||||||
|
├── components/ # Shared components (used across modules)
|
||||||
|
│ ├── AppHeader.vue
|
||||||
|
│ └── ConfirmDialog.vue
|
||||||
|
├── composables/ # Shared composables
|
||||||
|
│ └── useWindowResize.ts
|
||||||
|
├── types/ # Global types
|
||||||
|
│ └── appTypes.ts
|
||||||
|
├── layouts/ # Page layouts
|
||||||
|
├── pages/ # Route pages
|
||||||
|
├── router/ # Vue Router config
|
||||||
|
├── boot/ # Startup/init files
|
||||||
|
├── css/ # Global styles
|
||||||
|
└── utils/ # Platform detection, formatters
|
||||||
|
└── platformDetection.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key conventions:**
|
||||||
|
- Each feature module has `index.ts`, `store.ts`, `api.ts`, `types.ts`
|
||||||
|
- Components are PascalCase: `UserProfileCard.vue`
|
||||||
|
- Everything else is camelCase: `apiClient.ts`, `useAuth.ts`
|
||||||
|
- Path aliases: `@/` for `src/`, `@modules/`, `@components/`, `@stores/`
|
||||||
|
|
||||||
|
### Rust
|
||||||
|
|
||||||
|
Apply the same categorical nesting using Rust's module system:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # Entry point, minimal — just init and run
|
||||||
|
├── app.rs # Application setup, config, state
|
||||||
|
├── lib.rs # Library entry (if crate is also a lib)
|
||||||
|
├── commands/ # Tauri commands or CLI handlers
|
||||||
|
│ ├── mod.rs # Re-exports all command modules
|
||||||
|
│ ├── file_operations.rs # File read/write/watch commands
|
||||||
|
│ ├── window_management.rs # Window creation/positioning
|
||||||
|
│ └── backend_lifecycle.rs # Start/stop/health-check backend
|
||||||
|
├── services/ # Business logic, reusable across commands
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ ├── process_manager.rs # Spawn/kill/monitor child processes
|
||||||
|
│ ├── port_allocator.rs # Find available ports
|
||||||
|
│ └── log_writer.rs # Write structured logs to files
|
||||||
|
├── models/ # Data structures, enums, types
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ ├── app_config.rs # Config structs
|
||||||
|
│ └── backend_state.rs # Backend process state
|
||||||
|
├── platform/ # OS-specific code
|
||||||
|
│ ├── mod.rs # Conditional compilation re-exports
|
||||||
|
│ ├── macos.rs # macOS-specific (menus, notifications)
|
||||||
|
│ ├── windows.rs
|
||||||
|
│ └── linux.rs
|
||||||
|
└── utils/ # Small, focused helpers
|
||||||
|
├── mod.rs
|
||||||
|
└── path_resolver.rs # Resolve app paths, home dir, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key conventions:**
|
||||||
|
- `mod.rs` in each directory for re-exports (like `index.ts`)
|
||||||
|
- snake_case for everything: files, functions, variables, modules
|
||||||
|
- Descriptive module names: `backend_lifecycle.rs` not `backend.rs`
|
||||||
|
- Keep `main.rs` thin — delegate to `app.rs` or modules immediately
|
||||||
|
- Group by responsibility, not by Rust convention alone
|
||||||
|
- Include `lib.rs` when the crate should be usable as a library
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
Follow categorical nesting for headers, with source files mirroring the structure. Every module should be structured for library extraction from day one.
|
||||||
|
|
||||||
|
```
|
||||||
|
include/ # Public headers (categorical nesting)
|
||||||
|
├── core/ # Core functionality
|
||||||
|
│ ├── memory_allocator.h
|
||||||
|
│ ├── string_operations.h
|
||||||
|
│ └── error_codes.h
|
||||||
|
├── arch/ # Architecture-specific
|
||||||
|
│ ├── arm64.h
|
||||||
|
│ ├── x86_64.h
|
||||||
|
│ └── riscv32.h
|
||||||
|
├── os/ # OS-specific abstractions
|
||||||
|
│ ├── linux_syscalls.h
|
||||||
|
│ ├── darwin_syscalls.h
|
||||||
|
│ └── windows_syscalls.h
|
||||||
|
├── fmt/ # File format parsers
|
||||||
|
│ ├── elf_parser.h
|
||||||
|
│ ├── macho_parser.h
|
||||||
|
│ └── pe_parser.h
|
||||||
|
└── sys/ # System-level interfaces
|
||||||
|
└── thread_pool.h
|
||||||
|
|
||||||
|
src/ # Implementation files (mirror include/ structure)
|
||||||
|
├── core/
|
||||||
|
│ ├── memory_allocator.c
|
||||||
|
│ ├── string_operations.c
|
||||||
|
│ └── error_codes.c
|
||||||
|
├── arch/
|
||||||
|
│ ├── arm64.c
|
||||||
|
│ └── x86_64.c
|
||||||
|
└── os/
|
||||||
|
├── linux_syscalls.c
|
||||||
|
└── darwin_syscalls.c
|
||||||
|
|
||||||
|
tests/ # Test files (mirror src/ structure)
|
||||||
|
├── unit/
|
||||||
|
│ ├── test_memory_allocator.c
|
||||||
|
│ └── test_string_operations.c
|
||||||
|
└── integration/
|
||||||
|
└── test_full_pipeline.c
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key conventions:**
|
||||||
|
- `include/` and `src/` mirror each other's structure
|
||||||
|
- Category directories by domain: `core/`, `arch/`, `os/`, `fmt/`, `sys/`
|
||||||
|
- snake_case for all file names
|
||||||
|
- Header names describe what's inside: `memory_allocator.h` not `mem.h`
|
||||||
|
- One header + one source per logical unit
|
||||||
|
- Headers define the full API contract (see Function Documentation section)
|
||||||
|
- Each category directory is a potential standalone library
|
||||||
|
|
||||||
|
### Swift (macOS / iOS)
|
||||||
|
|
||||||
|
```
|
||||||
|
Sources/
|
||||||
|
├── App/ # App entry, lifecycle
|
||||||
|
│ ├── AppMain.swift
|
||||||
|
│ └── AppDelegate.swift
|
||||||
|
├── Features/ # Feature modules
|
||||||
|
│ ├── Camera/
|
||||||
|
│ │ ├── CameraManager.swift
|
||||||
|
│ │ ├── CameraPreviewView.swift
|
||||||
|
│ │ └── CameraPermissions.swift
|
||||||
|
│ └── Recording/
|
||||||
|
│ ├── RecordingSession.swift
|
||||||
|
│ ├── RecordingOverlay.swift
|
||||||
|
│ └── RecordingStorage.swift
|
||||||
|
├── Services/ # Shared services
|
||||||
|
│ ├── FileStorageService.swift
|
||||||
|
│ └── NotificationService.swift
|
||||||
|
├── Models/ # Data models
|
||||||
|
│ ├── RecordingMetadata.swift
|
||||||
|
│ └── UserPreferences.swift
|
||||||
|
├── Views/ # Shared UI components
|
||||||
|
│ ├── PermissionRequestView.swift
|
||||||
|
│ └── StatusIndicator.swift
|
||||||
|
├── Extensions/ # Swift extensions
|
||||||
|
│ └── DateFormattingExtensions.swift
|
||||||
|
└── Utilities/ # Small helpers
|
||||||
|
└── DeviceCapabilities.swift
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key conventions:**
|
||||||
|
- PascalCase for files (Swift convention), but still descriptive
|
||||||
|
- Feature-based grouping under `Features/`
|
||||||
|
- `CameraPermissions.swift` not `Permissions.swift` — qualify the name
|
||||||
|
|
||||||
|
### General Pattern (Any Language)
|
||||||
|
|
||||||
|
When you encounter a new language, apply this structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/ (or equivalent)
|
||||||
|
├── [entry point] # Minimal — just init and delegate
|
||||||
|
├── features/ or modules/ # Self-contained feature units
|
||||||
|
│ └── [feature_name]/
|
||||||
|
│ ├── [entry/index] # Re-exports or module definition
|
||||||
|
│ ├── [types/models] # Data structures for this feature
|
||||||
|
│ ├── [api/handlers] # External interface (API calls, commands)
|
||||||
|
│ └── [service/logic] # Business logic
|
||||||
|
├── services/ # Shared, cross-cutting services
|
||||||
|
├── models/ or types/ # Global data structures
|
||||||
|
├── platform/ # OS/environment-specific code
|
||||||
|
└── utils/ # Small, focused helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style Conventions
|
||||||
|
|
||||||
|
### Import Organization (JS/TS)
|
||||||
|
|
||||||
|
Organize imports in 3 groups, separated by blank lines:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Framework & language imports
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
// 2. Third-party libraries
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
// 3. Local imports (project code)
|
||||||
|
import { useGlobalStore } from '@/stores/globalAppState';
|
||||||
|
import { request } from '@/services/apiClient';
|
||||||
|
import type { UserProfile } from './types';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue Component Structure
|
||||||
|
|
||||||
|
Always this order in single-file components:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- Template first — the visual output -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Script second — the logic
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// Style last — scoped to this component
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
Within `<script setup>`:
|
||||||
|
1. Imports
|
||||||
|
2. Store references
|
||||||
|
3. Props & emits
|
||||||
|
4. Reactive state (`ref`, `reactive`)
|
||||||
|
5. Computed properties
|
||||||
|
6. Methods / handlers
|
||||||
|
7. Watchers
|
||||||
|
8. Lifecycle hooks (`onMounted`, etc.)
|
||||||
|
|
||||||
|
### Pinia Store Pattern
|
||||||
|
|
||||||
|
Always use composition API style:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const useFeatureStore = defineStore('feature-name', () => {
|
||||||
|
// State
|
||||||
|
const items = ref<Item[]>([]);
|
||||||
|
const selectedItem = ref<Item | null>(null);
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
|
||||||
|
// Init guard — prevent double initialization
|
||||||
|
async function initialize() {
|
||||||
|
if (isInitialized.value) return;
|
||||||
|
items.value = await featureApi.loadItems();
|
||||||
|
isInitialized.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function selectItem(item: Item) {
|
||||||
|
selectedItem.value = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return everything that should be accessible
|
||||||
|
return { items, selectedItem, isInitialized, initialize, selectItem };
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Layer Pattern
|
||||||
|
|
||||||
|
Centralized request function + module-level API objects:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// services/apiClient.ts — single fetch wrapper, typed
|
||||||
|
export async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||||
|
throw new Error(error.error || 'Request failed');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// modules/inventory/api.ts — module-specific API
|
||||||
|
export const inventoryApi = {
|
||||||
|
listProducts: (warehouseId: string): Promise<Product[]> =>
|
||||||
|
request(`/warehouses/${warehouseId}/products`),
|
||||||
|
|
||||||
|
updateStock: (productId: string, quantity: number): Promise<void> =>
|
||||||
|
request(`/products/${productId}/stock`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ quantity })
|
||||||
|
})
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript Conventions
|
||||||
|
|
||||||
|
- **Strict mode** always on
|
||||||
|
- **No `any`** — use `unknown` with type narrowing instead
|
||||||
|
- **Discriminated unions** with type guard functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type EditorTab =
|
||||||
|
| { type: 'file'; path: string; content: string }
|
||||||
|
| { type: 'terminal'; sessionId: string }
|
||||||
|
| { type: 'diff'; leftPath: string; rightPath: string };
|
||||||
|
|
||||||
|
function isFileTab(tab: EditorTab): tab is EditorTab & { type: 'file' } {
|
||||||
|
return tab.type === 'file';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Interfaces for object shapes**, types for unions and aliases
|
||||||
|
- **PascalCase** for types/interfaces, no `I` prefix (`User` not `IUser`)
|
||||||
|
|
||||||
|
### CSS / Styling
|
||||||
|
|
||||||
|
- Scoped styles in components: `<style scoped lang="scss">`
|
||||||
|
- CSS custom properties for theming: `var(--app-surface-color)`
|
||||||
|
- Dark theme by default
|
||||||
|
- kebab-case for CSS classes: `.file-tree-node`, `.panel-header`
|
||||||
|
- SCSS variables for design tokens (colors, spacing)
|
||||||
|
|
||||||
|
### Formatting (JS/TS/Vue)
|
||||||
|
|
||||||
|
```
|
||||||
|
Semicolons: required
|
||||||
|
Quotes: single quotes
|
||||||
|
Trailing commas: none
|
||||||
|
Line width: 100 characters
|
||||||
|
Indentation: 2 spaces
|
||||||
|
```
|
||||||
|
|
||||||
|
### General Code Style
|
||||||
|
|
||||||
|
- **`const` by default** — only use `let` when reassignment is needed, never `var`
|
||||||
|
- **Arrow functions** for most cases: `const handleClick = () => { ... }`
|
||||||
|
- **Nullish coalescing**: `value ?? fallback` over `value || fallback`
|
||||||
|
- **Optional chaining**: `obj?.property?.method?.()` over nested null checks
|
||||||
|
- **No magic numbers/strings** — use named constants
|
||||||
|
- **Colocate related code** — don't scatter logic across files unnecessarily
|
||||||
|
- **One blank line** between logical chunks for readability — no more than one
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Naming Reference
|
||||||
|
|
||||||
|
| Thing | Convention | Example |
|
||||||
|
|-------|-----------|---------|
|
||||||
|
| Vue components | PascalCase | `UserProfileCard.vue` |
|
||||||
|
| TS/JS files | camelCase | `userProfileService.ts` |
|
||||||
|
| Rust files | snake_case | `process_manager.rs` |
|
||||||
|
| C files | snake_case | `memory_allocator.c` |
|
||||||
|
| Swift files | PascalCase | `CameraManager.swift` |
|
||||||
|
| Config files | kebab-case | `quasar.config.js` |
|
||||||
|
| Directories | camelCase (JS/TS), snake_case (Rust/C) | `userAuth/`, `user_auth/` |
|
||||||
|
| Variables/functions | camelCase (JS/TS), snake_case (Rust/C), camelCase (Swift) | |
|
||||||
|
| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_ATTEMPTS` |
|
||||||
|
| Types/Interfaces | PascalCase | `UserSession`, `FileTreeNode` |
|
||||||
|
| CSS classes | kebab-case | `.sidebar-panel-header` |
|
||||||
|
| Database tables | snake_case | `user_sessions` |
|
||||||
|
| Database columns | snake_case | `created_at`, `session_token` |
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* Console Logger Boot File (Tauri)
|
||||||
|
*
|
||||||
|
* Intercepts console.log/warn/error/info/debug and forwards them to the
|
||||||
|
* Rust backend via Tauri invoke. This allows all frontend console output
|
||||||
|
* to be captured in the build.sh console logs.
|
||||||
|
*
|
||||||
|
* The original console methods are preserved and still work normally,
|
||||||
|
* so you'll see output in both the WebView devtools AND the terminal logs.
|
||||||
|
*
|
||||||
|
* SETUP:
|
||||||
|
* 1. Copy this file to: app/src/boot/console-logger.ts
|
||||||
|
* 2. Add 'console-logger' to boot array in quasar.config.js
|
||||||
|
* 3. Add log_to_stdout command to Rust (see below)
|
||||||
|
* 4. Add chrono = "0.4" to Cargo.toml dependencies
|
||||||
|
*
|
||||||
|
* RUST COMMAND (add to src-tauri/src/commands.rs):
|
||||||
|
*
|
||||||
|
* use chrono::Local;
|
||||||
|
*
|
||||||
|
* #[tauri::command]
|
||||||
|
* pub fn log_to_stdout(level: String, message: String) {
|
||||||
|
* let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||||
|
* let prefix = match level.as_str() {
|
||||||
|
* "error" => "[ERROR]",
|
||||||
|
* "warn" => "[WARN]",
|
||||||
|
* "info" => "[INFO]",
|
||||||
|
* "debug" => "[DEBUG]",
|
||||||
|
* _ => "[LOG]",
|
||||||
|
* };
|
||||||
|
* println!("[{}] {} {}", timestamp, prefix, message);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Don't forget to register it in lib.rs:
|
||||||
|
* commands::log_to_stdout,
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
// Store original console methods
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log.bind(console),
|
||||||
|
warn: console.warn.bind(console),
|
||||||
|
error: console.error.bind(console),
|
||||||
|
info: console.info.bind(console),
|
||||||
|
debug: console.debug.bind(console),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to stringify arguments
|
||||||
|
function formatArgs(args: unknown[]): string {
|
||||||
|
return args
|
||||||
|
.map((arg) => {
|
||||||
|
if (typeof arg === 'string') return arg;
|
||||||
|
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to Rust backend
|
||||||
|
function logToBackend(level: string, args: unknown[]) {
|
||||||
|
const message = formatArgs(args);
|
||||||
|
invoke('log_to_stdout', { level, message }).catch(() => {
|
||||||
|
// Silently fail if Tauri isn't available (e.g., in browser dev mode)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override console methods
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
originalConsole.log(...args);
|
||||||
|
logToBackend('log', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = (...args: unknown[]) => {
|
||||||
|
originalConsole.warn(...args);
|
||||||
|
logToBackend('warn', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
originalConsole.error(...args);
|
||||||
|
logToBackend('error', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info = (...args: unknown[]) => {
|
||||||
|
originalConsole.info(...args);
|
||||||
|
logToBackend('info', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug = (...args: unknown[]) => {
|
||||||
|
originalConsole.debug(...args);
|
||||||
|
logToBackend('debug', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also capture unhandled errors and promise rejections
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
logToBackend('error', [`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`]);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
logToBackend('error', [`Unhandled promise rejection: ${event.reason}`]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log that the console logger is active
|
||||||
|
console.info('[Console Logger] Active - all console output will be logged to stdout');
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# .teamide/ Template
|
||||||
|
|
||||||
|
Template files for TeamIDE's project management system. When scaffolding a new project from Ideation, copy `project.yml` into the project's `.teamide/` directory and update the `name` field.
|
||||||
|
|
||||||
|
Plan and task files are not included here because they are generated dynamically from each project's `build_plan.md` content. See the "TeamIDE PM Files" section in `claude.md` for the conversion rules.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `project.yml` | Project configuration with board columns and discovery ignore patterns. Replace `PROJECT_NAME` with the actual project name. |
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: PROJECT_NAME
|
||||||
|
status: active
|
||||||
|
team: []
|
||||||
|
labels: []
|
||||||
|
references: []
|
||||||
|
board_columns:
|
||||||
|
tasks:
|
||||||
|
- backlog
|
||||||
|
- todo
|
||||||
|
- in-progress
|
||||||
|
- review
|
||||||
|
- done
|
||||||
|
plans:
|
||||||
|
- draft
|
||||||
|
- active
|
||||||
|
- complete
|
||||||
|
discovery_ignore:
|
||||||
|
- node_modules
|
||||||
|
- .git
|
||||||
|
- dist
|
||||||
|
- build
|
||||||
|
- vendor
|
||||||
|
- __pycache__
|
||||||
|
- .venv
|
||||||
|
- target
|
||||||