@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,477 @@
1
+ # Good Patterns
2
+
3
+ The patterns below survived real production use. Each one has a failure mode it prevents — the rationale matters more than the rule.
4
+
5
+ ## Architecture
6
+
7
+ ### Thin frontend, rich Rust backend
8
+
9
+ All audio, file I/O, system permissions, OS-specific behavior, and long-running computation lives in Rust. The frontend's only job is to invoke commands and render state.
10
+
11
+ **Why:** Tauri's selling point over Electron is performance and OS integration. Pushing work into the frontend reintroduces the same memory and startup costs you came to Tauri to avoid. It also makes the frontend untestable without a running Tauri instance.
12
+
13
+ **How to spot violations:**
14
+ - `import { open } from '@tauri-apps/plugin-fs'` in component code reading large files
15
+ - Frontend code parsing/processing audio buffers
16
+ - `setInterval` polling driving state that could be event-driven from Rust via `app.emit("...", payload)`
17
+
18
+ ### Modular `src-tauri/src/`
19
+
20
+ One module per concern (`commands/`, `state/`, `storage/`, `platform/`, `settings/`, `error/`, plus per-feature folders). `lib.rs` re-exports a public API for tests and registers commands; `main.rs` is the tiny shim that calls `pub fn run()`.
21
+
22
+ **Why:** A monolithic `lib.rs` past ~1500 lines is unmaintainable. Splitting by feature lets `cargo test --test <feature>` target one slice and surfaces accidental cross-cutting dependencies.
23
+
24
+ ### Trait-based platform abstractions
25
+
26
+ `platform/traits.rs` defines `PermissionChecker`, `FileOpener`, `TextInserter`, etc. `platform/macos.rs|windows.rs|linux.rs` implement them gated with `#[cfg(target_os = "...")]`. Wrappers in `platform/wrappers.rs` implement `Send + Sync` and are managed in Tauri State.
27
+
28
+ **Why:** Without this, `cfg!(target_os = "...")` checks scatter through command bodies, untestable and tedious to audit. The trait gives one place to swap behavior per OS and a clean seam for mocking in tests.
29
+
30
+ ```rust
31
+ // platform/traits.rs
32
+ pub trait PermissionChecker: Send + Sync {
33
+ fn check_microphone(&self) -> bool;
34
+ fn check_accessibility(&self) -> bool;
35
+ fn open_system_settings(&self, kind: &str) -> Result<(), String>;
36
+ }
37
+ ```
38
+
39
+ ```rust
40
+ // platform/wrappers.rs
41
+ pub struct PlatformPermissionChecker {
42
+ #[cfg(target_os = "macos")] inner: crate::platform::MacOsPlatform,
43
+ #[cfg(target_os = "windows")] inner: crate::platform::WindowsPlatform,
44
+ #[cfg(target_os = "linux")] inner: crate::platform::LinuxPlatform,
45
+ }
46
+ ```
47
+
48
+ ## Tauri runtime
49
+
50
+ ### Single-instance plugin first
51
+
52
+ ```rust
53
+ tauri::Builder::default()
54
+ .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
55
+ if let Some(window) = app.get_webview_window("main") {
56
+ let _ = window.show();
57
+ let _ = window.set_focus();
58
+ let _ = window.unminimize();
59
+ }
60
+ }))
61
+ .plugin(tauri_plugin_opener::init())
62
+ // ...
63
+ ```
64
+
65
+ **Why:** Without single-instance, double-clicking the launcher spawns a second process that competes for the global hotkey, the audio device, the lock file, and the WebSocket. The plugin must register **first** so the second instance is intercepted before any other plugin initializes resources.
66
+
67
+ ### `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` in `main.rs`
68
+
69
+ ```rust
70
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
71
+
72
+ #[cfg(target_os = "macos")]
73
+ embed_plist::embed_info_plist!("../Info.plist");
74
+
75
+ fn main() { {{app_name}}_lib::run() }
76
+ ```
77
+
78
+ **Why:** Without it, Windows release builds open a console window behind the Tauri window. With it, debug builds still get a console (useful for `println!` while developing) and release builds are clean.
79
+
80
+ `embed_plist` ensures macOS standalone binaries (not run via `.app` bundle) still carry their plist.
81
+
82
+ ### Show window FIRST in `.setup()`
83
+
84
+ ```rust
85
+ .setup(|app| {
86
+ let main_window = app.get_webview_window("main").unwrap();
87
+ // (window is already visible by default — don't .hide() it)
88
+
89
+ // Reset any stuck state from a previous run (force-quit / crash)
90
+ crate::cancellation::force_clear_transcription_handle();
91
+
92
+ // Now do heavy initialization on a background thread
93
+ let handle = app.handle().clone();
94
+ std::thread::spawn(move || {
95
+ // model load, audio device enumeration, etc.
96
+ // emit events back: handle.emit("init-progress", ...);
97
+ });
98
+
99
+ Ok(())
100
+ })
101
+ ```
102
+
103
+ **Why:** Users perceive a Tauri app as broken if the window doesn't appear within ~500ms. Synchronous setup work (model loading, device enumeration, network calls) blocks the window from drawing.
104
+
105
+ ### Reset state on startup
106
+
107
+ ```rust
108
+ // In .setup(), before any "ready" event:
109
+ let was_in_progress = crate::cancellation::has_ongoing_transcription();
110
+ crate::cancellation::force_clear_transcription_handle();
111
+ if was_in_progress {
112
+ tracing::warn!("Cleared stuck state from previous session (app likely force-quit)");
113
+ }
114
+ ```
115
+
116
+ **Why:** Crashes and force-quits leave `static` / global state in odd places. Treating startup as recovery makes the app robust to ungraceful shutdowns.
117
+
118
+ ## Commands
119
+
120
+ ### `#[tauri::command]` shape
121
+
122
+ ```rust
123
+ #[tauri::command]
124
+ pub async fn save_user_setting(
125
+ state: State<'_, std::sync::Mutex<AppState>>,
126
+ app_handle: AppHandle,
127
+ key: String,
128
+ value: String,
129
+ ) -> Result<(), String> {
130
+ // Hold the lock for the minimum time
131
+ let mut settings = {
132
+ let guard = state.lock().map_err(|e| e.to_string())?;
133
+ guard.settings.clone().ok_or("settings not loaded")?
134
+ };
135
+
136
+ settings.set(&key, &value);
137
+
138
+ // Persist outside the lock
139
+ crate::settings::save_settings(&app_handle, &settings)?;
140
+
141
+ // Re-take the lock to commit the in-memory copy
142
+ state.lock().map_err(|e| e.to_string())?.settings = Some(settings);
143
+
144
+ Ok(())
145
+ }
146
+ ```
147
+
148
+ **Why:** Holding `std::sync::Mutex` across `.await` deadlocks the Tauri runtime. Clone what you need, drop the guard, do the work, re-take the guard to commit.
149
+
150
+ ### `spawn_blocking` for sync work inside async commands
151
+
152
+ ```rust
153
+ #[tauri::command]
154
+ pub async fn open_file_dialog(app: AppHandle) -> Result<Option<String>, String> {
155
+ let app_handle = app.clone();
156
+ let path = tokio::task::spawn_blocking(move || {
157
+ app_handle
158
+ .dialog()
159
+ .file()
160
+ .add_filter("Audio", &["mp3", "wav"])
161
+ .blocking_pick_file()
162
+ })
163
+ .await
164
+ .map_err(|e| format!("Join error: {}", e))?;
165
+
166
+ Ok(path.map(|fp| fp.to_string()))
167
+ }
168
+ ```
169
+
170
+ **Why:** `blocking_pick_file()` blocks the calling thread. Awaiting it inside an `async fn` stalls Tauri's IPC executor and any other in-flight command queues up.
171
+
172
+ ### Error conversion at the IPC boundary
173
+
174
+ ```rust
175
+ // error/mod.rs
176
+ #[macro_export]
177
+ macro_rules! into_string_err {
178
+ ($expr:expr) => { $expr.map_err(|e| e.to_string()) };
179
+ ($expr:expr, $msg:expr) => { $expr.map_err(|e| format!("{}: {}", $msg, e)) };
180
+ }
181
+
182
+ pub trait ResultExt<T, E>: Sized {
183
+ fn into_string(self) -> Result<T, String>;
184
+ fn into_string_msg(self, msg: &str) -> Result<T, String>;
185
+ }
186
+
187
+ impl<T, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
188
+ fn into_string(self) -> Result<T, String> { self.map_err(|e| e.to_string()) }
189
+ fn into_string_msg(self, msg: &str) -> Result<T, String> {
190
+ self.map_err(|e| format!("{}: {}", msg, e))
191
+ }
192
+ }
193
+ ```
194
+
195
+ ```rust
196
+ // commands/<domain>.rs
197
+ use crate::error::ResultExt;
198
+
199
+ #[tauri::command]
200
+ pub fn write_file(path: String, content: String) -> Result<(), String> {
201
+ std::fs::write(&path, content).into_string_msg("Failed to write file")
202
+ }
203
+ ```
204
+
205
+ **Why:** Tauri commands must return `Result<T, String>` because the error crosses an IPC boundary. Internal APIs use `thiserror` enums for type safety. The macro / trait keeps the conversion one-line at the boundary.
206
+
207
+ ## State & storage
208
+
209
+ ### Shared `AppState` managed in a `Mutex`
210
+
211
+ ```rust
212
+ // state/mod.rs
213
+ pub struct AppState {
214
+ pub app_handle: Option<AppHandle>,
215
+ pub settings: Option<Settings>,
216
+ // domain-specific managers
217
+ }
218
+
219
+ // lib.rs, inside .setup():
220
+ let state = std::sync::Mutex::new(AppState::new());
221
+ app.manage(state);
222
+ ```
223
+
224
+ **Why:** Tauri's `State<T>` is a managed singleton accessible from every command. Using `std::sync::Mutex` (not `tokio::sync::Mutex`) is fine **as long as** you never hold the lock across an `.await` — which is the pattern enforced above.
225
+
226
+ ### Settings with serde camelCase + migrations
227
+
228
+ ```rust
229
+ #[derive(Debug, Clone, Serialize, Deserialize)]
230
+ #[serde(rename_all = "camelCase", default)]
231
+ pub struct Settings {
232
+ pub default_microphone: Option<String>,
233
+ pub hands_free_mode: bool,
234
+ pub local_whisper_model: String,
235
+ // ...
236
+ }
237
+ ```
238
+
239
+ **Why:**
240
+ - `rename_all = "camelCase"` matches frontend naming, no manual key remapping.
241
+ - `default` on the struct makes every field optional — old settings files load with sensible fallbacks for new fields.
242
+ - Add migration logic in a `deserialize_settings(&Value)` pass for non-trivial shape changes (e.g. `"English"` → `"en"`).
243
+
244
+ ### Shared storage utilities
245
+
246
+ ```rust
247
+ // storage/mod.rs
248
+ pub fn get_app_data_dir(app: &AppHandle) -> Result<PathBuf, String> {
249
+ let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
250
+ fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
251
+ Ok(dir)
252
+ }
253
+
254
+ pub fn load_json<T: serde::de::DeserializeOwned>(
255
+ app: &AppHandle, filename: &str,
256
+ ) -> Vec<T> { /* read or return empty */ }
257
+
258
+ pub fn save_json<T: serde::Serialize>(
259
+ app: &AppHandle, filename: &str, entries: &[T],
260
+ ) -> Result<(), String> { /* serialize + write */ }
261
+ ```
262
+
263
+ **Why:** Every persistent feature (settings, history, error log) needs the same primitives. Centralizing them ensures one consistent failure mode (empty on missing file, log + empty on parse error) and one place to add cache/locking later.
264
+
265
+ ## Security
266
+
267
+ ### Encrypted secrets at rest
268
+
269
+ ```rust
270
+ // encryption/mod.rs
271
+ #[derive(Serialize, Deserialize)]
272
+ pub struct EncryptedApiKey {
273
+ pub ciphertext: String, // base64 AES-256-GCM ciphertext
274
+ pub nonce: String, // base64 12-byte nonce
275
+ pub salt: String, // base64 Argon2id salt
276
+ }
277
+
278
+ pub fn encrypt_api_key(plaintext: &str) -> Result<EncryptedApiKey, String> { /* ... */ }
279
+ pub fn decrypt_api_key(enc: &EncryptedApiKey) -> Result<String, String> { /* ... */ }
280
+ ```
281
+
282
+ The key is derived from a hardware-bound machine ID:
283
+ - macOS: `IOPlatformUUID` via `ioreg -rd1 -c IOPlatformExpertDevice`
284
+ - Windows: `wmic csproduct get uuid` (use `creation_flags(CREATE_NO_WINDOW)` to avoid a flashing console)
285
+ - Linux: `/etc/machine-id` or the `machine-uid` crate
286
+
287
+ **Why:** Plaintext API keys in `settings.json` is a known liability. AES-GCM gives authenticated encryption; Argon2id is memory-hard against GPU brute-force; hardware-binding means a stolen `settings.json` file alone isn't useful — the attacker needs the machine too.
288
+
289
+ Note: this is **at-rest** protection, not perfect security. A privileged process on the same machine can still decrypt. For higher assurance, integrate the OS keychain (macOS Keychain via `security-framework`, Windows DPAPI, Linux Secret Service).
290
+
291
+ ### Granular capability scopes
292
+
293
+ ```json
294
+ // capabilities/default.json
295
+ {
296
+ "$schema": "../gen/schemas/desktop-schema.json",
297
+ "identifier": "default",
298
+ "description": "Capability for the main window",
299
+ "windows": ["main"],
300
+ "permissions": [
301
+ "core:default",
302
+ "opener:default",
303
+ "opener:allow-open-url",
304
+ "dialog:allow-save",
305
+ "dialog:allow-open",
306
+ "fs:allow-write-file",
307
+ "fs:allow-read-file",
308
+ "notification:default",
309
+ "notification:allow-show",
310
+ "global-shortcut:allow-register",
311
+ "global-shortcut:allow-unregister",
312
+ "process:allow-restart",
313
+ "updater:default"
314
+ ]
315
+ }
316
+ ```
317
+
318
+ **Why:** Listing exactly what the app uses (not blanket `"*"` or `"core:*"`) means the security audit surface is the capability file. If a plugin update adds new permissions, your app doesn't silently inherit them.
319
+
320
+ ## Cargo
321
+
322
+ ### Release profile
323
+
324
+ ```toml
325
+ [profile.release]
326
+ lto = "fat"
327
+ codegen-units = 1
328
+ opt-level = 3
329
+ strip = true
330
+ panic = "abort"
331
+
332
+ [profile.dev]
333
+ opt-level = 1 # Faster dev builds while keeping some optimization
334
+ ```
335
+
336
+ **Why:**
337
+ - `lto = "fat"` + `codegen-units = 1` = full whole-program optimization, smaller and faster binaries.
338
+ - `strip = true` removes symbols (~30% smaller binary).
339
+ - `panic = "abort"` skips unwinding code (smaller binary; no panic recovery, but Tauri apps generally restart on panic anyway).
340
+ - `opt-level = 1` in dev keeps debug builds 2-3× faster than the default `opt-level = 0` while staying fast to compile.
341
+
342
+ ### Target-specific dependencies
343
+
344
+ ```toml
345
+ [target.'cfg(target_os = "macos")'.dependencies]
346
+ security-framework = "3.0"
347
+ cocoa = "0.25"
348
+ objc = "0.2"
349
+
350
+ [target.'cfg(target_os = "windows")'.dependencies]
351
+ windows-sys = { version = "0.59", features = [
352
+ "Win32_System_ProcessStatus",
353
+ "Win32_Foundation",
354
+ "Win32_UI_Input_KeyboardAndMouse",
355
+ ] }
356
+
357
+ [target.'cfg(target_os = "linux")'.dependencies]
358
+ machine-uid = "0.2"
359
+ ```
360
+
361
+ **Why:** Platform crates don't compile on the wrong platform. Putting them under `[target.'cfg(...)']` makes the default `cargo build` portable. Enable only the `windows-sys` features the code actually uses — pulling all features in adds minutes to compile time.
362
+
363
+ ## Frontend
364
+
365
+ ### Typed `useTauriCommand` hook
366
+
367
+ ```ts
368
+ // hooks/useTauriCommand.ts
369
+ export function useTauriCommand<T = unknown>(options: UseCommandOptions<T>): CommandState<T> {
370
+ const [data, setData] = useState<T | null>(null);
371
+ const [isLoading, setIsLoading] = useState(false);
372
+ const [error, setError] = useState<string | null>(null);
373
+
374
+ const execute = useCallback(async (args?: Record<string, unknown>) => {
375
+ setIsLoading(true);
376
+ setError(null);
377
+ try {
378
+ const result = await invoke<T>(options.command, args || options.args);
379
+ setData(result);
380
+ return result;
381
+ } catch (err) {
382
+ setError(String(err));
383
+ throw err;
384
+ } finally {
385
+ setIsLoading(false);
386
+ }
387
+ }, [options.command, options.args]);
388
+
389
+ return { data, isLoading, error, execute };
390
+ }
391
+ ```
392
+
393
+ **Why:** Every component would otherwise reinvent loading/error wrappers around `invoke()`, with subtle differences. One hook = one place to add retries, debouncing, logging, telemetry. Plus type safety — `T` flows through.
394
+
395
+ ### `isTauriReady()` guard
396
+
397
+ ```ts
398
+ // tauriReady.ts
399
+ export function isTauriReady(): boolean {
400
+ return typeof window !== "undefined";
401
+ }
402
+ ```
403
+
404
+ Auto-loading hooks should call this before invoking. In Tauri 2 the IPC bridge is ready before React mounts, but a frontend that's ever previewed via plain `vite dev` (without Tauri) needs the guard.
405
+
406
+ ### `vite.config.ts` tuned for Tauri
407
+
408
+ ```ts
409
+ export default defineConfig({
410
+ base: './', // relative paths so bundled HTML loads from tauri://localhost
411
+ plugins: [react()],
412
+ clearScreen: false, // don't hide Rust errors
413
+ server: {
414
+ port: 5173,
415
+ strictPort: true, // fail if port is taken (Tauri expects it)
416
+ watch: { ignored: ['**/src-tauri/**'] }, // Cargo handles its own files
417
+ },
418
+ esbuild: { jsx: 'automatic' },
419
+ });
420
+ ```
421
+
422
+ **Why:**
423
+ - `base: './'`: without it, asset URLs are absolute (`/assets/index.css`) and break inside the `tauri://localhost` context.
424
+ - `strictPort: true`: Tauri's `devUrl` points at 5173 — if Vite silently picks 5174, Tauri shows a blank window.
425
+ - Ignoring `src-tauri/` avoids HMR loops triggered by Cargo's `target/` writes.
426
+ - `clearScreen: false`: Vite's screen-clear hides important Rust compiler errors.
427
+
428
+ ### TypeScript strict config
429
+
430
+ ```json
431
+ {
432
+ "compilerOptions": {
433
+ "strict": true,
434
+ "noUnusedLocals": true,
435
+ "noUnusedParameters": true,
436
+ "noFallthroughCasesInSwitch": true,
437
+ "isolatedModules": true,
438
+ "allowImportingTsExtensions": true
439
+ }
440
+ }
441
+ ```
442
+
443
+ **Why:** Strict mode catches real bugs (`undefined` access, implicit `any`). `noUnusedLocals`/`noUnusedParameters` flag the kind of "I forgot to wire this up" mistakes that pass code review.
444
+
445
+ ## CI
446
+
447
+ ### Cross-platform matrix
448
+
449
+ ```yaml
450
+ # .github/workflows/unit-tests.yml
451
+ name: Unit Tests
452
+ on:
453
+ push: { branches: ['**'] }
454
+ pull_request:
455
+
456
+ jobs:
457
+ test:
458
+ strategy:
459
+ fail-fast: false
460
+ matrix:
461
+ platform: [macos-latest, windows-latest, ubuntu-latest]
462
+ runs-on: ${{ matrix.platform }}
463
+ steps:
464
+ - uses: actions/checkout@v4
465
+ - uses: dtolnay/rust-toolchain@stable
466
+ - uses: actions/setup-node@v4
467
+ with: { node-version: '20', cache: 'npm' }
468
+ - run: npm ci
469
+ - name: Install Ubuntu deps
470
+ if: matrix.platform == 'ubuntu-latest'
471
+ run: |
472
+ sudo apt-get update
473
+ sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev pkg-config
474
+ - run: cd src-tauri && cargo test
475
+ ```
476
+
477
+ **Why:** Tauri's behavior diverges per OS (file pickers, permissions, hotkeys). A green test on macOS does not imply Windows works. `fail-fast: false` lets you see all three results.
@@ -0,0 +1,51 @@
1
+ # `src-tauri/build.rs`
2
+
3
+ Tauri's standard build script plus platform-specific framework links. Use `CARGO_CFG_TARGET_OS` (the **target** OS — survives cross-compilation), never the host OS.
4
+
5
+ ```rust
6
+ fn main() {
7
+ tauri_build::build();
8
+
9
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
10
+
11
+ match target_os.as_str() {
12
+ "macos" => {
13
+ // Frameworks commonly needed for system integration:
14
+ // - Accelerate, Metal: GPU/ML acceleration
15
+ // - CoreGraphics, ApplicationServices: window placement, accessibility
16
+ // - Foundation: NS* types
17
+ println!("cargo:rustc-link-lib=framework=Foundation");
18
+ println!("cargo:rustc-link-lib=framework=ApplicationServices");
19
+ // Enable the others on demand — keep build.rs to what the code uses:
20
+ // println!("cargo:rustc-link-lib=framework=Accelerate");
21
+ // println!("cargo:rustc-link-lib=framework=Metal");
22
+ // println!("cargo:rustc-link-lib=framework=CoreGraphics");
23
+ }
24
+ "windows" => {
25
+ // Most Windows APIs come in via `windows-sys` features. Only declare libs
26
+ // here for low-level needs not covered by a `windows-sys` feature.
27
+ // println!("cargo:rustc-link-lib=user32");
28
+ }
29
+ "linux" => {
30
+ // pthread + libm are usually pulled in by the system, but be explicit.
31
+ println!("cargo:rustc-link-lib=pthread");
32
+ println!("cargo:rustc-link-lib=m");
33
+ }
34
+ _ => {}
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Why `CARGO_CFG_TARGET_OS` (not `cfg!`)
40
+
41
+ `cfg!(target_os = "...")` evaluates at compile time of `build.rs` against the **host** OS. When cross-compiling (macOS host → Linux target, Windows host → ARM target), `cfg!(target_os = "linux")` is `false` on a macOS host even though the actual build target is Linux.
42
+
43
+ `CARGO_CFG_TARGET_OS` is the environment variable Cargo sets to the target. Always use it in `build.rs`.
44
+
45
+ ## Don't over-link
46
+
47
+ Every `cargo:rustc-link-lib=framework=*` line adds a framework dependency to the binary, even if the code doesn't actually use it. This bloats binaries and can cause notarization warnings. Start with the minimum (`Foundation`, `ApplicationServices` on macOS; `pthread`, `m` on Linux) and add more only when a linker error proves you need them.
48
+
49
+ ## Don't run external commands
50
+
51
+ `build.rs` runs on every developer's machine. Calling out to `make`, `cmake`, `python` etc. introduces dependencies that may not be installed. If a Rust crate needs C dependencies (`whisper.cpp`, `opencv`), check whether the crate already handles the build — almost all do. Only add custom build logic when you're authoring a crate from scratch.
@@ -0,0 +1,68 @@
1
+ # `src-tauri/capabilities/default.json`
2
+
3
+ Capabilities are Tauri 2's permission system. The frontend can only invoke a plugin command if a capability file grants that exact scope to the current window.
4
+
5
+ ```json
6
+ {
7
+ "$schema": "../gen/schemas/desktop-schema.json",
8
+ "identifier": "default",
9
+ "description": "Capability for the main window",
10
+ "windows": ["main"],
11
+ "permissions": [
12
+ "core:default",
13
+
14
+ "opener:default",
15
+ "opener:allow-open-url",
16
+
17
+ "dialog:allow-save",
18
+ "dialog:allow-open",
19
+
20
+ "fs:allow-read-file",
21
+ "fs:allow-write-file",
22
+
23
+ "notification:default",
24
+ "notification:allow-show",
25
+ "notification:allow-is-permission-granted",
26
+ "notification:allow-request-permission",
27
+
28
+ "global-shortcut:allow-is-registered",
29
+ "global-shortcut:allow-register",
30
+ "global-shortcut:allow-unregister",
31
+
32
+ "process:allow-restart",
33
+
34
+ "updater:default"
35
+ ]
36
+ }
37
+ ```
38
+
39
+ ## Per-window capability files
40
+
41
+ If the app has multiple windows (overlay, settings popup, waveform), each one gets its own capability file scoped to just that window's needs. A transparent waveform overlay does not need `fs:allow-write-file` or `dialog:*` — it usually only emits events.
42
+
43
+ ```json
44
+ // capabilities/waveform.json
45
+ {
46
+ "$schema": "../gen/schemas/desktop-schema.json",
47
+ "identifier": "waveform",
48
+ "description": "Capability for the waveform overlay window",
49
+ "windows": ["waveform"],
50
+ "permissions": [
51
+ "core:default"
52
+ ]
53
+ }
54
+ ```
55
+
56
+ ## Why granular over blanket
57
+
58
+ - **Granular permissions = audit surface.** The capability file is the single document a security reviewer reads to learn "what can the frontend do?". `"core:default"` only is OK; `"fs:default"` without listing specific actions is suspect.
59
+ - **Plugin updates can widen `default`.** If a plugin's `default` permission set expands in a new version, your app silently inherits the new permissions. Listing exact actions pins the contract.
60
+ - **Frontend reflection breaks at the boundary.** A malicious or buggy frontend can call any command listed in the handler, but the capability file is checked before the call ever reaches Rust. Tight capabilities are a defense in depth.
61
+
62
+ ## Reading the schema
63
+
64
+ The `$schema` reference points at `../gen/schemas/desktop-schema.json`, which Tauri generates on `cargo build`. Editors with JSON schema support (VS Code, JetBrains) will autocomplete valid permission names and show inline docs for each one. If the file is missing, run `cd src-tauri && cargo build` once to generate it.
65
+
66
+ ## Don't ship `"core:*"` glob
67
+
68
+ `"core:*"` exists but matches every core permission, including ones the app doesn't use. If a future Tauri release adds a new core permission (e.g. clipboard read), your app silently gains it. Either list the specific `core:allow-*` permissions, or use `"core:default"` (which is curated and stable across versions).