@dennisrongo/skills 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +45 -16
  2. package/package.json +1 -1
  3. package/skills/nextjs-app-router/SKILL.md +228 -175
  4. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  5. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  6. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  7. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  8. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  9. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  10. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  11. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  12. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  13. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  14. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  15. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  16. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  17. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  18. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  19. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  20. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  21. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  22. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  23. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
  24. package/skills/tauri-2-app/SKILL.md +381 -0
  25. package/skills/tauri-2-app/references/anti-patterns.md +434 -0
  26. package/skills/tauri-2-app/references/folder-layout.md +161 -0
  27. package/skills/tauri-2-app/references/good-patterns.md +477 -0
  28. package/skills/tauri-2-app/references/templates/build-rs.md +51 -0
  29. package/skills/tauri-2-app/references/templates/capabilities.md +68 -0
  30. package/skills/tauri-2-app/references/templates/cargo-toml.md +118 -0
  31. package/skills/tauri-2-app/references/templates/ci-workflow.md +99 -0
  32. package/skills/tauri-2-app/references/templates/encryption-mod.md +228 -0
  33. package/skills/tauri-2-app/references/templates/error-mod.md +126 -0
  34. package/skills/tauri-2-app/references/templates/lib-rs.md +98 -0
  35. package/skills/tauri-2-app/references/templates/macos-plist.md +89 -0
  36. package/skills/tauri-2-app/references/templates/main-rs.md +21 -0
  37. package/skills/tauri-2-app/references/templates/platform-traits.md +217 -0
  38. package/skills/tauri-2-app/references/templates/storage-mod.md +172 -0
  39. package/skills/tauri-2-app/references/templates/tauri-conf.md +136 -0
  40. package/skills/tauri-2-app/references/templates/use-tauri-command.md +194 -0
  41. package/skills/tauri-2-app/references/templates/vite-and-tsconfig.md +189 -0
@@ -0,0 +1,434 @@
1
+ # Anti-patterns
2
+
3
+ Each anti-pattern below was observed in a real Tauri 2 codebase. The "Why it's bad" explains the failure mode; "Fix" shows what to do instead.
4
+
5
+ ## Committed `.backup` / `.orig` / `.temp` files
6
+
7
+ **Symptom:** Files like `src-tauri/src/lib.rs.backup`, `platform/macos.rs.orig`, `lib.rs.temp` checked into git.
8
+
9
+ **Why it's bad:** They're either WIP code paths that compile and silently win on some branch, or merge-conflict artefacts that obscure history. Either way they confuse `grep`, break `cargo build` with duplicate-definition errors, and leak unfinished implementations.
10
+
11
+ **Fix:** Add `*.backup`, `*.orig`, `*.temp`, `*.rej` to `.gitignore`. Delete every existing one. Use a VCS branch for WIP, not a sibling file.
12
+
13
+ ## Plaintext API keys in `settings.json`
14
+
15
+ **Symptom:** `"openaiApiKey": "sk-proj-..."` in `settings.json`.
16
+
17
+ **Why it's bad:** Any process that can read the app's data directory (malware, backup-syncing services, a developer's grep) gets the key. App data directories are not encrypted on disk by default on any major OS.
18
+
19
+ **Fix:** Encrypt at rest. AES-256-GCM ciphertext + Argon2id-derived key + machine-bound salt. Store `EncryptedApiKey { ciphertext, nonce, salt }`. Auto-migrate legacy plaintext keys on load (log a warning, encrypt, save).
20
+
21
+ ## Tokens in `localStorage` / `sessionStorage`
22
+
23
+ **Symptom:** Frontend stores auth tokens via `localStorage.setItem("token", ...)`.
24
+
25
+ **Why it's bad:** Any XSS-equivalent path in the WebView reads them. Tauri's WebView is sandboxed from arbitrary websites, but extension content scripts, embedded WebViews, or future bundled JS dependencies can still touch `localStorage`.
26
+
27
+ **Fix:** Store secrets in Rust-side encrypted storage. Frontend asks the backend for tokens only when needed (e.g. for an outbound request). Better: have the backend make the outbound request and return the result.
28
+
29
+ ## `cfg!(target_os = "...")` in command bodies
30
+
31
+ **Symptom:**
32
+
33
+ ```rust
34
+ #[tauri::command]
35
+ pub fn open_settings(path: &str) -> Result<(), String> {
36
+ if cfg!(target_os = "macos") {
37
+ std::process::Command::new("open").arg(path).spawn()...
38
+ } else if cfg!(target_os = "windows") {
39
+ std::process::Command::new("explorer").arg(path).spawn()...
40
+ } else {
41
+ std::process::Command::new("xdg-open").arg(path).spawn()...
42
+ }
43
+ }
44
+ ```
45
+
46
+ **Why it's bad:** Every command grows the branch count. Tests need to mock the OS. Platform-specific dependencies leak into shared code. Adding a fourth OS means editing every command.
47
+
48
+ **Fix:** Define a trait in `platform/traits.rs`. Implement it per-OS in `platform/macos.rs` etc., each gated with `#[cfg(target_os = "...")]`. Inject the impl via Tauri State.
49
+
50
+ ```rust
51
+ #[tauri::command]
52
+ pub fn open_settings(
53
+ opener: tauri::State<'_, PlatformFileOpener>,
54
+ path: String,
55
+ ) -> Result<(), String> {
56
+ opener.open_in_default_app(Path::new(&path))
57
+ }
58
+ ```
59
+
60
+ ## Hand-rolled date / leap-year math
61
+
62
+ **Symptom:**
63
+
64
+ ```rust
65
+ fn get_timestamp() -> String {
66
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
67
+ let days_since_epoch = now / 86400;
68
+ // ... 50 lines of leap-year arithmetic ...
69
+ }
70
+ ```
71
+
72
+ **Why it's bad:** Boundary bugs are inevitable. Daylight savings, leap seconds, leap years on century boundaries, year-2038 — every special case is a future fire. The "I'll skip the dep" instinct loses to the dependency every time.
73
+
74
+ **Fix:**
75
+
76
+ ```rust
77
+ use chrono::Utc;
78
+
79
+ fn get_timestamp() -> String {
80
+ Utc::now().to_rfc3339() // "2026-01-15T10:30:00+00:00"
81
+ }
82
+
83
+ fn parse_timestamp(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
84
+ chrono::DateTime::parse_from_rfc3339(s).ok().map(|dt| dt.with_timezone(&Utc))
85
+ }
86
+ ```
87
+
88
+ ## Raw `std::fs::write(&user_supplied_path, ...)` in commands
89
+
90
+ **Symptom:**
91
+
92
+ ```rust
93
+ #[tauri::command]
94
+ pub fn save_file(path: String, content: String) -> Result<(), String> {
95
+ std::fs::write(&path, content).map_err(|e| e.to_string())
96
+ }
97
+ ```
98
+
99
+ **Why it's bad:** Bypasses Tauri's capability system. A misconfigured frontend can ask the command to overwrite `~/.ssh/authorized_keys` or `/etc/hosts`. The whole point of capabilities is to gate dangerous APIs.
100
+
101
+ **Fix:** Either route through `tauri-plugin-fs` (which respects `fs:scope-*` capability scopes), or restrict raw I/O to paths derived from `app_handle.path().app_data_dir()`.
102
+
103
+ ```rust
104
+ #[tauri::command]
105
+ pub fn save_app_data(
106
+ app: AppHandle,
107
+ filename: String,
108
+ content: String,
109
+ ) -> Result<(), String> {
110
+ let path = crate::storage::get_storage_path(&app, &filename)?;
111
+ std::fs::write(&path, content).map_err(|e| e.to_string())
112
+ }
113
+ ```
114
+
115
+ For user-chosen paths (export, save-as), use the dialog plugin to ask the user, **then** write to the path they explicitly picked.
116
+
117
+ ## Blocking I/O inside async commands without `spawn_blocking`
118
+
119
+ **Symptom:**
120
+
121
+ ```rust
122
+ #[tauri::command]
123
+ pub async fn pick_file(app: AppHandle) -> Result<Option<String>, String> {
124
+ let path = app.dialog().file().blocking_pick_file(); // BLOCKS the executor
125
+ Ok(path.map(|p| p.to_string()))
126
+ }
127
+ ```
128
+
129
+ **Why it's bad:** Tauri's IPC commands share an executor. A blocked task stalls every other in-flight command. Users see "the app froze" while one command pretends to be async.
130
+
131
+ **Fix:**
132
+
133
+ ```rust
134
+ #[tauri::command]
135
+ pub async fn pick_file(app: AppHandle) -> Result<Option<String>, String> {
136
+ let handle = app.clone();
137
+ let path = tokio::task::spawn_blocking(move || {
138
+ handle.dialog().file().blocking_pick_file()
139
+ })
140
+ .await
141
+ .map_err(|e| format!("Join error: {}", e))?;
142
+ Ok(path.map(|p| p.to_string()))
143
+ }
144
+ ```
145
+
146
+ ## Holding `std::sync::Mutex` across `.await`
147
+
148
+ **Symptom:**
149
+
150
+ ```rust
151
+ let guard = state.lock().map_err(|e| e.to_string())?;
152
+ let result = some_async_fn(&guard).await?; // 💥 blocks executor + deadlock risk
153
+ ```
154
+
155
+ **Why it's bad:** `std::sync::Mutex` is not `Send`-aware in the async sense. The compiler may even refuse to compile this if the future crosses thread boundaries. When it does compile, you can deadlock the runtime: thread holds the lock, awaits a future scheduled on the same thread, future needs the lock.
156
+
157
+ **Fix:** Read what you need from the guard, drop the guard, then await.
158
+
159
+ ```rust
160
+ let snapshot = {
161
+ let guard = state.lock().map_err(|e| e.to_string())?;
162
+ guard.settings.clone().ok_or("not loaded")?
163
+ };
164
+ let result = some_async_fn(&snapshot).await?;
165
+ ```
166
+
167
+ If genuinely shared mutable state must persist across awaits, use `tokio::sync::Mutex` and call `.lock().await` instead.
168
+
169
+ ## Missing `windows_subsystem = "windows"`
170
+
171
+ **Symptom:** `main.rs` lacks `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]`.
172
+
173
+ **Why it's bad:** Windows release builds open a console window behind the Tauri window. Looks unprofessional and adds a process that can't be cleanly closed without killing the app.
174
+
175
+ **Fix:** Add the attribute as the **first line** of `main.rs`. Debug builds keep their console (useful for `println!` during development).
176
+
177
+ ## No `tauri-plugin-single-instance`
178
+
179
+ **Symptom:** App allows multiple concurrent launches.
180
+
181
+ **Why it's bad:** Both instances fight for the global hotkey (one wins, the other silently registers nothing). Both try to bind the same lock files / WebSocket port. Audio device reservation may pick the wrong instance. Settings written by one are clobbered by the other.
182
+
183
+ **Fix:** Wire `tauri-plugin-single-instance` **first** in the builder chain. On second launch, focus the existing window:
184
+
185
+ ```rust
186
+ .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
187
+ if let Some(w) = app.get_webview_window("main") {
188
+ let _ = w.show();
189
+ let _ = w.set_focus();
190
+ let _ = w.unminimize();
191
+ }
192
+ }))
193
+ ```
194
+
195
+ ## `devtools: true` in production `tauri.conf.json`
196
+
197
+ **Symptom:**
198
+
199
+ ```json
200
+ "windows": [{ "devtools": true }]
201
+ ```
202
+
203
+ **Why it's bad:** Ships an inspect-element / network-tab surface to every user. They can read the app's state, mutate it, and reverse-engineer internal APIs trivially. Even when the app is open-source, it makes accidental data exposure easier (anyone debugging a friend's app sees their notes).
204
+
205
+ **Fix:** Omit the field (Tauri defaults to off in release) or gate via a feature flag. If you need DevTools in a built debug binary, use `#[cfg(debug_assertions)]` to open them programmatically in `.setup()`.
206
+
207
+ ## Blanket capability permissions
208
+
209
+ **Symptom:**
210
+
211
+ ```json
212
+ "permissions": ["core:default", "fs:default", "dialog:default"]
213
+ ```
214
+
215
+ … without listing the specific scopes the app uses.
216
+
217
+ **Why it's bad:** Capability files are the audit surface for what the frontend can ask Rust to do. Blanket `default` permissions vary across plugin versions — an update can silently widen what the frontend can call.
218
+
219
+ **Fix:** List exactly the action scopes used:
220
+
221
+ ```json
222
+ "permissions": [
223
+ "core:default",
224
+ "fs:allow-read-file",
225
+ "fs:allow-write-file",
226
+ "dialog:allow-open",
227
+ "dialog:allow-save"
228
+ ]
229
+ ```
230
+
231
+ ## CSP `null` without explanation
232
+
233
+ **Symptom:**
234
+
235
+ ```json
236
+ "app": { "security": { "csp": null } }
237
+ ```
238
+
239
+ **Why it's bad:** Disables every Content Security Policy protection in the WebView. Any script the WebView fetches (intentional or injected) runs with full access. CSP `null` may be necessary for some apps (e.g. those that load remote content), but it should be a documented decision.
240
+
241
+ **Fix:** Set an explicit CSP that whitelists `tauri://localhost` plus any external origins the app legitimately uses. If `null` is required, add a comment in `tauri.conf.json` (JSON doesn't support comments — put it in `CLAUDE.md` or `README.md`) explaining why.
242
+
243
+ ## Bundling external binaries without listing them
244
+
245
+ **Symptom:** App calls `Command::new("ffmpeg")` but `ffmpeg` is not in `tauri.conf.json` `bundle.externalBin`.
246
+
247
+ **Why it's bad:** Works in `cargo run` (PATH finds the system ffmpeg) but the installer doesn't include the binary. Users get a "binary not found" error in production.
248
+
249
+ **Fix:**
250
+
251
+ ```json
252
+ "bundle": {
253
+ "externalBin": ["binaries/ffmpeg"]
254
+ }
255
+ ```
256
+
257
+ Place the per-platform binary at `binaries/ffmpeg-<target-triple>` (e.g. `ffmpeg-aarch64-apple-darwin`). Tauri's bundler picks the right one per build target.
258
+
259
+ ## macOS `Info.plist` missing usage descriptions
260
+
261
+ **Symptom:** App calls `AVAudioSession.requestRecordPermission` but `Info.plist` lacks `NSMicrophoneUsageDescription`.
262
+
263
+ **Why it's bad:** macOS refuses the permission prompt silently — the call returns "denied" without showing the user a dialog. The user has no idea the app needs the mic.
264
+
265
+ **Fix:** For every OS-level permission the app requests, add a usage description string explaining *why*:
266
+
267
+ ```xml
268
+ <key>NSMicrophoneUsageDescription</key>
269
+ <string>This app needs the microphone to record voice input.</string>
270
+ <key>NSAccessibilityUsageDescription</key>
271
+ <string>This app needs Accessibility access to insert text into other windows.</string>
272
+ ```
273
+
274
+ ## Over-broad macOS entitlements
275
+
276
+ **Symptom:** `entitlements.plist` has `com.apple.security.cs.allow-jit` and `com.apple.security.cs.allow-unsigned-executable-memory` enabled by default.
277
+
278
+ **Why it's bad:** These weaken the hardened runtime. Apple's notarization service will reject them if they're not justified; if accepted, they expand the attack surface (allowing JIT means write+execute pages are possible).
279
+
280
+ **Fix:** Start with an empty entitlements file. Add entitlements only when a build failure or runtime error proves you need them. Common minimally-required entitlements:
281
+
282
+ - `com.apple.security.device.audio-input` — microphone access
283
+ - `com.apple.security.automation.apple-events` — sending Apple events (e.g. for keystroke insertion)
284
+
285
+ Never copy a full entitlements file from another project — audit each line.
286
+
287
+ ## Two `[[bin]]` entries in one crate
288
+
289
+ **Symptom:**
290
+
291
+ ```toml
292
+ [[bin]]
293
+ name = "myapp"
294
+ path = "src/main.rs"
295
+
296
+ [[bin]]
297
+ name = "myapp-sidecar"
298
+ path = "src/sidecar/main.rs"
299
+ ```
300
+
301
+ **Why it's bad:** Both binaries pull the full Tauri dependency tree (compile time, binary size). Conditional compilation gets tangled. Cross-compilation for different targets per binary is awkward. The "sidecar" pattern in Tauri usually means a separate process — that separate process deserves a separate crate.
302
+
303
+ **Fix:** Use a Cargo workspace:
304
+
305
+ ```
306
+ src-tauri/
307
+ Cargo.toml # workspace root
308
+ app/
309
+ Cargo.toml # main app crate
310
+ src/main.rs
311
+ sidecar/
312
+ Cargo.toml # sidecar crate (no Tauri dep)
313
+ src/main.rs
314
+ ```
315
+
316
+ If you genuinely need only one extra bin with no Tauri deps, a single workspace with two crates is still cleaner than two `[[bin]]` entries fighting over shared deps.
317
+
318
+ ## Multiple `createRoot()` calls
319
+
320
+ **Symptom:** `main.tsx` calls `createRoot(...)` more than once, or renders before the DOM root exists.
321
+
322
+ **Why it's bad:** React 18+'s strict mode hates multiple roots on the same node. Hydration warnings, "Cannot update a component while rendering" errors.
323
+
324
+ **Fix:** One entry point. Look up the root once. Render once.
325
+
326
+ ```ts
327
+ // src/main.tsx
328
+ const rootElement = document.getElementById("app");
329
+ if (rootElement) {
330
+ createRoot(rootElement).render(<App />);
331
+ }
332
+ ```
333
+
334
+ ## Inline `invoke()` calls in components
335
+
336
+ **Symptom:**
337
+
338
+ ```tsx
339
+ function MyComponent() {
340
+ const [data, setData] = useState(null);
341
+ useEffect(() => {
342
+ invoke('get_settings').then(setData).catch(console.error);
343
+ }, []);
344
+ // ...
345
+ }
346
+ ```
347
+
348
+ **Why it's bad:** Every component reinvents loading/error handling. Refactoring a command name means grepping every file. Type safety dies — `data` is `any`.
349
+
350
+ **Fix:** Use `useTauriCommand<T>`. Per-domain hooks compose it:
351
+
352
+ ```ts
353
+ // hooks/useSettings.ts
354
+ export function useSettings() {
355
+ return useTauriLoad<Settings>({ command: "get_settings" });
356
+ }
357
+ ```
358
+
359
+ ```tsx
360
+ function MyComponent() {
361
+ const { data: settings, isLoading, error } = useSettings();
362
+ // ...
363
+ }
364
+ ```
365
+
366
+ ## Hardcoded user-specific config in templates
367
+
368
+ **Symptom:** A scaffold writes `"identifier": "com.acme.myapp"`, `"endpoints": ["https://cdn.example.com/manifest.json"]`, or any non-empty `"pubkey": "..."` value copied from a previous project.
369
+
370
+ **Why it's bad:** Bundle identifiers are immutable user-facing IDs (OS-level keychain, settings paths, notification permissions are all keyed on them — changing one is migration pain forever). Updater pubkeys lock the app's update channel to whoever holds the matching private key. R2/S3 URLs leak previous customers.
371
+
372
+ **Fix:** Ask the user for these values. Refuse to scaffold if they're missing or look like placeholders. Never copy them from another project.
373
+
374
+ ## `.env` committed to git
375
+
376
+ **Symptom:** `.env` shows up in `git ls-files`.
377
+
378
+ **Why it's bad:** Credentials, API keys, signing certificate paths leak. GitHub scans for known token formats and may auto-revoke them — better than leaving them live, but disruptive.
379
+
380
+ **Fix:** Only `.env.example` is committed (keys only, no values). `.env` is in `.gitignore`. If `.env` was ever committed, rotate the secrets immediately — git history retains them even after deletion.
381
+
382
+ ## Silent error swallowing
383
+
384
+ **Symptom:**
385
+
386
+ ```rust
387
+ let _ = some_operation();
388
+ // or
389
+ if let Err(_) = some_operation() {}
390
+ ```
391
+
392
+ **Why it's bad:** A future bug surfaces as "the app does nothing" with no log line to find it from. Months later, you're stepping through a debugger trying to find which silent failure broke the flow.
393
+
394
+ **Fix:** Either handle the error meaningfully or log it:
395
+
396
+ ```rust
397
+ if let Err(e) = some_operation() {
398
+ tracing::warn!("some_operation failed: {}", e);
399
+ }
400
+ ```
401
+
402
+ Use `tracing::error!` for errors that should page someone, `warn!` for "something went wrong but we recovered", `info!` for normal-path state changes, `debug!` for diagnostic detail.
403
+
404
+ ## Heavy startup work blocks the window
405
+
406
+ **Symptom:**
407
+
408
+ ```rust
409
+ .setup(|app| {
410
+ let model = load_huge_ml_model()?; // 5 seconds
411
+ let devices = enumerate_audio_devices()?; // 2 seconds
412
+ // ... window appears after 7+ seconds
413
+ Ok(())
414
+ })
415
+ ```
416
+
417
+ **Why it's bad:** Users see a missing window for seconds and assume the app crashed.
418
+
419
+ **Fix:** Show the window first (Tauri does this by default — don't `.hide()` it). Move heavy work to a background thread that emits events back to the frontend:
420
+
421
+ ```rust
422
+ .setup(|app| {
423
+ let handle = app.handle().clone();
424
+ std::thread::spawn(move || {
425
+ match load_huge_ml_model() {
426
+ Ok(_) => { let _ = handle.emit("model-ready", ()); }
427
+ Err(e) => { let _ = handle.emit("model-error", e.to_string()); }
428
+ }
429
+ });
430
+ Ok(())
431
+ })
432
+ ```
433
+
434
+ The frontend renders a "Loading model…" state until it receives `model-ready`.
@@ -0,0 +1,161 @@
1
+ # Tauri 2 App — Canonical Folder Layout
2
+
3
+ The layout below is the source of truth for `scaffold-app`. Every rule has a reason — when in doubt, default to the structure here.
4
+
5
+ ## Top-level tree
6
+
7
+ ```
8
+ {{app-name}}/
9
+ ├── package.json # frontend deps + scripts (dev, build, tauri:dev, tauri:build)
10
+ ├── package-lock.json # commit it (or pnpm-lock.yaml / bun.lockb — whichever PM)
11
+ ├── tsconfig.json # strict TS config
12
+ ├── tsconfig.node.json # for vite.config.ts itself
13
+ ├── vite.config.ts # base: './', port 5173, ignores src-tauri
14
+ ├── index.html # single entry; anti-flash theme script if dark/light is in scope
15
+ ├── .gitignore # MUST include: src-tauri/target/, dist/, .env, *.backup, *.orig, *.temp
16
+ ├── .env.example # only the keys, never the values
17
+ ├── README.md
18
+ ├── .github/
19
+ │ └── workflows/
20
+ │ ├── unit-tests.yml # cross-platform cargo test on PRs + pushes
21
+ │ └── publish.yml # (optional) tag-triggered release pipeline
22
+ ├── src/ # FRONTEND (TS / React)
23
+ │ ├── main.tsx # createRoot(...).render(<App />)
24
+ │ ├── App.tsx
25
+ │ ├── tauriReady.ts # isTauriReady() guard
26
+ │ ├── types.ts # cross-cutting frontend types
27
+ │ ├── styles/ # or styles.css if simple
28
+ │ ├── hooks/
29
+ │ │ ├── useTauriCommand.ts # generic invoke() wrapper with loading/error/data
30
+ │ │ └── use<Domain>.ts # per-domain hooks composing useTauriCommand
31
+ │ ├── components/ # PascalCase .tsx files
32
+ │ └── utils/
33
+ └── src-tauri/ # RUST BACKEND
34
+ ├── Cargo.toml # [lib] name = "{{app_name}}_lib", target-cfg deps, release profile
35
+ ├── Cargo.lock # commit (binary crate)
36
+ ├── build.rs # links Accelerate/Metal (macOS), user32 (Windows), pthread/m (Linux)
37
+ ├── tauri.conf.json # productName, identifier, bundle, plugins, windows
38
+ ├── tauri.local-no-updater.conf.json # (optional) local-only overlay disabling updater
39
+ ├── Info.plist # macOS bundle id + permission usage descriptions
40
+ ├── entitlements.plist # macOS hardened-runtime entitlements
41
+ ├── icons/
42
+ │ ├── icon.png # 1024x1024 source
43
+ │ ├── icon.ico # Windows
44
+ │ ├── icon.icns # macOS
45
+ │ ├── 32x32.png
46
+ │ ├── 128x128.png
47
+ │ └── 128x128@2x.png
48
+ ├── capabilities/
49
+ │ └── default.json # permissions for the "main" window
50
+ ├── binaries/ # (optional) externalBin entries — ffmpeg, sidecar, etc.
51
+ ├── src/
52
+ │ ├── main.rs # #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + macOS embed_plist + fn main()
53
+ │ ├── lib.rs # pub mod ...; pub use ...; pub fn run() { tauri::Builder... }
54
+ │ ├── commands/
55
+ │ │ ├── mod.rs # pub mod ...; pub use *;
56
+ │ │ ├── system.rs # generic: write_file, dialogs, notifications
57
+ │ │ └── <domain>.rs # one file per cohesive command set
58
+ │ ├── state/
59
+ │ │ └── mod.rs # pub struct AppState { app_handle, settings, ... }
60
+ │ ├── storage/
61
+ │ │ └── mod.rs # get_app_data_dir, get_storage_path, load_json, save_json, ids, timestamps
62
+ │ ├── settings/
63
+ │ │ ├── mod.rs # Settings struct + AppHandleSettings extension trait
64
+ │ │ ├── defaults.rs # impl Default for Settings + sub-settings
65
+ │ │ ├── storage.rs # load_settings, save_settings, deserialize_settings (with migrations)
66
+ │ │ └── commands.rs # get_settings, save_settings_command, reset_settings_command
67
+ │ ├── platform/
68
+ │ │ ├── mod.rs # re-exports + #[cfg(target_os)] gated module decls
69
+ │ │ ├── traits.rs # PermissionChecker, FileOpener, TextInserter, WaveformWindowBuilder
70
+ │ │ ├── macos.rs # #[cfg(target_os = "macos")] impls
71
+ │ │ ├── windows.rs # #[cfg(target_os = "windows")] impls
72
+ │ │ ├── linux.rs # #[cfg(target_os = "linux")] impls
73
+ │ │ └── wrappers.rs # Send + Sync wrappers managed in Tauri State
74
+ │ ├── error/
75
+ │ │ └── mod.rs # into_string_err! macro, ResultExt trait
76
+ │ ├── encryption/ # (optional)
77
+ │ │ └── mod.rs # AES-256-GCM + Argon2id + machine-bound salt
78
+ │ ├── tray.rs # (optional) tray icon menu, click events
79
+ │ └── <domain>/ # one folder per major feature area
80
+ │ ├── mod.rs
81
+ │ └── ...
82
+ └── tests/ # cargo integration tests (one binary per file)
83
+ ├── common/
84
+ │ └── mod.rs # shared test helpers (tempfile fixtures, mock builders)
85
+ ├── system_test.rs
86
+ ├── storage_test.rs
87
+ ├── settings_test.rs
88
+ └── <domain>_test.rs
89
+ ```
90
+
91
+ ## Why this shape
92
+
93
+ ### `src-tauri/src/` is modular, not monolithic
94
+
95
+ A flat `src-tauri/src/main.rs` + huge `lib.rs` becomes unmaintainable past ~1500 lines. Splitting by feature folder (`audio/`, `history/`, `whisper/`, …) keeps related code together and lets `cargo test --test <name>` target one slice. The pattern enforces "one purpose per module" by making bloated modules awkward — you can't easily add a tenth file to a folder that's already named for something specific.
96
+
97
+ ### `commands/` is its own module, not scattered across feature folders
98
+
99
+ Tauri commands are an **IPC boundary**, not a feature concern. Keeping them all under `commands/` means:
100
+ - One place to look when grepping for "what can the frontend call?"
101
+ - `tauri::generate_handler![...]` registration is a single, reviewable list
102
+ - Capability scopes line up with command modules
103
+ - Feature modules (`audio`, `history`) can change internals without touching the frontend API
104
+
105
+ Commands are thin: they parse inputs, call into a feature module, convert errors to `String`, and return. **No business logic in command bodies.**
106
+
107
+ ### `state/`, `storage/`, `settings/` are separate
108
+
109
+ - **`state/`**: in-memory shared state (the `AppState` managed by Tauri). Lives only while the app runs.
110
+ - **`storage/`**: disk persistence utilities (`load_json`, `save_json`, paths, IDs, timestamps). The shared file-I/O layer that every module uses.
111
+ - **`settings/`**: the specific shape of `settings.json` (with serde derives, migrations, defaults). Built on top of `storage/`.
112
+
113
+ Conflating these leads to "settings holds the app handle" coupling that breaks tests.
114
+
115
+ ### `platform/` uses traits + `#[cfg(target_os = "...")]`, not `cfg!()` checks
116
+
117
+ Anti-pattern: a command body that branches on `cfg!(target_os = "macos")`. Every command grows the branch count, tests have to mock the OS, and platform-specific dependencies leak across the codebase.
118
+
119
+ Pattern: define a trait in `platform/traits.rs` (`PermissionChecker`, `FileOpener`, `TextInserter`). Implement it in `platform/macos.rs`, `windows.rs`, `linux.rs` (each gated with `#[cfg(target_os = "...")]`). Expose `Send + Sync` wrappers via `platform/wrappers.rs` that Tauri's `State<>` can manage. Commands receive `State<'_, PlatformPermissionChecker>` and never see `cfg!`.
120
+
121
+ Tests can then assert `Send + Sync` on the wrappers and substitute mock impls.
122
+
123
+ ### `tests/common/mod.rs` is one file per integration binary
124
+
125
+ Cargo compiles each file in `tests/` as a separate binary. That means `tests/audio_test.rs` and `tests/history_test.rs` cannot share helpers via a normal module — they need `common/mod.rs` referenced from both.
126
+
127
+ ```rust
128
+ // tests/audio_test.rs
129
+ mod common;
130
+ use common::*;
131
+ ```
132
+
133
+ ### Capability files per window
134
+
135
+ Tauri 2 capabilities are scoped per window. `capabilities/default.json` covers the `main` window. Auxiliary windows (overlay, waveform, settings popup) get their own capability files. This matters because each window should only have the permissions it actually uses — a transparent overlay window does not need `fs:allow-write-file`.
136
+
137
+ ### Icons are in `src-tauri/icons/`, not `src/assets/`
138
+
139
+ The frontend bundle doesn't ship the icons — Tauri's bundler reads them from `src-tauri/icons/` at build time and embeds them in the platform-specific installer. Keeping them in `src/` confuses both Vite and Tauri.
140
+
141
+ ## Dependency rules (enforced)
142
+
143
+ - `src-tauri/src/commands/**` may call into `state/`, `settings/`, `storage/`, `platform/`, and any `<domain>/`. The reverse is forbidden.
144
+ - `src-tauri/src/<domain>/**` may use `storage/` and `platform::traits::*`, but **must not** depend on `commands/` or hold an `AppHandle` long-term. Pass `&AppHandle` where needed and return.
145
+ - `src-tauri/src/platform/macos.rs|windows.rs|linux.rs` are the **only** files that may contain `cfg!(target_os = "...")` checks at runtime. All other modules go through the trait.
146
+ - `src/hooks/use<Domain>.ts` compose `useTauriCommand` — frontend **components** never call `invoke()` directly.
147
+ - `src/components/**` may use hooks from `src/hooks/`, but a hook may **not** import from a component.
148
+
149
+ ## Files you do NOT commit
150
+
151
+ Add to `.gitignore` and verify they're absent:
152
+
153
+ - `src-tauri/target/`
154
+ - `dist/`
155
+ - `node_modules/`
156
+ - `.env` (only `.env.example` is committed)
157
+ - `*.backup`, `*.orig`, `*.temp`, `*.rej` (WIP / merge-conflict artefacts)
158
+ - `src-tauri/binaries/*` if those are downloaded at build time by CI (commit a `.gitkeep` instead)
159
+ - Generated icons output (`icons/icon.icns`, `icons/icon.ico` if you regenerate from `icon.png` each build — but most projects DO commit these, so it's a project-by-project call)
160
+
161
+ If you find `lib.rs.backup` / `macos.rs.orig` / `lib.rs.temp` in `src-tauri/src/`, that's a sign the previous developer was hand-merging in a way the VCS should have handled. Delete them; don't ship them.