diff --git a/.agents/skills/auto-pr-merge/SKILL.md b/.agents/skills/auto-pr-merge/SKILL.md index 7b9f4fee10d..42f4fed9d09 100644 --- a/.agents/skills/auto-pr-merge/SKILL.md +++ b/.agents/skills/auto-pr-merge/SKILL.md @@ -117,6 +117,7 @@ Use the Skill tool: skill: "review-code" ``` After review completes: + - If **Critical or High** issues found: fix them using the Edit tool, then re-run `pn typecheck` to verify the review fixes don't introduce type errors - If only **Medium or Low** issues: acceptable, continue - If **no issues**: continue @@ -181,6 +182,7 @@ fi - `--delete-branch` cleans up the feature branch after merge (skipped in worktrees to avoid checkout conflict) If merge fails: + 1. Check the error message 2. If the error contains `'master' is already used by worktree` or similar: retry without `--delete-branch` and delete the remote branch manually with `git push origin --delete ` 3. If merge conflicts: use Skill tool with `skill: "resolve-conflicts"`, push, then retry merge once diff --git a/.agents/skills/auto-review-fix/SKILL.md b/.agents/skills/auto-review-fix/SKILL.md index 46811735f7f..8edd67151b3 100644 --- a/.agents/skills/auto-review-fix/SKILL.md +++ b/.agents/skills/auto-review-fix/SKILL.md @@ -81,6 +81,7 @@ git diff $(git merge-base origin/main HEAD) --stat | tail -1 ``` If the diff contains more than 500 changed files, **EXIT immediately** with: + ``` ❌ Diff too large (>500 files). Please split this PR into smaller, focused changes. ``` @@ -101,28 +102,35 @@ Create `00-review-context.md` in the working directory with: # Review Context ## Branch Info + - Base: origin/main - Current: [branch name] ## Changed Files Summary + [List all changed files with their change type: A/M/D] ## Changed Line Ranges (PR Scope) + -| File | Changed Lines | -|------|---------------| + +| File | Changed Lines | +| ------------------ | ---------------- | | [path/to/file1.ts] | [45-67, 120-135] | -| [path/to/file2.ts] | [10-25] | +| [path/to/file2.ts] | [10-25] | ## Review Standards Reference + - Follow /review-code standards - Focus on: correctness, security, performance, maintainability - Priority levels: Critical > High > Medium > Low ## File Categories + [Categorized list - see below] ## Skipped Issues (Do Not Re-validate) + @@ -130,11 +138,12 @@ Create `00-review-context.md` in the working directory with: [Initially empty - populated during validation phase] ## Iteration State + + Current iteration: 1 Last completed phase: Setup Files fixed this iteration: [] - ``` **IMPORTANT**: The "Skipped Issues" section persists across iterations. However, skips should be RARE - only purely cosmetic issues (naming, JSDoc, import ordering) may be skipped. Functional issues like error handling, type safety, and performance must ALWAYS be fixed. @@ -143,17 +152,18 @@ Files fixed this iteration: [] Assign each file to **exactly ONE** file category (no duplicates): -| Priority | Category | File Patterns | -|----------|----------|---------------| -| 1 | Electron/Main | `src/main/`, `src/preload/`, `electron.*` | -| 2 | Backend/IPC | `*ipc*`, `*handler*`, `*service*` (in main process) | -| 3 | Frontend/UI | `src/renderer/`, `components/`, `*.tsx`, `*.css` | -| 4 | Config/Build | `*.config.*`, `package.json`, `tsconfig.*`, `electron-builder.*` | -| 5 | Utility/Common | Everything else | +| Priority | Category | File Patterns | +| -------- | -------------- | ---------------------------------------------------------------- | +| 1 | Electron/Main | `src/main/`, `src/preload/`, `electron.*` | +| 2 | Backend/IPC | `*ipc*`, `*handler*`, `*service*` (in main process) | +| 3 | Frontend/UI | `src/renderer/`, `components/`, `*.tsx`, `*.css` | +| 4 | Config/Build | `*.config.*`, `package.json`, `tsconfig.*`, `electron-builder.*` | +| 5 | Utility/Common | Everything else | **Deduplication rule**: A file belongs to the FIRST matching category only. **Tiebreaker rule**: If a file path matches multiple categories at different directory depths: + 1. Use the **deepest matching directory** (e.g., `src/main/services/ui/dialog.ts` → Backend/IPC because `services/` is deeper than `main/`) 2. If same depth, use priority order (lower number wins) @@ -161,12 +171,12 @@ Assign each file to **exactly ONE** file category (no duplicates): Each file category gets reviewed by multiple specialized review commands: -| Category | Review Commands | -|----------|-----------------| -| Electron/Main | `/review-code`, `/review-algorithm-architecture` | -| Backend/IPC | `/review-code`, `/review-algorithm-architecture` | -| Frontend/UI | `/review-code`, `/review-algorithm-architecture` | -| Config/Build | `/review-code` | +| Category | Review Commands | +| -------------- | ------------------------------------------------ | +| Electron/Main | `/review-code`, `/review-algorithm-architecture` | +| Backend/IPC | `/review-code`, `/review-algorithm-architecture` | +| Frontend/UI | `/review-code`, `/review-algorithm-architecture` | +| Config/Build | `/review-code` | | Utility/Common | `/review-code`, `/review-algorithm-architecture` | **Total review agents**: (Categories with files) × (Applicable review commands per category) @@ -179,10 +189,10 @@ Spawn review subagents using the Task tool. **CRITICAL**: Use the exact Task() d ### Review Command Descriptions -| Command | Focus | Engine | -|---------|-------|--------| -| `/review-code` | Logical bugs, security issues, TypeScript best practices, error handling | Claude | -| `/review-algorithm-architecture` | File organization, module boundaries, performance on hot paths | Claude | +| Command | Focus | Engine | +| -------------------------------- | ------------------------------------------------------------------------ | ------ | +| `/review-code` | Logical bugs, security issues, TypeScript best practices, error handling | Claude | +| `/review-algorithm-architecture` | File organization, module boundaries, performance on hot paths | Claude | ### Spawn agents by category × review command @@ -243,6 +253,7 @@ Total: 6 parallel review agents **Launch ALL Task() calls in a single message block for true parallelism.** **IMPORTANT — WAIT FOR ALL AGENTS**: + - Do NOT use `run_in_background: true` for any Phase 1 review agent. - Task() calls without `run_in_background` are blocking by default — they return only when the subagent finishes. - **VERIFICATION CHECKPOINT**: Before starting Phase 2, you MUST count the number of Task() results you received. It MUST equal the total number of Task() calls you made. If any result is missing, DO NOT proceed — wait or re-spawn the missing agent. @@ -290,6 +301,7 @@ Total: 6 parallel review agents Validate all issues that weren't excluded by the "Skipped Issues" list. ### Grouping strategy: + - Group findings by file - One validation agent per file (handles all findings for that file) - Maximum 10 validation agents total @@ -356,6 +368,7 @@ For any issues marked as ⏭️ **Skip**, append them to the "Skipped Issues" se ```markdown ## Skipped Issues (Do Not Re-validate) + src/utils/helper.ts:42-45 | Low | Stylistic preference | Variable naming convention @@ -398,10 +411,12 @@ git add -A && git commit -m "WIP: Changes before auto-review fixes" **MANDATORY**: Create an explicit mapping from validation results to fix actions. For every issue marked "✅ Fix" in validation: + 1. Record: `[file] → [issue description] → [suggested fix]` 2. Group by file **Traceability requirement**: Every "✅ Fix" issue MUST appear in the fix manifest. Do not: + - Skip issues because you believe a different fix "solves the root cause" - Substitute your judgment for what validation explicitly said to fix - Rationalize away issues as "will be handled by another fix" @@ -419,6 +434,7 @@ If validation says "✅ Fix" for file X, spawn a fix agent for file X. Period. ### Conflict prevention To avoid race conditions when multiple agents edit overlapping code: + - Each file should have **at most ONE fix agent running at a time** - If a file has >5 issues requiring multiple agents, spawn them **sequentially** (not in parallel) - After each fix agent completes for a multi-agent file, verify the file is syntactically valid before spawning the next @@ -466,6 +482,7 @@ npm run typecheck 2>&1 ``` If the type check fails: + - Log which fix agent(s) likely caused the failure - Continue to Phase 5 - the next iteration's review will catch the type errors as new issues - Update Iteration State: `Build status: FAILED (type errors in [files])` @@ -473,6 +490,7 @@ If the type check fails: ### Step 6: Verify fix manifest completeness Before proceeding to Phase 5, verify: + - Every file in the fix manifest had a fix agent spawned - Every "✅ Fix" issue from validation was addressed by a fix agent - No issues were skipped due to assumptions about "root cause" fixes elsewhere @@ -506,6 +524,7 @@ OTHERWISE: **The key insight**: "I fixed all issues" ≠ "There are no issues". Another review round must confirm. **Note on convergence**: The loop converges because: + 1. Fixed issues no longer appear in subsequent reviews 2. Skipped issues are tracked and excluded from future validation 3. Only genuinely new issues trigger additional work @@ -612,13 +631,13 @@ ls -t .context/auto-review-*.md 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/nu ## Token Efficiency Summary -| Before | After | Improvement | -|--------|-------|-------------| -| Full diff to all agents | Relevant files only per category | ~6x reduction | -| Generic review for all | Specialized review commands | Better issue coverage | -| Re-validate skipped issues | Track in Skipped Issues list | ~2x reduction per iteration | -| 1 agent per finding | 1 agent per file (up to 5 issues) | ~5x reduction | -| No deduplication | Files assigned to one category only | ~2x reduction | +| Before | After | Improvement | +| -------------------------- | ----------------------------------- | --------------------------- | +| Full diff to all agents | Relevant files only per category | ~6x reduction | +| Generic review for all | Specialized review commands | Better issue coverage | +| Re-validate skipped issues | Track in Skipped Issues list | ~2x reduction per iteration | +| 1 agent per finding | 1 agent per file (up to 5 issues) | ~5x reduction | +| No deduplication | Files assigned to one category only | ~2x reduction | --- diff --git a/.agents/skills/typescript/SKILL.md b/.agents/skills/typescript/SKILL.md index 4c00a087ba6..8de8e7d7754 100644 --- a/.agents/skills/typescript/SKILL.md +++ b/.agents/skills/typescript/SKILL.md @@ -10,6 +10,7 @@ Comprehensive performance optimization guide for TypeScript applications. Contai ## When to Apply Reference these guidelines when: + - Configuring tsconfig.json for a new or existing project - Writing complex type definitions or generics - Optimizing async/await patterns and data fetching @@ -18,16 +19,16 @@ Reference these guidelines when: ## Rule Categories by Priority -| Priority | Category | Impact | Prefix | -|----------|----------|--------|--------| -| 1 | Type System Performance | CRITICAL | `type-` | -| 2 | Compiler Configuration | CRITICAL | `tscfg-` | -| 3 | Async Patterns | HIGH | `async-` | -| 4 | Module Organization | HIGH | `module-` | -| 5 | Type Safety Patterns | MEDIUM-HIGH | `safety-` | -| 6 | Memory Management | MEDIUM | `mem-` | -| 7 | Runtime Optimization | LOW-MEDIUM | `runtime-` | -| 8 | Advanced Patterns | LOW | `advanced-` | +| Priority | Category | Impact | Prefix | +| -------- | ----------------------- | ----------- | ----------- | +| 1 | Type System Performance | CRITICAL | `type-` | +| 2 | Compiler Configuration | CRITICAL | `tscfg-` | +| 3 | Async Patterns | HIGH | `async-` | +| 4 | Module Organization | HIGH | `module-` | +| 5 | Type Safety Patterns | MEDIUM-HIGH | `safety-` | +| 6 | Memory Management | MEDIUM | `mem-` | +| 7 | Runtime Optimization | LOW-MEDIUM | `runtime-` | +| 8 | Advanced Patterns | LOW | `advanced-` | ## Table of Contents diff --git a/.agents/skills/typescript/references/advanced-branded-types.md b/.agents/skills/typescript/references/advanced-branded-types.md index 9db9577461c..12f14f6dedf 100644 --- a/.agents/skills/typescript/references/advanced-branded-types.md +++ b/.agents/skills/typescript/references/advanced-branded-types.md @@ -16,15 +16,19 @@ type UserId = string type OrderId = string type ProductId = string -function fetchUser(id: UserId): Promise { /* ... */ } -function fetchOrder(id: OrderId): Promise { /* ... */ } +function fetchUser(id: UserId): Promise { + /* ... */ +} +function fetchOrder(id: OrderId): Promise { + /* ... */ +} const userId: UserId = 'user-123' const orderId: OrderId = 'order-456' // No error - all strings are interchangeable -fetchUser(orderId) // Bug: passed OrderId to UserId parameter -fetchOrder(userId) // Bug: passed UserId to OrderId parameter +fetchUser(orderId) // Bug: passed OrderId to UserId parameter +fetchOrder(userId) // Bug: passed UserId to OrderId parameter ``` **Correct (branded types prevent mixing):** @@ -44,15 +48,19 @@ function createOrderId(id: string): OrderId { return id as OrderId } -function fetchUser(id: UserId): Promise { /* ... */ } -function fetchOrder(id: OrderId): Promise { /* ... */ } +function fetchUser(id: UserId): Promise { + /* ... */ +} +function fetchOrder(id: OrderId): Promise { + /* ... */ +} const userId = createUserId('user-123') const orderId = createOrderId('order-456') -fetchUser(orderId) // Error: Argument of type 'OrderId' is not assignable to 'UserId' -fetchOrder(userId) // Error: Argument of type 'UserId' is not assignable to 'OrderId' -fetchUser(userId) // OK +fetchUser(orderId) // Error: Argument of type 'OrderId' is not assignable to 'UserId' +fetchOrder(userId) // Error: Argument of type 'UserId' is not assignable to 'OrderId' +fetchUser(userId) // OK ``` **For numeric types:** @@ -70,11 +78,12 @@ function formatPrice(cents: Cents): string { } const price = 29.99 as Dollars -formatPrice(price) // Error: Dollars not assignable to Cents -formatPrice(toCents(price)) // OK: '$29.99' +formatPrice(price) // Error: Dollars not assignable to Cents +formatPrice(toCents(price)) // OK: '$29.99' ``` **When to use branded types:** + - Entity IDs that shouldn't be mixed - Currency/unit conversions - Validated strings (email, URL, slug) diff --git a/.agents/skills/typescript/references/advanced-satisfies-operator.md b/.agents/skills/typescript/references/advanced-satisfies-operator.md index e3a70040f0b..83d069ea04f 100644 --- a/.agents/skills/typescript/references/advanced-satisfies-operator.md +++ b/.agents/skills/typescript/references/advanced-satisfies-operator.md @@ -17,13 +17,13 @@ type ColorConfig = Record const colors: ColorConfig = { red: [255, 0, 0], green: [0, 255, 0], - blue: [0, 0, 255], + blue: [0, 0, 255] // Can't access colors.red - it's just string keys } // TypeScript doesn't know 'red' is a valid key -const redValue = colors.red // Type: [number, number, number] -const pinkValue = colors.pink // No error! Type: [number, number, number] +const redValue = colors.red // Type: [number, number, number] +const pinkValue = colors.pink // No error! Type: [number, number, number] ``` **Correct (satisfies preserves literal types):** @@ -34,12 +34,12 @@ type ColorConfig = Record const colors = { red: [255, 0, 0], green: [0, 255, 0], - blue: [0, 0, 255], + blue: [0, 0, 255] } satisfies ColorConfig // TypeScript knows exact keys -const redValue = colors.red // Type: [number, number, number] -const pinkValue = colors.pink // Error: Property 'pink' does not exist +const redValue = colors.red // Type: [number, number, number] +const pinkValue = colors.pink // Error: Property 'pink' does not exist ``` **For configuration objects:** @@ -54,18 +54,18 @@ interface Route { // Without satisfies - loses literal path types const routes: Route[] = [ { path: '/', component: Home }, - { path: '/users', component: Users }, + { path: '/users', component: Users } ] // routes[0].path is just 'string' // With satisfies - preserves literal paths const routes = [ { path: '/', component: Home }, - { path: '/users', component: Users }, + { path: '/users', component: Users } ] satisfies Route[] // routes[0].path is '/' -type RoutePath = typeof routes[number]['path'] // '/' | '/users' +type RoutePath = (typeof routes)[number]['path'] // '/' | '/users' ``` **Combining with as const:** @@ -74,7 +74,7 @@ type RoutePath = typeof routes[number]['path'] // '/' | '/users' const config = { apiUrl: 'https://api.example.com', timeout: 5000, - retries: 3, + retries: 3 } as const satisfies { apiUrl: string timeout: number @@ -82,11 +82,12 @@ const config = { } // Both validated AND readonly with literal types -config.apiUrl // Type: 'https://api.example.com' (not just string) -config.timeout = 3000 // Error: Cannot assign to 'timeout' (readonly) +config.apiUrl // Type: 'https://api.example.com' (not just string) +config.timeout = 3000 // Error: Cannot assign to 'timeout' (readonly) ``` **When to use satisfies vs type annotation:** + - Use `satisfies` when you want validation but need literal types - Use type annotation (`:`) when you want the variable to be exactly that type - Use `as const satisfies` for readonly config with validation diff --git a/.agents/skills/typescript/references/advanced-template-literal-types.md b/.agents/skills/typescript/references/advanced-template-literal-types.md index dce653096bc..a43c45b23bc 100644 --- a/.agents/skills/typescript/references/advanced-template-literal-types.md +++ b/.agents/skills/typescript/references/advanced-template-literal-types.md @@ -18,17 +18,17 @@ type EventHandler = { } const handler: EventHandler = { - event: 'click', // OK + event: 'click', // OK handler: () => {} } const badHandler: EventHandler = { - event: 'clck', // Typo - no error + event: 'clck', // Typo - no error handler: () => {} } -function addEventListener(event: string, handler: () => void): void { } -addEventListener('onlcick', () => {}) // Typo compiles fine +function addEventListener(event: string, handler: () => void): void {} +addEventListener('onlcick', () => {}) // Typo compiles fine ``` **Correct (template literal type validates pattern):** @@ -43,12 +43,12 @@ type EventHandler = { } const handler: EventHandler = { - event: 'onClick', // OK + event: 'onClick', // OK handler: () => {} } const badHandler: EventHandler = { - event: 'onClck', // Error: Type '"onClck"' is not assignable to type 'EventHandlerName' + event: 'onClck', // Error: Type '"onClck"' is not assignable to type 'EventHandlerName' handler: () => {} } ``` @@ -63,10 +63,10 @@ function setWidth(element: HTMLElement, width: CSSValue): void { element.style.width = width } -setWidth(div, '100px') // OK -setWidth(div, '2.5rem') // OK -setWidth(div, '100') // Error: Type '"100"' is not assignable to type 'CSSValue' -setWidth(div, '100pixels') // Error +setWidth(div, '100px') // OK +setWidth(div, '2.5rem') // OK +setWidth(div, '100') // Error: Type '"100"' is not assignable to type 'CSSValue' +setWidth(div, '100pixels') // Error ``` **For API route patterns:** @@ -80,10 +80,10 @@ function fetchResource(route: APIRoute): Promise { return fetch(route) } -fetchResource('/api/v1/users') // OK -fetchResource('/api/v2/orders') // OK -fetchResource('/api/v3/users') // Error: 'v3' not in APIVersion -fetchResource('/users') // Error: doesn't match pattern +fetchResource('/api/v1/users') // OK +fetchResource('/api/v2/orders') // OK +fetchResource('/api/v3/users') // Error: 'v3' not in APIVersion +fetchResource('/users') // Error: doesn't match pattern ``` **Combining with mapped types:** diff --git a/.agents/skills/typescript/references/async-avoid-loop-await.md b/.agents/skills/typescript/references/async-avoid-loop-await.md index 1cf6ba842c4..e299ef0d99a 100644 --- a/.agents/skills/typescript/references/async-avoid-loop-await.md +++ b/.agents/skills/typescript/references/async-avoid-loop-await.md @@ -16,7 +16,7 @@ async function enrichUsers(userIds: string[]): Promise { const enrichedUsers: EnrichedUser[] = [] for (const userId of userIds) { - const user = await fetchUser(userId) // Waits for each request + const user = await fetchUser(userId) // Waits for each request const profile = await fetchProfile(userId) enrichedUsers.push({ ...user, profile }) } @@ -32,10 +32,7 @@ async function enrichUsers(userIds: string[]): Promise { async function enrichUsers(userIds: string[]): Promise { const enrichedUsers = await Promise.all( userIds.map(async (userId) => { - const [user, profile] = await Promise.all([ - fetchUser(userId), - fetchProfile(userId), - ]) + const [user, profile] = await Promise.all([fetchUser(userId), fetchProfile(userId)]) return { ...user, profile } }) ) @@ -56,10 +53,7 @@ async function enrichUsers(userIds: string[]): Promise { const batch = userIds.slice(i, i + BATCH_SIZE) const batchResults = await Promise.all( batch.map(async (userId) => { - const [user, profile] = await Promise.all([ - fetchUser(userId), - fetchProfile(userId), - ]) + const [user, profile] = await Promise.all([fetchUser(userId), fetchProfile(userId)]) return { ...user, profile } }) ) @@ -71,6 +65,7 @@ async function enrichUsers(userIds: string[]): Promise { ``` **When sequential loop await is acceptable:** + - Each iteration depends on the previous result - API strictly requires sequential calls - Processing order affects correctness diff --git a/.agents/skills/typescript/references/async-avoid-unnecessary-async.md b/.agents/skills/typescript/references/async-avoid-unnecessary-async.md index f1de3ddc968..252f11b8617 100644 --- a/.agents/skills/typescript/references/async-avoid-unnecessary-async.md +++ b/.agents/skills/typescript/references/async-avoid-unnecessary-async.md @@ -53,6 +53,7 @@ async function getUser(userId: string): Promise { ``` **When async IS needed:** + - Multiple sequential await statements - Try/catch around await (use `return await` here) - Conditional await logic diff --git a/.agents/skills/typescript/references/async-defer-await.md b/.agents/skills/typescript/references/async-defer-await.md index 4947a40a345..0a9f0296258 100644 --- a/.agents/skills/typescript/references/async-defer-await.md +++ b/.agents/skills/typescript/references/async-defer-await.md @@ -13,8 +13,8 @@ Start async operations immediately but defer `await` until the value is actually ```typescript async function processOrder(orderId: string): Promise { - const order = await fetchOrder(orderId) // Blocks here - const config = await loadProcessingConfig() // Waits for order first, unnecessarily + const order = await fetchOrder(orderId) // Blocks here + const config = await loadProcessingConfig() // Waits for order first, unnecessarily // config doesn't depend on order — these could run in parallel if (order.priority === 'express') { @@ -28,10 +28,10 @@ async function processOrder(orderId: string): Promise { ```typescript async function processOrder(orderId: string): Promise { - const orderPromise = fetchOrder(orderId) // Start immediately - const config = await loadProcessingConfig() // Runs while order fetches + const orderPromise = fetchOrder(orderId) // Start immediately + const config = await loadProcessingConfig() // Runs while order fetches - const order = await orderPromise // Now await when needed + const order = await orderPromise // Now await when needed if (order.priority === 'express') { return processExpress(order, config) @@ -61,7 +61,7 @@ async function loadUserContent(userId: string): Promise { settingsPromise, featuresPromise, ordersPromise, - prefsPromise, + prefsPromise ]) return { user, settings, features, orders, prefs } diff --git a/.agents/skills/typescript/references/async-explicit-return-types.md b/.agents/skills/typescript/references/async-explicit-return-types.md index 19b1ba47b8b..82683582a26 100644 --- a/.agents/skills/typescript/references/async-explicit-return-types.md +++ b/.agents/skills/typescript/references/async-explicit-return-types.md @@ -15,14 +15,14 @@ Explicit return types on async functions catch mismatches at the function bounda async function fetchUserOrders(userId: string) { const response = await fetch(`/api/users/${userId}/orders`) if (!response.ok) { - return null // Implicit: Promise + return null // Implicit: Promise } - return response.json() // Implicit: Promise + return response.json() // Implicit: Promise } // Caller has unclear type: Promise const orders = await fetchUserOrders('123') -orders.map(o => o.id) // No type error even if orders is null +orders.map((o) => o.id) // No type error even if orders is null ``` **Correct (explicit Promise type):** @@ -45,7 +45,7 @@ async function fetchUserOrders(userId: string): Promise { // Caller knows the exact type const orders = await fetchUserOrders('123') if (orders) { - orders.map(o => o.id) // Type-safe access + orders.map((o) => o.id) // Type-safe access } ``` @@ -60,7 +60,7 @@ async function fetchUserOrders(userId: string): Promise> { if (!response.ok) { return { ok: false, error: new Error(`HTTP ${response.status}`) } } - const orders = await response.json() as Order[] + const orders = (await response.json()) as Order[] return { ok: true, value: orders } } catch (error) { return { ok: false, error: error as Error } @@ -69,6 +69,7 @@ async function fetchUserOrders(userId: string): Promise> { ``` **Benefits:** + - Errors caught at function definition, not call sites - Better IDE autocomplete for consumers - Self-documenting API contracts diff --git a/.agents/skills/typescript/references/async-parallel-promises.md b/.agents/skills/typescript/references/async-parallel-promises.md index d6d95654ce4..6f565b947f6 100644 --- a/.agents/skills/typescript/references/async-parallel-promises.md +++ b/.agents/skills/typescript/references/async-parallel-promises.md @@ -13,9 +13,9 @@ Sequential `await` statements create request waterfalls—each operation waits f ```typescript async function loadDashboard(userId: string): Promise { - const user = await fetchUser(userId) // 200ms - const orders = await fetchOrders(userId) // 300ms - const notifications = await fetchNotifications(userId) // 150ms + const user = await fetchUser(userId) // 200ms + const orders = await fetchOrders(userId) // 300ms + const notifications = await fetchNotifications(userId) // 150ms // Total: 650ms (sequential) return { user, orders, notifications } @@ -27,9 +27,9 @@ async function loadDashboard(userId: string): Promise { ```typescript async function loadDashboard(userId: string): Promise { const [user, orders, notifications] = await Promise.all([ - fetchUser(userId), // 200ms ─┐ - fetchOrders(userId), // 300ms ─┼─ Run in parallel - fetchNotifications(userId), // 150ms ─┘ + fetchUser(userId), // 200ms ─┐ + fetchOrders(userId), // 300ms ─┼─ Run in parallel + fetchNotifications(userId) // 150ms ─┘ ]) // Total: 300ms (max of all operations) @@ -44,18 +44,19 @@ async function loadDashboard(userId: string): Promise { const results = await Promise.allSettled([ fetchUser(userId), fetchOrders(userId), - fetchNotifications(userId), + fetchNotifications(userId) ]) return { user: results[0].status === 'fulfilled' ? results[0].value : null, orders: results[1].status === 'fulfilled' ? results[1].value : [], - notifications: results[2].status === 'fulfilled' ? results[2].value : [], + notifications: results[2].status === 'fulfilled' ? results[2].value : [] } } ``` **When sequential is correct:** + - Operations have data dependencies (need result A to make request B) - Rate limiting requires sequential requests - Order of execution matters for side effects diff --git a/.agents/skills/typescript/references/mem-avoid-closure-leaks.md b/.agents/skills/typescript/references/mem-avoid-closure-leaks.md index d49e9845a7a..af2aa3b9049 100644 --- a/.agents/skills/typescript/references/mem-avoid-closure-leaks.md +++ b/.agents/skills/typescript/references/mem-avoid-closure-leaks.md @@ -18,7 +18,7 @@ function createDataProcessor(largeDataset: DataRecord[]): () => void { return function processNext(): void { // This closure retains reference to largeDataset // even though it only needs processedIds - const next = largeDataset.find(r => !processedIds.has(r.id)) + const next = largeDataset.find((r) => !processedIds.has(r.id)) if (next) { processedIds.add(next.id) sendToServer(next) @@ -28,7 +28,7 @@ function createDataProcessor(largeDataset: DataRecord[]): () => void { // largeDataset (100MB) stays in memory as long as processNext exists const processor = createDataProcessor(hugeDataset) -setInterval(processor, 1000) // Runs forever, 100MB never freed +setInterval(processor, 1000) // Runs forever, 100MB never freed ``` **Correct (closure captures only what it needs):** @@ -36,9 +36,8 @@ setInterval(processor, 1000) // Runs forever, 100MB never freed ```typescript function createDataProcessor(largeDataset: DataRecord[]): () => void { // Build a queue of just the IDs and a lookup for individual records - const pendingQueue: string[] = largeDataset.map(r => r.id) - const getRecord = (id: string): DataRecord | undefined => - largeDataset.find(r => r.id === id) + const pendingQueue: string[] = largeDataset.map((r) => r.id) + const getRecord = (id: string): DataRecord | undefined => largeDataset.find((r) => r.id === id) // Release the array reference — closure only captures pendingQueue and getRecord // Caller should also release their reference to largeDataset @@ -76,7 +75,7 @@ class Dashboard { initialize(): void { window.addEventListener('resize', () => { - this.handleResize() // 'this' keeps entire Dashboard alive + this.handleResize() // 'this' keeps entire Dashboard alive }) } } diff --git a/.agents/skills/typescript/references/mem-avoid-global-state.md b/.agents/skills/typescript/references/mem-avoid-global-state.md index 932dd631679..e1a0fdde2b9 100644 --- a/.agents/skills/typescript/references/mem-avoid-global-state.md +++ b/.agents/skills/typescript/references/mem-avoid-global-state.md @@ -13,7 +13,7 @@ Global variables and module-level state persist for the application's lifetime. ```typescript // cache.ts -const userCache = new Map() // Never cleared +const userCache = new Map() // Never cleared export function getCachedUser(id: string): User | undefined { return userCache.get(id) @@ -61,7 +61,7 @@ class LRUCache { } } -const userCache = new LRUCache(1000) // Max 1000 entries +const userCache = new LRUCache(1000) // Max 1000 entries export function getCachedUser(id: string): User | undefined { return userCache.get(id) diff --git a/.agents/skills/typescript/references/mem-cleanup-event-listeners.md b/.agents/skills/typescript/references/mem-cleanup-event-listeners.md index f54bec0e0d4..ae58671d7cf 100644 --- a/.agents/skills/typescript/references/mem-cleanup-event-listeners.md +++ b/.agents/skills/typescript/references/mem-cleanup-event-listeners.md @@ -80,7 +80,7 @@ class WebSocketManager { } disconnect(): void { - this.abortController.abort() // Removes all listeners at once + this.abortController.abort() // Removes all listeners at once this.socket.close() } } diff --git a/.agents/skills/typescript/references/mem-clear-timers.md b/.agents/skills/typescript/references/mem-clear-timers.md index 9a809df7a16..0d8e7a28b82 100644 --- a/.agents/skills/typescript/references/mem-clear-timers.md +++ b/.agents/skills/typescript/references/mem-clear-timers.md @@ -41,7 +41,7 @@ class DataPoller { private intervalId: ReturnType | null = null start(): void { - if (this.intervalId) return // Prevent duplicate intervals + if (this.intervalId) return // Prevent duplicate intervals this.intervalId = setInterval(() => { this.data = fetchLatestData() @@ -87,7 +87,7 @@ class AnimationController { function usePolling(callback: () => void, interval: number): void { useEffect(() => { const id = setInterval(callback, interval) - return () => clearInterval(id) // Cleanup on unmount + return () => clearInterval(id) // Cleanup on unmount }, [callback, interval]) } @@ -96,7 +96,7 @@ function useDebounce(value: T, delay: number): T { useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay) - return () => clearTimeout(timer) // Clear on value change or unmount + return () => clearTimeout(timer) // Clear on value change or unmount }, [value, delay]) return debouncedValue diff --git a/.agents/skills/typescript/references/mem-use-weakmap-for-metadata.md b/.agents/skills/typescript/references/mem-use-weakmap-for-metadata.md index 0ef27817c6f..b76cbbe6914 100644 --- a/.agents/skills/typescript/references/mem-use-weakmap-for-metadata.md +++ b/.agents/skills/typescript/references/mem-use-weakmap-for-metadata.md @@ -24,7 +24,7 @@ function trackUser(user: User): void { function removeUser(user: User): void { // Even after user is "removed" from app state, // Map still holds reference, preventing GC - userMetadata.delete(user) // Must manually clean up + userMetadata.delete(user) // Must manually clean up } // If delete is forgotten, user objects leak forever @@ -77,6 +77,7 @@ function getComputedConfig(config: Config): ComputedConfig { ``` **Limitations of WeakMap:** + - Keys must be objects (not primitives) - Not iterable (no `.keys()`, `.values()`, `.entries()`) - No `.size` property diff --git a/.agents/skills/typescript/references/module-avoid-barrel-imports.md b/.agents/skills/typescript/references/module-avoid-barrel-imports.md index 9816e8cc1b3..85fbeae7f79 100644 --- a/.agents/skills/typescript/references/module-avoid-barrel-imports.md +++ b/.agents/skills/typescript/references/module-avoid-barrel-imports.md @@ -16,7 +16,7 @@ Barrel files (index.ts re-exports) defeat tree-shaking and force bundlers to loa export * from './string' export * from './date' export * from './validation' -export * from './crypto' // Heavy, rarely used +export * from './crypto' // Heavy, rarely used // consumer.ts import { formatDate } from '@/utils' @@ -63,6 +63,7 @@ export default { ``` **When barrels are acceptable:** + - Internal modules with few exports (< 10) - Package entry points for library consumers - When bundler is configured to optimize them diff --git a/.agents/skills/typescript/references/module-avoid-circular-dependencies.md b/.agents/skills/typescript/references/module-avoid-circular-dependencies.md index 958d6bf629f..d56e0e6f781 100644 --- a/.agents/skills/typescript/references/module-avoid-circular-dependencies.md +++ b/.agents/skills/typescript/references/module-avoid-circular-dependencies.md @@ -20,10 +20,12 @@ export interface User { orders: Order[] } -export function createUser(): User { /* ... */ } +export function createUser(): User { + /* ... */ +} // order.ts -import { User } from './user' // Circular! +import { User } from './user' // Circular! export interface Order { id: string @@ -52,12 +54,16 @@ export interface Order { // user.ts import { User, Order } from './types' -export function createUser(): User { /* ... */ } +export function createUser(): User { + /* ... */ +} // order.ts import { User, Order } from './types' -export function createOrder(user: User): Order { /* ... */ } +export function createOrder(user: User): Order { + /* ... */ +} ``` **Alternative (interface segregation):** @@ -74,7 +80,7 @@ import { UserBase } from './user-types' export interface Order { id: string - user: UserBase // Only needs base interface, not full User + user: UserBase // Only needs base interface, not full User } ``` diff --git a/.agents/skills/typescript/references/module-control-types-inclusion.md b/.agents/skills/typescript/references/module-control-types-inclusion.md index e73e61b857b..ce22a732740 100644 --- a/.agents/skills/typescript/references/module-control-types-inclusion.md +++ b/.agents/skills/typescript/references/module-control-types-inclusion.md @@ -76,8 +76,8 @@ By default, TypeScript loads all `@types/*` packages from `node_modules`. This c { "compilerOptions": { "typeRoots": [ - "./types", // Custom declarations first - "./node_modules/@types" // Then @types + "./types", // Custom declarations first + "./node_modules/@types" // Then @types ], "types": ["node"] } @@ -85,6 +85,7 @@ By default, TypeScript loads all `@types/*` packages from `node_modules`. This c ``` **Benefits:** + - Prevents type conflicts between similar packages - Reduces memory usage during compilation - Faster IDE responsiveness diff --git a/.agents/skills/typescript/references/module-dynamic-imports.md b/.agents/skills/typescript/references/module-dynamic-imports.md index f957a455930..c2f35946573 100644 --- a/.agents/skills/typescript/references/module-dynamic-imports.md +++ b/.agents/skills/typescript/references/module-dynamic-imports.md @@ -12,9 +12,9 @@ Dynamic `import()` creates separate chunks that load on demand. Use them for lar **Incorrect (static import, always loaded):** ```typescript -import { PDFGenerator } from 'pdfkit' // 500KB -import { ExcelExporter } from 'exceljs' // 800KB -import { ChartLibrary } from 'chart.js' // 300KB +import { PDFGenerator } from 'pdfkit' // 500KB +import { ExcelExporter } from 'exceljs' // 800KB +import { ChartLibrary } from 'chart.js' // 300KB export async function exportReport(format: 'pdf' | 'excel' | 'chart') { if (format === 'pdf') { diff --git a/.agents/skills/typescript/references/module-use-type-imports.md b/.agents/skills/typescript/references/module-use-type-imports.md index f8ad3a195ed..bffd6d20b7a 100644 --- a/.agents/skills/typescript/references/module-use-type-imports.md +++ b/.agents/skills/typescript/references/module-use-type-imports.md @@ -13,8 +13,8 @@ Type-only imports (`import type`) are completely erased during compilation, prev ```typescript // config.ts -import { DatabaseConfig } from './database' // Loads entire database module -import { Logger } from './logger' // Loads entire logger module +import { DatabaseConfig } from './database' // Loads entire database module +import { Logger } from './logger' // Loads entire logger module interface AppConfig { db: DatabaseConfig @@ -73,6 +73,7 @@ import { createUser, type User, type UserRole } from './user' ``` **Benefits:** + - Smaller bundles (unused modules not included) - Faster cold starts (fewer modules to parse) - Clearer code intent (types vs runtime values) diff --git a/.agents/skills/typescript/references/runtime-avoid-object-spread-in-loops.md b/.agents/skills/typescript/references/runtime-avoid-object-spread-in-loops.md index 53f2b688a05..e01e455eb79 100644 --- a/.agents/skills/typescript/references/runtime-avoid-object-spread-in-loops.md +++ b/.agents/skills/typescript/references/runtime-avoid-object-spread-in-loops.md @@ -13,9 +13,9 @@ Object spread (`...`) creates a new object on each use. In loops, this causes N ```typescript function enrichOrders(orders: Order[]): EnrichedOrder[] { - return orders.map(order => ({ - ...order, // Creates new object - ...calculateTotals(order), // Spreads another object + return orders.map((order) => ({ + ...order, // Creates new object + ...calculateTotals(order), // Spreads another object processedAt: new Date() })) } @@ -33,7 +33,7 @@ interface EnrichedOrder extends Order { } function enrichOrders(orders: Order[]): EnrichedOrder[] { - return orders.map(order => { + return orders.map((order) => { const totals = calculateTotals(order) return { @@ -56,21 +56,28 @@ function enrichOrders(orders: Order[]): EnrichedOrder[] { ```typescript // Incorrect - spreads on every iteration -const result = items.reduce((acc, item) => ({ - ...acc, - [item.id]: item.value -}), {}) +const result = items.reduce( + (acc, item) => ({ + ...acc, + [item.id]: item.value + }), + {} +) // O(n²) - each spread copies growing object // Correct - mutate accumulator -const result = items.reduce((acc, item) => { - acc[item.id] = item.value - return acc -}, {} as Record) +const result = items.reduce( + (acc, item) => { + acc[item.id] = item.value + return acc + }, + {} as Record +) // O(n) - direct property assignment ``` **When spread is acceptable:** + - Outside hot paths - Small objects (< 10 properties) - When immutability is required for state management diff --git a/.agents/skills/typescript/references/runtime-cache-property-access.md b/.agents/skills/typescript/references/runtime-cache-property-access.md index 213ae389a8f..eab402e5dce 100644 --- a/.agents/skills/typescript/references/runtime-cache-property-access.md +++ b/.agents/skills/typescript/references/runtime-cache-property-access.md @@ -16,8 +16,8 @@ function processOrders(orders: Order[], config: AppConfig): ProcessedOrder[] { const results: ProcessedOrder[] = [] for (const order of orders) { - const tax = order.total * config.tax.rate // Nested access each iteration - const shipping = config.shipping.rates[order.region] // Nested access again + const tax = order.total * config.tax.rate // Nested access each iteration + const shipping = config.shipping.rates[order.region] // Nested access again results.push({ ...order, tax, shipping, final: order.total + tax + shipping }) } @@ -49,14 +49,16 @@ function processOrders(orders: Order[], config: AppConfig): ProcessedOrder[] { // Monomorphic — all objects have same shape, V8 ICs optimize this function sumOrders(orders: Order[]): number { let total = 0 - for (let i = 0; i < orders.length; i++) { // orders.length is fine - total += orders[i].total // Same shape every time + for (let i = 0; i < orders.length; i++) { + // orders.length is fine + total += orders[i].total // Same shape every time } return total } ``` **When to skip this optimization:** + - Arrays under 1,000 items - Monomorphic objects (same shape/class) - Non-hot paths executed infrequently diff --git a/.agents/skills/typescript/references/runtime-prefer-array-methods.md b/.agents/skills/typescript/references/runtime-prefer-array-methods.md index bbf347bff31..950074a3987 100644 --- a/.agents/skills/typescript/references/runtime-prefer-array-methods.md +++ b/.agents/skills/typescript/references/runtime-prefer-array-methods.md @@ -12,25 +12,25 @@ Modern JavaScript includes most common array operations. Native methods are fast **Incorrect (lodash for native operations):** ```typescript -import _ from 'lodash' // Imports entire library +import _ from 'lodash' // Imports entire library -const activeUsers = _.filter(users, u => u.isActive) -const userNames = _.map(activeUsers, u => u.name) -const firstAdmin = _.find(users, u => u.role === 'admin') -const hasAdmin = _.some(users, u => u.role === 'admin') -const allActive = _.every(users, u => u.isActive) -const userIds = _.uniq(users.map(u => u.id)) +const activeUsers = _.filter(users, (u) => u.isActive) +const userNames = _.map(activeUsers, (u) => u.name) +const firstAdmin = _.find(users, (u) => u.role === 'admin') +const hasAdmin = _.some(users, (u) => u.role === 'admin') +const allActive = _.every(users, (u) => u.isActive) +const userIds = _.uniq(users.map((u) => u.id)) ``` **Correct (native methods):** ```typescript -const activeUsers = users.filter(u => u.isActive) -const userNames = activeUsers.map(u => u.name) -const firstAdmin = users.find(u => u.role === 'admin') -const hasAdmin = users.some(u => u.role === 'admin') -const allActive = users.every(u => u.isActive) -const userIds = [...new Set(users.map(u => u.id))] +const activeUsers = users.filter((u) => u.isActive) +const userNames = activeUsers.map((u) => u.name) +const firstAdmin = users.find((u) => u.role === 'admin') +const hasAdmin = users.some((u) => u.role === 'admin') +const allActive = users.every((u) => u.isActive) +const userIds = [...new Set(users.map((u) => u.id))] ``` **Native replacements for common Lodash functions:** @@ -48,23 +48,27 @@ function chunk(array: T[], size: number): T[][] { // _.groupBy function groupBy(array: T[], key: keyof T): Record { - return array.reduce((groups, item) => { - const group = String(item[key]) - groups[group] = groups[group] ?? [] - groups[group].push(item) - return groups - }, {} as Record) + return array.reduce( + (groups, item) => { + const group = String(item[key]) + groups[group] = groups[group] ?? [] + groups[group].push(item) + return groups + }, + {} as Record + ) } // Object.groupBy (ES2024) -const grouped = Object.groupBy(users, user => user.role) +const grouped = Object.groupBy(users, (user) => user.role) // _.pick / _.omit -const { password, ...userWithoutPassword } = user // omit -const { id, name } = user // pick +const { password, ...userWithoutPassword } = user // omit +const { id, name } = user // pick ``` **When Lodash is still valuable:** + - `_.debounce`, `_.throttle` - complex timing logic - `_.cloneDeep` - deep object cloning - `_.merge` - deep object merging diff --git a/.agents/skills/typescript/references/runtime-use-for-of-for-iteration.md b/.agents/skills/typescript/references/runtime-use-for-of-for-iteration.md index d23c45992b0..d5961f86dbc 100644 --- a/.agents/skills/typescript/references/runtime-use-for-of-for-iteration.md +++ b/.agents/skills/typescript/references/runtime-use-for-of-for-iteration.md @@ -54,7 +54,7 @@ for (const item of items) { } // forEach: when you want functional style (but can't break/return) -items.forEach(item => process(item)) +items.forEach((item) => process(item)) // for-in: only for object keys (never for arrays) for (const key in config) { @@ -64,8 +64,8 @@ for (const key in config) { // Traditional for: when you need index, or need to modify loop for (let i = 0; i < items.length; i++) { if (items[i].id === targetId) { - items[i] = updatedItem // Modifying array - break // Early exit + items[i] = updatedItem // Modifying array + break // Early exit } } diff --git a/.agents/skills/typescript/references/runtime-use-set-for-lookups.md b/.agents/skills/typescript/references/runtime-use-set-for-lookups.md index 58406da671b..eb4734134c7 100644 --- a/.agents/skills/typescript/references/runtime-use-set-for-lookups.md +++ b/.agents/skills/typescript/references/runtime-use-set-for-lookups.md @@ -15,12 +15,12 @@ Array methods like `.includes()` and `.find()` are O(n) operations. For frequent const allowedRoles = ['admin', 'editor', 'viewer', 'moderator'] function hasPermission(userRole: string): boolean { - return allowedRoles.includes(userRole) // O(n) every call + return allowedRoles.includes(userRole) // O(n) every call } // In a loop, this becomes O(n × m) function filterAuthorizedUsers(users: User[]): User[] { - return users.filter(user => allowedRoles.includes(user.role)) + return users.filter((user) => allowedRoles.includes(user.role)) // 1000 users × 4 roles = 4000 comparisons } ``` @@ -31,11 +31,11 @@ function filterAuthorizedUsers(users: User[]): User[] { const allowedRoles = new Set(['admin', 'editor', 'viewer', 'moderator']) function hasPermission(userRole: string): boolean { - return allowedRoles.has(userRole) // O(1) every call + return allowedRoles.has(userRole) // O(1) every call } function filterAuthorizedUsers(users: User[]): User[] { - return users.filter(user => allowedRoles.has(user.role)) + return users.filter((user) => allowedRoles.has(user.role)) // 1000 users × O(1) = 1000 operations } ``` @@ -44,19 +44,22 @@ function filterAuthorizedUsers(users: User[]): User[] { ```typescript // Incorrect - O(n) search -const users: User[] = [/* ... */] +const users: User[] = [ + /* ... */ +] function findUserById(id: string): User | undefined { - return users.find(u => u.id === id) // Scans entire array + return users.find((u) => u.id === id) // Scans entire array } // Correct - O(1) lookup -const userById = new Map(users.map(u => [u.id, u])) +const userById = new Map(users.map((u) => [u.id, u])) function findUserById(id: string): User | undefined { return userById.get(id) } ``` **When to stick with arrays:** + - Small collections (< 10 items) - One-time lookups where conversion cost exceeds benefit - When you need array methods like `.map()`, `.filter()`, `.slice()` diff --git a/.agents/skills/typescript/references/runtime-use-string-methods.md b/.agents/skills/typescript/references/runtime-use-string-methods.md index 7315c1124d1..e908fb95a9e 100644 --- a/.agents/skills/typescript/references/runtime-use-string-methods.md +++ b/.agents/skills/typescript/references/runtime-use-string-methods.md @@ -25,7 +25,7 @@ function containsSearchTerm(text: string, term: string): boolean { } function formatOrderId(id: number): string { - return ('000000' + id).slice(-6) // Pad to 6 digits + return ('000000' + id).slice(-6) // Pad to 6 digits } ``` @@ -33,9 +33,7 @@ function formatOrderId(id: number): string { ```typescript function isImageFile(filename: string): boolean { - return filename.endsWith('.jpg') || - filename.endsWith('.png') || - filename.endsWith('.gif') + return filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif') } function hasHttpPrefix(url: string): boolean { @@ -58,12 +56,12 @@ function formatOrderId(id: number): string { const sanitized = input.replaceAll('<', '<').replaceAll('>', '>') // at() for negative indexing -const lastChar = filename.at(-1) // Last character -const extension = filename.split('.').at(-1) // Last segment +const lastChar = filename.at(-1) // Last character +const extension = filename.split('.').at(-1) // Last segment // trimStart/trimEnd for directional trimming -const trimmedLeft = ' text '.trimStart() // 'text ' -const trimmedRight = ' text '.trimEnd() // ' text' +const trimmedLeft = ' text '.trimStart() // 'text ' +const trimmedRight = ' text '.trimEnd() // ' text' // repeat for string multiplication const separator = '-'.repeat(40) @@ -71,6 +69,7 @@ const indent = ' '.repeat(depth) ``` **When regex is still needed:** + - Complex pattern matching - Capture groups - Case-insensitive matching (`/pattern/i`) diff --git a/.agents/skills/typescript/references/safety-assertion-functions.md b/.agents/skills/typescript/references/safety-assertion-functions.md index c59a3c640f0..e9472e4522e 100644 --- a/.agents/skills/typescript/references/safety-assertion-functions.md +++ b/.agents/skills/typescript/references/safety-assertion-functions.md @@ -40,7 +40,7 @@ function shipOrder(order: Order | null): void { ```typescript interface ValidOrder extends Order { status: 'pending' - items: [OrderItem, ...OrderItem[]] // Non-empty array + items: [OrderItem, ...OrderItem[]] // Non-empty array } function assertValidOrder(order: Order | null): asserts order is ValidOrder { @@ -58,7 +58,7 @@ function assertValidOrder(order: Order | null): asserts order is ValidOrder { function processOrder(order: Order | null): void { assertValidOrder(order) // order is now typed as ValidOrder - submitOrder(order) // Type-safe + submitOrder(order) // Type-safe } function shipOrder(order: Order | null): void { @@ -85,6 +85,7 @@ function processUser(user: User | null): void { ``` **Benefits:** + - Centralizes validation logic - Automatic type narrowing after assertion - Clearer intent than if-throw patterns diff --git a/.agents/skills/typescript/references/safety-const-assertions.md b/.agents/skills/typescript/references/safety-const-assertions.md index 2d06a9b50a0..3f978ccc13d 100644 --- a/.agents/skills/typescript/references/safety-const-assertions.md +++ b/.agents/skills/typescript/references/safety-const-assertions.md @@ -19,7 +19,7 @@ const config = { } // Type: { apiUrl: string; retries: number; methods: string[] } -function makeRequest(method: 'GET' | 'POST'): void { } +function makeRequest(method: 'GET' | 'POST'): void {} makeRequest(config.methods[0]) // Error: Argument of type 'string' is not assignable to 'GET' | 'POST' @@ -41,9 +41,9 @@ const config = { } as const // Type: { readonly apiUrl: 'https://api.example.com'; readonly retries: 3; readonly methods: readonly ['GET', 'POST'] } -function makeRequest(method: 'GET' | 'POST'): void { } +function makeRequest(method: 'GET' | 'POST'): void {} -makeRequest(config.methods[0]) // Works: 'GET' is assignable to 'GET' | 'POST' +makeRequest(config.methods[0]) // Works: 'GET' is assignable to 'GET' | 'POST' const STATUS = { PENDING: 'pending', @@ -51,24 +51,25 @@ const STATUS = { } as const // Type: { readonly PENDING: 'pending'; readonly ACTIVE: 'active' } -type StatusType = typeof STATUS[keyof typeof STATUS] // 'pending' | 'active' +type StatusType = (typeof STATUS)[keyof typeof STATUS] // 'pending' | 'active' ``` **For function parameters:** ```typescript // Incorrect - tuple becomes array -function setCoordinates(coords: [number, number]): void { } -setCoordinates([10, 20]) // Error: number[] not assignable to [number, number] +function setCoordinates(coords: [number, number]): void {} +setCoordinates([10, 20]) // Error: number[] not assignable to [number, number] // Correct - const preserves tuple -setCoordinates([10, 20] as const) // Works +setCoordinates([10, 20] as const) // Works // Or inline -function setCoordinates(coords: readonly [number, number]): void { } +function setCoordinates(coords: readonly [number, number]): void {} ``` **When to use const assertions:** + - Configuration objects that shouldn't change - Enum-like objects with string values - Array/tuple literals passed to functions expecting specific types diff --git a/.agents/skills/typescript/references/safety-exhaustive-checks.md b/.agents/skills/typescript/references/safety-exhaustive-checks.md index e78a71eeb87..9bc887ebd30 100644 --- a/.agents/skills/typescript/references/safety-exhaustive-checks.md +++ b/.agents/skills/typescript/references/safety-exhaustive-checks.md @@ -51,7 +51,7 @@ function getStatusMessage(status: OrderStatus): string { case 'delivered': return 'Order complete' default: - return assertNever(status) // Compile error if case missed + return assertNever(status) // Compile error if case missed } } @@ -66,7 +66,7 @@ const statusMessages: Record = { pending: 'Order received', processing: 'Preparing your order', shipped: 'On the way', - delivered: 'Order complete', + delivered: 'Order complete' // Missing key causes: Property 'cancelled' is missing in type } @@ -76,6 +76,7 @@ function getStatusMessage(status: OrderStatus): string { ``` **Benefits:** + - Compile-time error when union expands - Self-documenting: all cases explicitly handled - Runtime safety via assertNever fallback diff --git a/.agents/skills/typescript/references/safety-no-unchecked-indexed-access.md b/.agents/skills/typescript/references/safety-no-unchecked-indexed-access.md index 2a524efbe6a..125bccb029d 100644 --- a/.agents/skills/typescript/references/safety-no-unchecked-indexed-access.md +++ b/.agents/skills/typescript/references/safety-no-unchecked-indexed-access.md @@ -21,12 +21,12 @@ With `noUncheckedIndexedAccess` enabled, TypeScript adds `undefined` to the type ```typescript const users = ['Alice', 'Bob', 'Charlie'] -const first = users[0] // Type: string (lies — could be undefined) -console.log(first.toUpperCase()) // No error, but crashes if array is empty +const first = users[0] // Type: string (lies — could be undefined) +console.log(first.toUpperCase()) // No error, but crashes if array is empty const scores: Record = { math: 95 } -const science = scores['science'] // Type: number (lies — key doesn't exist) -console.log(science.toFixed(2)) // No error, but crashes at runtime +const science = scores['science'] // Type: number (lies — key doesn't exist) +console.log(science.toFixed(2)) // No error, but crashes at runtime ``` **Correct (with noUncheckedIndexedAccess):** @@ -42,18 +42,18 @@ console.log(science.toFixed(2)) // No error, but crashes at runtime ```typescript const users = ['Alice', 'Bob', 'Charlie'] -const first = users[0] // Type: string | undefined -console.log(first.toUpperCase()) // Error: 'first' is possibly undefined +const first = users[0] // Type: string | undefined +console.log(first.toUpperCase()) // Error: 'first' is possibly undefined // Handle the undefined case if (first) { - console.log(first.toUpperCase()) // OK after narrowing + console.log(first.toUpperCase()) // OK after narrowing } const scores: Record = { math: 95 } -const science = scores['science'] // Type: number | undefined +const science = scores['science'] // Type: number | undefined if (science !== undefined) { - console.log(science.toFixed(2)) // OK after narrowing + console.log(science.toFixed(2)) // OK after narrowing } ``` @@ -71,14 +71,15 @@ function getRequired(items: string[], index: number): string { if (index < 0 || index >= items.length) { throw new RangeError(`Index ${index} out of bounds`) } - return items[index]! // Safe — bounds checked above + return items[index]! // Safe — bounds checked above } // Array.at() returns T | undefined regardless of this flag -const last = items.at(-1) // Already T | undefined +const last = items.at(-1) // Already T | undefined ``` **When to disable:** + - Legacy codebases with heavy array indexing (migration cost too high) - Performance-critical inner loops where the narrowing pattern adds overhead diff --git a/.agents/skills/typescript/references/safety-prefer-unknown-over-any.md b/.agents/skills/typescript/references/safety-prefer-unknown-over-any.md index 69787b5840c..a60218b55e6 100644 --- a/.agents/skills/typescript/references/safety-prefer-unknown-over-any.md +++ b/.agents/skills/typescript/references/safety-prefer-unknown-over-any.md @@ -20,7 +20,7 @@ function processApiResponse(data: any): string { async function fetchData(): Promise { const response = await fetch('/api/data') - return response.json() // Returns Promise, loses all type info + return response.json() // Returns Promise, loses all type info } ``` @@ -46,7 +46,7 @@ function processApiResponse(data: unknown): string { if (!isApiResponse(data)) { throw new Error('Invalid API response') } - return data.user.name.toUpperCase() // Type-safe access + return data.user.name.toUpperCase() // Type-safe access } ``` @@ -54,7 +54,7 @@ function processApiResponse(data: unknown): string { ```typescript // Incorrect -const config = JSON.parse(configString) as AppConfig // Unsafe assertion +const config = JSON.parse(configString) as AppConfig // Unsafe assertion // Correct function parseConfig(configString: string): AppConfig { @@ -69,6 +69,7 @@ function parseConfig(configString: string): AppConfig { ``` **When any is acceptable:** + - Migrating JavaScript to TypeScript incrementally - Third-party library workarounds (with `// @ts-expect-error`) - Truly dynamic code where type is unknowable diff --git a/.agents/skills/typescript/references/safety-strict-null-checks.md b/.agents/skills/typescript/references/safety-strict-null-checks.md index 35ba0087c20..c155bb32e0e 100644 --- a/.agents/skills/typescript/references/safety-strict-null-checks.md +++ b/.agents/skills/typescript/references/safety-strict-null-checks.md @@ -15,11 +15,11 @@ With `strictNullChecks`, TypeScript distinguishes between `T`, `T | null`, and ` // tsconfig.json: { "strictNullChecks": false } function getUser(id: string): User { - return userMap.get(id) // Returns User | undefined, but typed as User + return userMap.get(id) // Returns User | undefined, but typed as User } const user = getUser('123') -console.log(user.email) // No error, but crashes if user is undefined +console.log(user.email) // No error, but crashes if user is undefined ``` **Correct (strictNullChecks enabled):** @@ -28,22 +28,22 @@ console.log(user.email) // No error, but crashes if user is undefined // tsconfig.json: { "strict": true } (includes strictNullChecks) function getUser(id: string): User | undefined { - return userMap.get(id) // Correctly typed as User | undefined + return userMap.get(id) // Correctly typed as User | undefined } const user = getUser('123') -console.log(user.email) // Error: 'user' is possibly 'undefined' +console.log(user.email) // Error: 'user' is possibly 'undefined' // Must handle the undefined case if (user) { - console.log(user.email) // Type narrowed to User + console.log(user.email) // Type narrowed to User } // Or use optional chaining -console.log(user?.email) // string | undefined +console.log(user?.email) // string | undefined // Or assert when you're certain -const confirmedUser = getUser('123')! // Non-null assertion (use sparingly) +const confirmedUser = getUser('123')! // Non-null assertion (use sparingly) ``` **Common patterns with strictNullChecks:** diff --git a/.agents/skills/typescript/references/safety-use-type-guards.md b/.agents/skills/typescript/references/safety-use-type-guards.md index 633c1f79170..7039d85dbac 100644 --- a/.agents/skills/typescript/references/safety-use-type-guards.md +++ b/.agents/skills/typescript/references/safety-use-type-guards.md @@ -19,12 +19,12 @@ interface User { } function handleUserEvent(event: MessageEvent): void { - const user = event.data as User // Unsafe assertion - sendEmail(user.email) // Crashes if data isn't actually a User + const user = event.data as User // Unsafe assertion + sendEmail(user.email) // Crashes if data isn't actually a User } function processResponse(data: unknown): User[] { - return data as User[] // No runtime check + return data as User[] // No runtime check } ``` @@ -46,7 +46,7 @@ function handleUserEvent(event: MessageEvent): void { console.error('Invalid user data received') return } - sendEmail(event.data.email) // Type-safe: event.data is User + sendEmail(event.data.email) // Type-safe: event.data is User } function processResponse(data: unknown): User[] { @@ -76,9 +76,9 @@ function isSuccess(result: ApiResult): result is SuccessResult { function handleResult(result: ApiResult): void { if (isSuccess(result)) { - console.log(result.data.email) // Type narrowed to SuccessResult + console.log(result.data.email) // Type narrowed to SuccessResult } else { - console.error(result.message) // Type narrowed to ErrorResult + console.error(result.message) // Type narrowed to ErrorResult } } ``` diff --git a/.agents/skills/typescript/references/tscfg-enable-incremental.md b/.agents/skills/typescript/references/tscfg-enable-incremental.md index e55c0a12e22..eda2a96df89 100644 --- a/.agents/skills/typescript/references/tscfg-enable-incremental.md +++ b/.agents/skills/typescript/references/tscfg-enable-incremental.md @@ -52,16 +52,14 @@ tsc # 15s first build, 1-3s subsequent builds "declaration": true, "declarationMap": true }, - "references": [ - { "path": "../shared" }, - { "path": "../utils" } - ] + "references": [{ "path": "../shared" }, { "path": "../utils" }] } ``` **Note:** The `composite` flag implies `incremental: true` and requires `declaration: true`. **When to disable incremental:** + - CI environments where cache isn't preserved between runs - One-off type-checking scripts diff --git a/.agents/skills/typescript/references/tscfg-erasable-syntax-only.md b/.agents/skills/typescript/references/tscfg-erasable-syntax-only.md index 5bbc6d0a33f..f8c2dfc7baf 100644 --- a/.agents/skills/typescript/references/tscfg-erasable-syntax-only.md +++ b/.agents/skills/typescript/references/tscfg-erasable-syntax-only.md @@ -50,7 +50,7 @@ export type OrderStatus = 'pending' | 'shipped' | 'delivered' export const OrderStatus = { Pending: 'pending', Shipped: 'shipped', - Delivered: 'delivered', + Delivered: 'delivered' } as const satisfies Record // Module-level functions instead of namespace @@ -80,6 +80,7 @@ class UserService { ``` **When NOT to use this flag:** + - Projects using a bundler (esbuild, swc, Vite) that supports enum transformation - Libraries that need to support both bundled and unbundled consumers - Codebases with extensive enum usage where migration cost is high diff --git a/.agents/skills/typescript/references/tscfg-exclude-properly.md b/.agents/skills/typescript/references/tscfg-exclude-properly.md index 880cb0479f9..a96ce5db3a6 100644 --- a/.agents/skills/typescript/references/tscfg-exclude-properly.md +++ b/.agents/skills/typescript/references/tscfg-exclude-properly.md @@ -34,14 +34,7 @@ TypeScript walks through all included directories to discover files. Overly broa "rootDir": "src" }, "include": ["src/**/*"], - "exclude": [ - "node_modules", - "dist", - "coverage", - "**/*.test.ts", - "**/*.spec.ts", - "**/__tests__/**" - ] + "exclude": ["node_modules", "dist", "coverage", "**/*.test.ts", "**/*.spec.ts", "**/__tests__/**"] } ``` @@ -77,6 +70,7 @@ tsc --explainFiles ``` **Common files to exclude:** + - `node_modules` (always) - Build output directories (`dist`, `build`, `out`) - Test files for production builds diff --git a/.agents/skills/typescript/references/tscfg-isolate-modules.md b/.agents/skills/typescript/references/tscfg-isolate-modules.md index cf9e9e6b5d5..813d0dc0ab2 100644 --- a/.agents/skills/typescript/references/tscfg-isolate-modules.md +++ b/.agents/skills/typescript/references/tscfg-isolate-modules.md @@ -29,7 +29,7 @@ export const enum Status { // user.ts import { Status } from './constants' -const status = Status.Active // Requires reading constants.ts to inline +const status = Status.Active // Requires reading constants.ts to inline ``` **Correct (single-file transpilable):** @@ -47,14 +47,15 @@ const status = Status.Active // Requires reading constants.ts to inline ```typescript // constants.ts -export enum Status { // Regular enum, not const enum +export enum Status { + // Regular enum, not const enum Active = 'active', Inactive = 'inactive' } // user.ts import { Status } from './constants' -const status = Status.Active // Reference preserved, no cross-file read +const status = Status.Active // Reference preserved, no cross-file read ``` **Build pipeline integration:** @@ -70,6 +71,7 @@ export default { ``` **Code patterns blocked by isolatedModules:** + - `const enum` (use regular `enum` or union types instead) - `export =` / `import =` syntax - Re-exporting types without `type` keyword diff --git a/.agents/skills/typescript/references/tscfg-isolated-declarations.md b/.agents/skills/typescript/references/tscfg-isolated-declarations.md index 2932cbe98cd..bac3fd9aae2 100644 --- a/.agents/skills/typescript/references/tscfg-isolated-declarations.md +++ b/.agents/skills/typescript/references/tscfg-isolated-declarations.md @@ -47,11 +47,13 @@ export function calculateTotal(items: CartItem[]): number { ``` **What requires annotation under isolatedDeclarations:** + - Exported function return types - Exported variable types when not inferable from a literal - Exported class method return types **What does NOT need annotation:** + - Local variables and functions (not exported) - Function parameters (already required by TypeScript) - Exports initialized with literals (`export const MAX = 100` is fine) diff --git a/.agents/skills/typescript/references/tscfg-project-references.md b/.agents/skills/typescript/references/tscfg-project-references.md index 7e4d35d39d0..634771bc870 100644 --- a/.agents/skills/typescript/references/tscfg-project-references.md +++ b/.agents/skills/typescript/references/tscfg-project-references.md @@ -66,9 +66,7 @@ my-app/ "declaration": true, "outDir": "dist" }, - "references": [ - { "path": "../shared" } - ], + "references": [{ "path": "../shared" }], "include": ["src/**/*"] } ``` @@ -90,6 +88,7 @@ tsc --build # Builds only changed projects ``` **Benefits:** + - Parallel compilation of independent projects - Change in `shared/` only rebuilds dependents - Declaration files used as API boundaries diff --git a/.agents/skills/typescript/references/tscfg-skip-lib-check.md b/.agents/skills/typescript/references/tscfg-skip-lib-check.md index 77fcdf99068..d25db6e1d7e 100644 --- a/.agents/skills/typescript/references/tscfg-skip-lib-check.md +++ b/.agents/skills/typescript/references/tscfg-skip-lib-check.md @@ -57,6 +57,7 @@ The `skipLibCheck` option skips type-checking of declaration files (`.d.ts`). Si This only skips checking the default library files (lib.d.ts), not third-party declarations. **When to disable skipLibCheck:** + - Debugging type conflicts between declaration files - Publishing a library where you want to verify `.d.ts` output - Encountering mysterious type errors that might originate in declarations diff --git a/.agents/skills/typescript/references/tscfg-strict-function-types.md b/.agents/skills/typescript/references/tscfg-strict-function-types.md index db0fe76b605..8e3bef2ed3b 100644 --- a/.agents/skills/typescript/references/tscfg-strict-function-types.md +++ b/.agents/skills/typescript/references/tscfg-strict-function-types.md @@ -25,7 +25,7 @@ type Handler = (event: T) => void // Without strictFunctionTypes, TypeScript uses bidirectional // (bivariant) checking - comparing structures both ways -const handler: Handler = (e: Event) => { } // Allowed but unsafe +const handler: Handler = (e: Event) => {} // Allowed but unsafe ``` **Correct (fast variance checking):** @@ -43,7 +43,7 @@ type Handler = (event: T) => void // With strictFunctionTypes, TypeScript uses contravariant // checking for parameters - faster and type-safe -const handler: Handler = (e: Event) => { } // Error: Event is not MouseEvent +const handler: Handler = (e: Event) => {} // Error: Event is not MouseEvent ``` **Note:** The `strict` flag enables `strictFunctionTypes` along with other strict options. Enable `strict` for all new projects. @@ -53,12 +53,12 @@ const handler: Handler = (e: Event) => { } // Error: Event is not M ```typescript // Use method syntax for intentional bivariance interface EventEmitter { - emit(event: T): void // Method syntax = bivariant + emit(event: T): void // Method syntax = bivariant } // vs property syntax for contravariance interface StrictEmitter { - emit: (event: T) => void // Property syntax = contravariant + emit: (event: T) => void // Property syntax = contravariant } ``` diff --git a/.agents/skills/typescript/references/type-avoid-deep-generics.md b/.agents/skills/typescript/references/type-avoid-deep-generics.md index 019f673e822..405f87aaee0 100644 --- a/.agents/skills/typescript/references/type-avoid-deep-generics.md +++ b/.agents/skills/typescript/references/type-avoid-deep-generics.md @@ -28,7 +28,7 @@ type CachedResponse = PaginatedResponse<{ }> // Usage creates 4+ levels of nesting -function fetchUsers(): CachedResponse { } +function fetchUsers(): CachedResponse {} // Compiler must resolve: CachedResponse → PaginatedResponse<...> → ApiResponse<...> ``` @@ -57,7 +57,7 @@ interface ApiResponse { // Compose at usage site instead of nesting type UserListResponse = ApiResponse & CacheInfo> -function fetchUsers(): UserListResponse { } +function fetchUsers(): UserListResponse {} // Single-level generic instantiation ``` diff --git a/.agents/skills/typescript/references/type-avoid-large-unions.md b/.agents/skills/typescript/references/type-avoid-large-unions.md index 630745034df..54967bbed91 100644 --- a/.agents/skills/typescript/references/type-avoid-large-unions.md +++ b/.agents/skills/typescript/references/type-avoid-large-unions.md @@ -14,9 +14,15 @@ Union type checking is quadratic — TypeScript compares each union member pairw ```typescript // Auto-generated from GraphQL schema — 200+ event types type AnalyticsEvent = - | 'page_view' | 'button_click' | 'form_submit' | 'scroll_depth' - | 'video_play' | 'video_pause' | 'video_complete' | 'ad_impression' - // ... 200 more event types from analytics schema + | 'page_view' + | 'button_click' + | 'form_submit' + | 'scroll_depth' + | 'video_play' + | 'video_pause' + | 'video_complete' + | 'ad_impression' +// ... 200 more event types from analytics schema // 200 members = 40,000 pairwise comparisons per usage ``` @@ -25,7 +31,7 @@ type AnalyticsEvent = ```typescript type AnalyticsEvent = string & { readonly __brand: 'AnalyticsEvent' } -const VALID_EVENTS = new Set(['page_view', 'button_click', 'form_submit', /* ... */]) +const VALID_EVENTS = new Set(['page_view', 'button_click', 'form_submit' /* ... */]) function createEvent(name: string): AnalyticsEvent { if (!VALID_EVENTS.has(name)) { @@ -48,6 +54,7 @@ type AppEvent = UserEvent | PageEvent | FormEvent ``` **When flat unions are fine:** + - Small unions (< 20 members) have negligible cost - Unions of primitive literals used in few places - `string | number | boolean` style utility unions diff --git a/.agents/skills/typescript/references/type-explicit-return-types.md b/.agents/skills/typescript/references/type-explicit-return-types.md index 239c32cf153..202155bab1a 100644 --- a/.agents/skills/typescript/references/type-explicit-return-types.md +++ b/.agents/skills/typescript/references/type-explicit-return-types.md @@ -15,13 +15,13 @@ Explicit return types accelerate compilation by eliminating inference overhead. export function fetchUserProfile(userId: string) { // Compiler must analyze entire function body to infer return type return fetch(`/api/users/${userId}`) - .then(res => res.json()) - .then(data => ({ + .then((res) => res.json()) + .then((data) => ({ id: data.id as string, name: data.name as string, email: data.email as string, createdAt: new Date(data.created_at), - permissions: data.permissions as Permission[], + permissions: data.permissions as Permission[] })) } // Inferred: Promise<{ id: string; name: string; email: string; createdAt: Date; permissions: Permission[] }> @@ -40,23 +40,25 @@ interface UserProfile { export function fetchUserProfile(userId: string): Promise { return fetch(`/api/users/${userId}`) - .then(res => res.json()) - .then(data => ({ + .then((res) => res.json()) + .then((data) => ({ id: data.id, name: data.name, email: data.email, createdAt: new Date(data.created_at), - permissions: data.permissions, + permissions: data.permissions })) } ``` **When to skip explicit return types:** + - Private/internal functions with simple returns - Arrow functions in local scope - Functions where the return type is obvious (e.g., `(): void`) **Benefits:** + - Declaration files use named type instead of expanded inline type - Faster incremental compilation when function body changes - Better error messages pointing to return type mismatch diff --git a/.agents/skills/typescript/references/type-extract-conditional-types.md b/.agents/skills/typescript/references/type-extract-conditional-types.md index ed421769c4c..5b2ea57cbd8 100644 --- a/.agents/skills/typescript/references/type-extract-conditional-types.md +++ b/.agents/skills/typescript/references/type-extract-conditional-types.md @@ -14,11 +14,7 @@ Inline conditional types are re-evaluated on every function call. Extracting the ```typescript function processResponse( response: T -): T extends { data: infer D } - ? D extends Array - ? Item[] - : D - : never { +): T extends { data: infer D } ? (D extends Array ? Item[] : D) : never { // Compiler re-computes this complex conditional on every call return response.data } @@ -50,6 +46,7 @@ function getFirstItem(collection: T): UnwrapArray { ``` **Benefits:** + - Type alias acts as a cache boundary - Reduces duplicate computation across multiple call sites - Improves IDE responsiveness for autocomplete diff --git a/.agents/skills/typescript/references/type-interfaces-over-intersections.md b/.agents/skills/typescript/references/type-interfaces-over-intersections.md index fab502b60b1..2b11365d653 100644 --- a/.agents/skills/typescript/references/type-interfaces-over-intersections.md +++ b/.agents/skills/typescript/references/type-interfaces-over-intersections.md @@ -34,6 +34,7 @@ interface ExtendedOrder extends Order, Timestamps { ``` **When to use intersections:** + - Combining function types or primitives (interfaces cannot extend these) - Creating mapped or conditional types - One-off type combinations not reused elsewhere diff --git a/.agents/skills/typescript/references/type-limit-recursion-depth.md b/.agents/skills/typescript/references/type-limit-recursion-depth.md index e988ef33dd9..585f3a3a766 100644 --- a/.agents/skills/typescript/references/type-limit-recursion-depth.md +++ b/.agents/skills/typescript/references/type-limit-recursion-depth.md @@ -17,13 +17,7 @@ type DeepPartial = { } // No depth limit - deeply nested objects cause exponential expansion -type JSONValue = - | string - | number - | boolean - | null - | JSONValue[] - | { [key: string]: JSONValue } +type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue } // Infinite recursion potential ``` @@ -31,21 +25,20 @@ type JSONValue = ```typescript type DeepPartial = Depth['length'] extends 5 - ? T // Stop at depth 5 + ? T // Stop at depth 5 : { - [P in keyof T]?: T[P] extends object - ? DeepPartial - : T[P] + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] } type JSONValue = Depth['length'] extends 10 ? unknown - : | string - | number - | boolean - | null - | JSONValue<[...Depth, 1]>[] - | { [key: string]: JSONValue<[...Depth, 1]> } + : + | string + | number + | boolean + | null + | JSONValue<[...Depth, 1]>[] + | { [key: string]: JSONValue<[...Depth, 1]> } ``` **Alternative (use built-in utilities):** @@ -59,6 +52,7 @@ type Config = Partial ``` **When unbounded recursion is acceptable:** + - Types with guaranteed shallow depth (max 2-3 levels) - Internal types not exposed in public APIs diff --git a/.agents/skills/typescript/references/type-simplify-mapped-types.md b/.agents/skills/typescript/references/type-simplify-mapped-types.md index 65118e02447..f4781762738 100644 --- a/.agents/skills/typescript/references/type-simplify-mapped-types.md +++ b/.agents/skills/typescript/references/type-simplify-mapped-types.md @@ -33,11 +33,7 @@ type ComplexTransform = { **Correct (composed utility types):** ```typescript -type TransformValue = T extends Date - ? string - : T extends number - ? string - : T +type TransformValue = T extends Date ? string : T extends number ? string : T type TransformObject = { [K in keyof T]: TransformProperty @@ -51,20 +47,20 @@ type TransformProperty = T extends Function ? TransformObject : TransformValue -type TransformArray = T extends object - ? TransformObject[] - : T[] +type TransformArray = T extends object ? TransformObject[] : T[] // Each utility is cached independently type TransformedUser = TransformObject ``` **Benefits:** + - Each small utility type is cached separately - Easier to debug type errors - More reusable across the codebase **When complex mapped types are acceptable:** + - Internal utility types used in few places - Types that genuinely require complex logic diff --git a/src/main/git/repo.ts b/src/main/git/repo.ts index 081dec71609..a20b0cd14de 100644 --- a/src/main/git/repo.ts +++ b/src/main/git/repo.ts @@ -10,9 +10,13 @@ const execFileAsync = promisify(execFile) */ export function isGitRepo(path: string): boolean { try { - if (!existsSync(path) || !statSync(path).isDirectory()) return false + if (!existsSync(path) || !statSync(path).isDirectory()) { + return false + } // .git dir or file (for worktrees) or bare repo - if (existsSync(join(path, '.git'))) return true + if (existsSync(join(path, '.git'))) { + return true + } // Might be a bare repo — ask git const result = execSync('git rev-parse --is-inside-work-tree', { cwd: path, @@ -73,7 +77,9 @@ function getGitConfigValue(path: string, key: string): string { function normalizeUsername(value: string): string { const trimmed = value.trim() - if (!trimmed) return '' + if (!trimmed) { + return '' + } const localPart = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed return localPart.replace(/^\d+\+/, '') @@ -85,7 +91,9 @@ function getGhLogin(): string { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() - if (apiLogin) return normalizeUsername(apiLogin) + if (apiLogin) { + return normalizeUsername(apiLogin) + } } catch { // Fall through to auth status parsing } @@ -100,7 +108,9 @@ function getGhLogin(): string { const activeAccountMatch = output.match( /Active account:\s+true[\s\S]*?account\s+([A-Za-z0-9-]+)/ ) - if (activeAccountMatch?.[1]) return normalizeUsername(activeAccountMatch[1]) + if (activeAccountMatch?.[1]) { + return normalizeUsername(activeAccountMatch[1]) + } const accountMatch = output.match(/Logged in to github\.com account\s+([A-Za-z0-9-]+)/) return normalizeUsername(accountMatch?.[1] ?? '') @@ -154,10 +164,18 @@ export function getDefaultBaseRef(path: string): string { // Fall through to explicit remote branch probes. } - if (hasGitRef(path, 'refs/remotes/origin/main')) return 'origin/main' - if (hasGitRef(path, 'refs/remotes/origin/master')) return 'origin/master' - if (hasGitRef(path, 'refs/heads/main')) return 'main' - if (hasGitRef(path, 'refs/heads/master')) return 'master' + if (hasGitRef(path, 'refs/remotes/origin/main')) { + return 'origin/main' + } + if (hasGitRef(path, 'refs/remotes/origin/master')) { + return 'origin/master' + } + if (hasGitRef(path, 'refs/heads/main')) { + return 'main' + } + if (hasGitRef(path, 'refs/heads/master')) { + return 'master' + } return 'origin/main' } @@ -184,17 +202,27 @@ async function getDefaultBaseRefAsync(path: string): Promise { // Fall through to explicit remote branch probes. } - if (await hasGitRefAsync(path, 'refs/remotes/origin/main')) return 'origin/main' - if (await hasGitRefAsync(path, 'refs/remotes/origin/master')) return 'origin/master' - if (await hasGitRefAsync(path, 'refs/heads/main')) return 'main' - if (await hasGitRefAsync(path, 'refs/heads/master')) return 'master' + if (await hasGitRefAsync(path, 'refs/remotes/origin/main')) { + return 'origin/main' + } + if (await hasGitRefAsync(path, 'refs/remotes/origin/master')) { + return 'origin/master' + } + if (await hasGitRefAsync(path, 'refs/heads/main')) { + return 'main' + } + if (await hasGitRefAsync(path, 'refs/heads/master')) { + return 'master' + } return 'origin/main' } export async function searchBaseRefs(path: string, query: string, limit = 25): Promise { const normalizedQuery = normalizeRefSearchQuery(query) - if (!normalizedQuery) return [] + if (!normalizedQuery) { + return [] + } try { const { stdout } = await execFileAsync( @@ -218,7 +246,9 @@ export async function searchBaseRefs(path: string, query: string, limit = 25): P .map((line) => line.trim()) .filter((line) => line && line !== 'origin/HEAD') .filter((line) => { - if (seen.has(line)) return false + if (seen.has(line)) { + return false + } seen.add(line) return true }) diff --git a/src/main/git/status.ts b/src/main/git/status.ts index da8dd55c56a..87ccf2ae5cd 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -20,7 +20,9 @@ export async function getStatus(worktreePath: string): Promise ) for (const line of stdout.split('\n')) { - if (!line) continue + if (!line) { + continue + } if (line.startsWith('1 ') || line.startsWith('2 ')) { // Changed entries: "1 XY sub mH mI mW hH path" or "2 XY sub mH mI mW hH X\tscore\tpath\torigPath" diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts index 7ef1376b016..70b07beac21 100644 --- a/src/main/git/worktree.ts +++ b/src/main/git/worktree.ts @@ -12,7 +12,9 @@ export function parseWorktreeList(output: string): GitWorktreeInfo[] { const blocks = output.trim().split('\n\n') for (const block of blocks) { - if (!block.trim()) continue + if (!block.trim()) { + continue + } const lines = block.trim().split('\n') let path = '' @@ -69,7 +71,9 @@ export function addWorktree( baseBranch?: string ): void { const args = ['worktree', 'add', '-b', branch, worktreePath] - if (baseBranch) args.push(baseBranch) + if (baseBranch) { + args.push(baseBranch) + } execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8', @@ -86,7 +90,9 @@ export async function removeWorktree( force = false ): Promise { const args = ['worktree', 'remove', worktreePath] - if (force) args.push('--force') + if (force) { + args.push('--force') + } await execFileAsync('git', args, { cwd: repoPath, encoding: 'utf-8' diff --git a/src/main/github/client.ts b/src/main/github/client.ts index c204fe4005d..d5621252ed8 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -7,7 +7,7 @@ const execFileAsync = promisify(execFile) // Concurrency limiter - max 4 parallel gh processes const MAX_CONCURRENT = 4 let running = 0 -const queue: Array<() => void> = [] +const queue: (() => void)[] = [] function acquire(): Promise { if (running < MAX_CONCURRENT) { @@ -25,7 +25,9 @@ function acquire(): Promise { function release(): void { running-- const next = queue.shift() - if (next) next() + if (next) { + next() + } } /** @@ -104,13 +106,13 @@ export async function listIssues(repoPath: string, limit = 20): Promise - }> + labels: { name: string }[] + }[] return data.map((d) => ({ number: d.number, title: d.title, @@ -127,19 +129,25 @@ export async function listIssues(repoPath: string, limit = 20): Promise) { + for (const check of rollup as { status?: string; conclusion?: string; state?: string }[]) { const conclusion = check.conclusion?.toUpperCase() const status = check.status?.toUpperCase() const state = check.state?.toUpperCase() @@ -162,7 +170,11 @@ function deriveCheckStatus(rollup: unknown[] | null | undefined): CheckStatus { } } - if (hasFailure) return 'failure' - if (hasPending) return 'pending' + if (hasFailure) { + return 'failure' + } + if (hasPending) { + return 'pending' + } return 'success' } diff --git a/src/main/hooks.ts b/src/main/hooks.ts index 554a062c049..f5433e2ac4a 100644 --- a/src/main/hooks.ts +++ b/src/main/hooks.ts @@ -16,7 +16,9 @@ function parseOrcaYaml(content: string): OrcaHooks | null { // Match top-level "scripts:" block const scriptsMatch = content.match(/^scripts:\s*$/m) - if (!scriptsMatch) return null + if (!scriptsMatch) { + return null + } const afterScripts = content.slice(scriptsMatch.index! + scriptsMatch[0].length) const lines = afterScripts.split('\n') @@ -26,7 +28,9 @@ function parseOrcaYaml(content: string): OrcaHooks | null { for (const line of lines) { // Another top-level key (not indented) — stop parsing scripts block - if (/^\S/.test(line) && line.trim().length > 0) break + if (/^\S/.test(line) && line.trim().length > 0) { + break + } // Indented key like " setup: |" or " archive: |" const keyMatch = line.match(/^ (setup|archive):\s*\|?\s*$/) @@ -42,7 +46,7 @@ function parseOrcaYaml(content: string): OrcaHooks | null { // Content line (indented by 4+ spaces under a key) if (currentKey && line.startsWith(' ')) { - currentValue += line.slice(4) + '\n' + currentValue += `${line.slice(4)}\n` } } @@ -51,7 +55,9 @@ function parseOrcaYaml(content: string): OrcaHooks | null { hooks.scripts[currentKey] = currentValue.trimEnd() } - if (!hooks.scripts.setup && !hooks.scripts.archive) return null + if (!hooks.scripts.setup && !hooks.scripts.archive) { + return null + } return hooks } @@ -60,7 +66,9 @@ function parseOrcaYaml(content: string): OrcaHooks | null { */ export function loadHooks(repoPath: string): OrcaHooks | null { const yamlPath = join(repoPath, 'orca.yaml') - if (!existsSync(yamlPath)) return null + if (!existsSync(yamlPath)) { + return null + } try { const content = readFileSync(yamlPath, 'utf-8') @@ -103,7 +111,9 @@ export function getEffectiveHooks(repo: Repo): OrcaHooks | null { } } - if (!hooks.scripts.setup && !hooks.scripts.archive) return null + if (!hooks.scripts.setup && !hooks.scripts.archive) { + return null + } return hooks } diff --git a/src/main/index.ts b/src/main/index.ts index a0587e9dfab..ec05396bae5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -109,6 +109,7 @@ function createWindow(): BrowserWindow { // File drag-and-drop: the preload script handles the drop event (because // File.path is only available there), sends paths here, and we relay to renderer. + ipcMain.removeAllListeners('terminal:file-dropped-from-preload') ipcMain.on('terminal:file-dropped-from-preload', (_event, args: { paths: string[] }) => { if (!mainWindow.isDestroyed()) { for (const p of args.paths) { @@ -286,7 +287,10 @@ app.whenReady().then(() => { app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createWindow() + registerRepoHandlers(mainWindow, store!) + registerWorktreeHandlers(mainWindow, store!) registerPtyHandlers(mainWindow) + setupAutoUpdater(mainWindow, { onBeforeQuit: () => store?.flush() }) } }) }) diff --git a/src/main/ipc/filesystem.ts b/src/main/ipc/filesystem.ts index b1c29f27ea1..376650627a9 100644 --- a/src/main/ipc/filesystem.ts +++ b/src/main/ipc/filesystem.ts @@ -17,7 +17,7 @@ function isPathAllowed(targetPath: string, store: Store): boolean { for (const repo of repos) { // Allow paths within the repo itself if ( - resolvedTarget.startsWith(resolve(repo.path) + '/') || + resolvedTarget.startsWith(`${resolve(repo.path)}/`) || resolvedTarget === resolve(repo.path) ) { return true @@ -29,7 +29,7 @@ function isPathAllowed(targetPath: string, store: Store): boolean { if (settings.workspaceDir) { const resolvedWorkspace = resolve(settings.workspaceDir) if ( - resolvedTarget.startsWith(resolvedWorkspace + '/') || + resolvedTarget.startsWith(`${resolvedWorkspace}/`) || resolvedTarget === resolvedWorkspace ) { return true @@ -45,7 +45,9 @@ function isPathAllowed(targetPath: string, store: Store): boolean { function isBinaryBuffer(buffer: Buffer): boolean { const len = Math.min(buffer.length, 8192) for (let i = 0; i < len; i++) { - if (buffer[i] === 0) return true + if (buffer[i] === 0) { + return true + } } return false } @@ -66,7 +68,9 @@ export function registerFilesystemHandlers(store: Store): void { })) .sort((a, b) => { // Directories first, then alphabetical - if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1 + if (a.isDirectory !== b.isDirectory) { + return a.isDirectory ? -1 : 1 + } return a.name.localeCompare(b.name) }) }) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 4bf10f5fe23..03b28dd01b0 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -13,6 +13,13 @@ let loadGeneration = 0 const ptyLoadGeneration = new Map() export function registerPtyHandlers(mainWindow: BrowserWindow): void { + // Remove any previously registered handlers so we can re-register them + // (e.g. when macOS re-activates the app and creates a new window). + ipcMain.removeHandler('pty:spawn') + ipcMain.removeHandler('pty:resize') + ipcMain.removeHandler('pty:kill') + ipcMain.removeAllListeners('pty:write') + // Kill orphaned PTY processes from previous page loads when the renderer reloads. // PTYs tagged with the current loadGeneration were spawned during THIS page load // and must be preserved — only kill PTYs from earlier generations. diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index b502d455ccb..ef6fc1e6cf4 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -1,4 +1,5 @@ -import { BrowserWindow, dialog, ipcMain } from 'electron' +import type { BrowserWindow } from 'electron' +import { dialog, ipcMain } from 'electron' import { randomUUID } from 'crypto' import type { Store } from '../persistence' import type { Repo } from '../../shared/types' @@ -12,6 +13,17 @@ import { } from '../git/repo' export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): void { + // Remove any previously registered handlers so we can re-register them + // (e.g. when macOS re-activates the app and creates a new window). + ipcMain.removeHandler('repos:list') + ipcMain.removeHandler('repos:add') + ipcMain.removeHandler('repos:remove') + ipcMain.removeHandler('repos:update') + ipcMain.removeHandler('repos:pickFolder') + ipcMain.removeHandler('repos:getGitUsername') + ipcMain.removeHandler('repos:getBaseRefDefault') + ipcMain.removeHandler('repos:searchBaseRefs') + ipcMain.handle('repos:list', () => { return store.getRepos() }) @@ -23,7 +35,9 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v // Check if already added const existing = store.getRepos().find((r) => r.path === args.path) - if (existing) return existing + if (existing) { + return existing + } const repo: Repo = { id: randomUUID(), @@ -55,7 +69,9 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v } ) => { const updated = store.updateRepo(args.repoId, args.updates) - if (updated) notifyReposChanged(mainWindow) + if (updated) { + notifyReposChanged(mainWindow) + } return updated } ) @@ -64,19 +80,25 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }) - if (result.canceled || result.filePaths.length === 0) return null + if (result.canceled || result.filePaths.length === 0) { + return null + } return result.filePaths[0] }) ipcMain.handle('repos:getGitUsername', (_event, args: { repoId: string }) => { const repo = store.getRepo(args.repoId) - if (!repo) return '' + if (!repo) { + return '' + } return getGitUsername(repo.path) }) ipcMain.handle('repos:getBaseRefDefault', async (_event, args: { repoId: string }) => { const repo = store.getRepo(args.repoId) - if (!repo) return 'origin/main' + if (!repo) { + return 'origin/main' + } return getBaseRefDefault(repo.path) }) @@ -84,7 +106,9 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v 'repos:searchBaseRefs', async (_event, args: { repoId: string; query: string; limit?: number }) => { const repo = store.getRepo(args.repoId) - if (!repo) return [] + if (!repo) { + return [] + } return searchBaseRefs(repo.path, args.query, args.limit ?? 25) } ) diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index cce43510478..2274837b18d 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -1,4 +1,5 @@ -import { BrowserWindow, ipcMain } from 'electron' +import type { BrowserWindow } from 'electron' +import { ipcMain } from 'electron' import { join, basename } from 'path' import type { Store } from '../persistence' import type { Worktree, WorktreeMeta } from '../../shared/types' @@ -7,6 +8,15 @@ import { getGitUsername, getDefaultBaseRef, getAvailableBranchName } from '../gi import { getEffectiveHooks, loadHooks, runHook, hasHooksFile } from '../hooks' export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store): void { + // Remove any previously registered handlers so we can re-register them + // (e.g. when macOS re-activates the app and creates a new window). + ipcMain.removeHandler('worktrees:listAll') + ipcMain.removeHandler('worktrees:list') + ipcMain.removeHandler('worktrees:create') + ipcMain.removeHandler('worktrees:remove') + ipcMain.removeHandler('worktrees:updateMeta') + ipcMain.removeHandler('hooks:check') + ipcMain.handle('worktrees:listAll', async () => { const repos = store.getRepos() const allWorktrees: Worktree[] = [] @@ -25,7 +35,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store ipcMain.handle('worktrees:list', async (_event, args: { repoId: string }) => { const repo = store.getRepo(args.repoId) - if (!repo) return [] + if (!repo) { + return [] + } const gitWorktrees = await listWorktrees(repo.path) return gitWorktrees.map((gw) => { @@ -39,7 +51,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store 'worktrees:create', async (_event, args: { repoId: string; name: string; baseBranch?: string }) => { const repo = store.getRepo(args.repoId) - if (!repo) throw new Error(`Repo not found: ${args.repoId}`) + if (!repo) { + throw new Error(`Repo not found: ${args.repoId}`) + } const settings = store.getSettings() @@ -78,7 +92,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store // Re-list to get the freshly created worktree info const gitWorktrees = await listWorktrees(repo.path) const created = gitWorktrees.find((gw) => gw.path === worktreePath) - if (!created) throw new Error('Worktree created but not found in listing') + if (!created) { + throw new Error('Worktree created but not found in listing') + } const worktreeId = `${repo.id}::${worktreePath}` const metaUpdates: Partial = @@ -108,7 +124,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store async (_event, args: { worktreeId: string; force?: boolean }) => { const { repoId, worktreePath } = parseWorktreeId(args.worktreeId) const repo = store.getRepo(repoId) - if (!repo) throw new Error(`Repo not found: ${repoId}`) + if (!repo) { + throw new Error(`Repo not found: ${repoId}`) + } // Run archive hook before removal const hooks = getEffectiveHooks(repo) @@ -142,7 +160,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store ipcMain.handle('hooks:check', (_event, args: { repoId: string }) => { const repo = store.getRepo(args.repoId) - if (!repo) return { hasHooks: false, hooks: null } + if (!repo) { + return { hasHooks: false, hooks: null } + } const has = hasHooksFile(repo.path) const hooks = has ? loadHooks(repo.path) : null @@ -178,7 +198,9 @@ function mergeWorktree( function parseWorktreeId(worktreeId: string): { repoId: string; worktreePath: string } { const sepIdx = worktreeId.indexOf('::') - if (sepIdx === -1) throw new Error(`Invalid worktreeId: ${worktreeId}`) + if (sepIdx === -1) { + throw new Error(`Invalid worktreeId: ${worktreeId}`) + } return { repoId: worktreeId.slice(0, sepIdx), worktreePath: worktreeId.slice(sepIdx + 2) @@ -196,7 +218,9 @@ function formatWorktreeRemovalError(error: unknown, worktreePath: string, force: ? `Failed to force delete worktree at ${worktreePath}.` : `Failed to delete worktree at ${worktreePath}.` - if (!(error instanceof Error)) return fallback + if (!(error instanceof Error)) { + return fallback + } const errorWithStreams = error as Error & { stderr?: string; stdout?: string } const details = [errorWithStreams.stderr, errorWithStreams.stdout, error.message] diff --git a/src/main/persistence.ts b/src/main/persistence.ts index bb1e3b4f7ae..e83a521d6ec 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -44,12 +44,16 @@ export class Store { } private scheduleSave(): void { - if (this.writeTimer) clearTimeout(this.writeTimer) + if (this.writeTimer) { + clearTimeout(this.writeTimer) + } this.writeTimer = setTimeout(() => { this.writeTimer = null try { const dir = dirname(DATA_FILE) - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } const tmpFile = `${DATA_FILE}.tmp` writeFileSync(tmpFile, JSON.stringify(this.state, null, 2), 'utf-8') renameSync(tmpFile, DATA_FILE) @@ -67,7 +71,9 @@ export class Store { getRepo(id: string): Repo | undefined { const repo = this.state.repos.find((r) => r.id === id) - if (!repo) return undefined + if (!repo) { + return undefined + } return this.hydrateRepo(repo) } @@ -93,7 +99,9 @@ export class Store { updates: Partial> ): Repo | null { const repo = this.state.repos.find((r) => r.id === id) - if (!repo) return null + if (!repo) { + return null + } Object.assign(repo, updates) this.scheduleSave() return this.hydrateRepo(repo) @@ -199,7 +207,9 @@ export class Store { } try { const dir = dirname(DATA_FILE) - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } const tmpFile = `${DATA_FILE}.tmp` writeFileSync(tmpFile, JSON.stringify(this.state, null, 2), 'utf-8') renameSync(tmpFile, DATA_FILE) diff --git a/src/main/system-fonts.ts b/src/main/system-fonts.ts index ed664e23647..7839f447afe 100644 --- a/src/main/system-fonts.ts +++ b/src/main/system-fonts.ts @@ -4,8 +4,12 @@ let cachedFonts: string[] | null = null let fontsPromise: Promise | null = null export async function listSystemFontFamilies(): Promise { - if (cachedFonts) return cachedFonts - if (fontsPromise) return fontsPromise + if (cachedFonts) { + return cachedFonts + } + if (fontsPromise) { + return fontsPromise + } fontsPromise = loadSystemFontFamilies() .then((fonts) => { @@ -28,8 +32,12 @@ export function warmSystemFontFamilies(): void { } function loadSystemFontFamilies(): Promise { - if (process.platform === 'darwin') return listMacFonts() - if (process.platform === 'win32') return listWindowsFonts() + if (process.platform === 'darwin') { + return listMacFonts() + } + if (process.platform === 'win32') { + return listWindowsFonts() + } return listLinuxFonts() } @@ -37,11 +45,11 @@ function listMacFonts(): Promise { return execFileText('system_profiler', ['SPFontsDataType', '-json'], 32 * 1024 * 1024).then( (output) => { const parsed = JSON.parse(output) as { - SPFontsDataType?: Array<{ - typefaces?: Array<{ + SPFontsDataType?: { + typefaces?: { family?: string - }> - }> + }[] + }[] } return uniqueSorted( @@ -98,7 +106,7 @@ function execFileText(command: string, args: string[], maxBuffer: number): Promi }) } -function uniqueSorted(values: Array): string[] { +function uniqueSorted(values: (string | undefined)[]): string[] { return Array.from( new Set( values diff --git a/src/main/updater.ts b/src/main/updater.ts index ebbec0462a1..84726b012ab 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -8,6 +8,7 @@ let mainWindowRef: BrowserWindow | null = null let currentStatus: UpdateStatus = { state: 'idle' } let userInitiatedCheck = false let onBeforeQuitCleanup: (() => void) | null = null +let autoUpdaterInitialized = false function sendStatus(status: UpdateStatus): void { currentStatus = status @@ -85,6 +86,11 @@ export function setupAutoUpdater( // This is safe since we don't publish prerelease versions. autoUpdater.allowPrerelease = true + if (autoUpdaterInitialized) { + return + } + autoUpdaterInitialized = true + autoUpdater.on('checking-for-update', () => { sendStatus({ state: 'checking', userInitiated: userInitiatedCheck || undefined }) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index d2a9e4f96f5..6e91befc31a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -10,7 +10,9 @@ document.addEventListener( 'dragover', (e) => { e.preventDefault() - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy' + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy' + } }, true ) @@ -21,13 +23,17 @@ document.addEventListener( e.preventDefault() e.stopPropagation() const files = e.dataTransfer?.files - if (!files || files.length === 0) return + if (!files || files.length === 0) { + return + } const paths: string[] = [] for (let i = 0; i < files.length; i++) { // webUtils.getPathForFile is the Electron 28+ replacement for File.path const filePath = webUtils.getPathForFile(files[i]) - if (filePath) paths.push(filePath) + if (filePath) { + paths.push(filePath) + } } if (paths.length > 0) { @@ -206,7 +212,6 @@ const api = { ipcRenderer.invoke('git:discard', args) }, - ui: { get: (): Promise => ipcRenderer.invoke('ui:get'), set: (args: Record): Promise => ipcRenderer.invoke('ui:set', args), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 607e2479698..1ee3b0c07d2 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -43,7 +43,6 @@ function App(): React.JSX.Element { const setRightSidebarOpen = useAppStore((s) => s.setRightSidebarOpen) const setRightSidebarTab = useAppStore((s) => s.setRightSidebarTab) - // Subscribe to IPC push events useIpcEvents() @@ -167,7 +166,7 @@ function App(): React.JSX.Element { mq.addEventListener('change', handler) return () => mq.removeEventListener('change', handler) } - }, [settings?.theme]) + }, [settings]) // Refresh GitHub data (PR/issue status) when window regains focus useEffect(() => { @@ -246,7 +245,6 @@ function App(): React.JSX.Element { return () => window.removeEventListener('keydown', onKeyDown, { capture: true }) }, [openModal, repos.length, setRightSidebarTab, setRightSidebarOpen]) - return (
diff --git a/src/renderer/src/components/TerminalSearch.tsx b/src/renderer/src/components/TerminalSearch.tsx index ade5a99c1b3..6219173500f 100644 --- a/src/renderer/src/components/TerminalSearch.tsx +++ b/src/renderer/src/components/TerminalSearch.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react' import { ChevronUp, ChevronDown, X, CaseSensitive, Regex } from 'lucide-react' import type { SearchAddon } from '@xterm/addon-search' -interface TerminalSearchProps { +type TerminalSearchProps = { isOpen: boolean onClose: () => void searchAddon: SearchAddon | null @@ -68,7 +68,9 @@ export default function TerminalSearch({ [onClose, findNext, findPrevious] ) - if (!isOpen) return null + if (!isOpen) { + return null + } return (
e.area !== 'untracked') - if (cancelled) return + if (cancelled) { + return + } // Initialize sections const initialSections: DiffSection[] = changed.map((entry) => ({ @@ -66,7 +68,9 @@ export default function CombinedDiffViewer({ }) ) - if (cancelled) return + if (cancelled) { + return + } setSections((prev) => prev.map((section, i) => ({ @@ -136,7 +140,7 @@ export default function CombinedDiffViewer({ return (
{/* Section header */} diff --git a/src/renderer/src/components/editor/DiffViewer.tsx b/src/renderer/src/components/editor/DiffViewer.tsx index 0368d6fefc9..2fc9501ae0b 100644 --- a/src/renderer/src/components/editor/DiffViewer.tsx +++ b/src/renderer/src/components/editor/DiffViewer.tsx @@ -4,7 +4,7 @@ import { Columns2, Rows2 } from 'lucide-react' import { useAppStore } from '@/store' import '@/lib/monaco-setup' -interface DiffViewerProps { +type DiffViewerProps = { originalContent: string modifiedContent: string language: string diff --git a/src/renderer/src/components/editor/MonacoEditor.tsx b/src/renderer/src/components/editor/MonacoEditor.tsx index 010f9529af6..eab15acaa91 100644 --- a/src/renderer/src/components/editor/MonacoEditor.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.tsx @@ -4,7 +4,7 @@ import type { editor } from 'monaco-editor' import { useAppStore } from '@/store' import '@/lib/monaco-setup' -interface MonacoEditorProps { +type MonacoEditorProps = { filePath: string content: string language: string @@ -51,12 +51,14 @@ export default function MonacoEditor({ // Update editor options when settings change useEffect(() => { - if (!editorRef.current || !settings) return + if (!editorRef.current || !settings) { + return + } editorRef.current.updateOptions({ fontSize: settings.terminalFontSize, fontFamily: settings.terminalFontFamily || 'monospace' }) - }, [settings?.terminalFontSize, settings?.terminalFontFamily]) + }, [settings]) return ( { - if (!activeWorktreeId) return null + if (!activeWorktreeId) { + return null + } for (const worktrees of Object.values(worktreesByRepo)) { const wt = worktrees.find((w) => w.id === activeWorktreeId) - if (wt) return wt.path + if (wt) { + return wt.path + } } return null }, [activeWorktreeId, worktreesByRepo]) @@ -48,7 +52,9 @@ export default function FileExplorer(): React.JSX.Element { // Load directory contents const loadDir = useCallback( async (dirPath: string, depth: number) => { - if (dirCache[dirPath]?.children.length > 0 || dirCache[dirPath]?.loading) return + if (dirCache[dirPath]?.children.length > 0 || dirCache[dirPath]?.loading) { + return + } setDirCache((prev) => ({ ...prev, @@ -90,7 +96,9 @@ export default function FileExplorer(): React.JSX.Element { // Load root when worktree changes useEffect(() => { - if (!worktreePath) return + if (!worktreePath) { + return + } setDirCache({}) void loadDir(worktreePath, -1) }, [worktreePath]) // eslint-disable-line react-hooks/exhaustive-deps @@ -109,13 +117,17 @@ export default function FileExplorer(): React.JSX.Element { // Flatten tree into visible rows const flatRows = useMemo(() => { - if (!worktreePath) return [] + if (!worktreePath) { + return [] + } const result: TreeNode[] = [] const addChildren = (parentPath: string): void => { const cached = dirCache[parentPath] - if (!cached?.children) return + if (!cached?.children) { + return + } for (const child of cached.children) { result.push(child) @@ -139,7 +151,9 @@ export default function FileExplorer(): React.JSX.Element { const handleClick = useCallback( (node: TreeNode) => { - if (!activeWorktreeId) return + if (!activeWorktreeId) { + return + } if (node.isDirectory) { toggleDir(activeWorktreeId, node.path) diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index d4423b03998..51bfb68a936 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -63,16 +63,22 @@ export default function SourceControl(): React.JSX.Element { // Find active worktree path const worktreePath = useMemo(() => { - if (!activeWorktreeId) return null + if (!activeWorktreeId) { + return null + } for (const worktrees of Object.values(worktreesByRepo)) { const wt = worktrees.find((w) => w.id === activeWorktreeId) - if (wt) return wt.path + if (wt) { + return wt.path + } } return null }, [activeWorktreeId, worktreesByRepo]) const fetchStatus = useCallback(async () => { - if (!activeWorktreeId || !worktreePath) return + if (!activeWorktreeId || !worktreePath) { + return + } try { const entries = (await window.api.git.status({ worktreePath })) as GitStatusEntry[] setGitStatus(activeWorktreeId, entries) @@ -86,7 +92,9 @@ export default function SourceControl(): React.JSX.Element { void fetchStatus() pollRef.current = setInterval(() => void fetchStatus(), 3000) return () => { - if (pollRef.current) clearInterval(pollRef.current) + if (pollRef.current) { + clearInterval(pollRef.current) + } } }, [fetchStatus]) @@ -110,15 +118,20 @@ export default function SourceControl(): React.JSX.Element { const toggleSection = useCallback((section: string) => { setCollapsedSections((prev) => { const next = new Set(prev) - if (next.has(section)) next.delete(section) - else next.add(section) + if (next.has(section)) { + next.delete(section) + } else { + next.add(section) + } return next }) }, []) const handleStage = useCallback( async (filePath: string) => { - if (!worktreePath) return + if (!worktreePath) { + return + } try { await window.api.git.stage({ worktreePath, filePath }) void fetchStatus() @@ -131,7 +144,9 @@ export default function SourceControl(): React.JSX.Element { const handleUnstage = useCallback( async (filePath: string) => { - if (!worktreePath) return + if (!worktreePath) { + return + } try { await window.api.git.unstage({ worktreePath, filePath }) void fetchStatus() @@ -144,7 +159,9 @@ export default function SourceControl(): React.JSX.Element { const handleDiscard = useCallback( async (filePath: string) => { - if (!worktreePath) return + if (!worktreePath) { + return + } try { await window.api.git.discard({ worktreePath, filePath }) void fetchStatus() @@ -156,13 +173,17 @@ export default function SourceControl(): React.JSX.Element { ) const handleViewAllChanges = useCallback(() => { - if (!activeWorktreeId || !worktreePath) return + if (!activeWorktreeId || !worktreePath) { + return + } openAllDiffs(activeWorktreeId, worktreePath) }, [activeWorktreeId, worktreePath, openAllDiffs]) const handleOpenDiff = useCallback( (entry: GitStatusEntry) => { - if (!activeWorktreeId) return + if (!activeWorktreeId) { + return + } const language = detectLanguage(entry.path) const absolutePath = worktreePath ? `${worktreePath}/${entry.path}` : entry.path openDiff(activeWorktreeId, absolutePath, entry.path, language, entry.area === 'staged') @@ -202,7 +223,9 @@ export default function SourceControl(): React.JSX.Element { {SECTION_ORDER.map((area) => { const items = grouped[area] - if (items.length === 0) return null + if (items.length === 0) { + return null + } const isCollapsed = collapsedSections.has(area) return ( diff --git a/src/renderer/src/components/right-sidebar/index.tsx b/src/renderer/src/components/right-sidebar/index.tsx index 18f1947e01a..77086874b68 100644 --- a/src/renderer/src/components/right-sidebar/index.tsx +++ b/src/renderer/src/components/right-sidebar/index.tsx @@ -21,7 +21,9 @@ export default function RightSidebar(): React.JSX.Element { const handleMouseMove = useCallback( (e: MouseEvent) => { - if (!isResizing.current) return + if (!isResizing.current) { + return + } // Dragging left = larger width (opposite of left sidebar) const delta = startX.current - e.clientX const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth.current + delta)) diff --git a/src/renderer/src/components/settings/GeneralPane.tsx b/src/renderer/src/components/settings/GeneralPane.tsx index d3b9c031f11..b2f92d17e2f 100644 --- a/src/renderer/src/components/settings/GeneralPane.tsx +++ b/src/renderer/src/components/settings/GeneralPane.tsx @@ -142,9 +142,7 @@ export function GeneralPane({

Updates

-

- Current version: {appVersion ?? '…'} -

+

Current version: {appVersion ?? '…'}

diff --git a/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx b/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx index 806b92afeb9..9daed11b9a6 100644 --- a/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx +++ b/src/renderer/src/components/sidebar/AddWorktreeDialog.tsx @@ -104,7 +104,9 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { ) const handleCreate = useCallback(async () => { - if (!repoId || !name.trim()) return + if (!repoId || !name.trim()) { + return + } setCreating(true) try { const wt = await createWorktree(repoId, name.trim()) @@ -161,7 +163,9 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { resetTimeoutRef.current = null } - if (isOpen) return + if (isOpen) { + return + } resetTimeoutRef.current = window.setTimeout(() => { setRepoId('') @@ -182,10 +186,14 @@ const AddWorktreeDialog = React.memo(function AddWorktreeDialog() { // Focus and select name input when suggestion is applied React.useEffect(() => { - if (!isOpen || !repoId || !suggestedName) return + if (!isOpen || !repoId || !suggestedName) { + return + } requestAnimationFrame(() => { const input = nameInputRef.current - if (!input) return + if (!input) { + return + } input.focus() input.select() }) @@ -318,7 +326,9 @@ function getSuggestedSpaceName( worktreesByRepo: Record, nestWorkspaces: boolean ): string { - if (!repoId) return SPACE_NAMES[0] + if (!repoId) { + return SPACE_NAMES[0] + } const usedNames = new Set() const repoWorktrees = worktreesByRepo[repoId] ?? [] @@ -365,7 +375,9 @@ function findRepoIdForWorktree( worktreeId: string | null, worktreesByRepo: Record ): string | null { - if (!worktreeId) return null + if (!worktreeId) { + return null + } for (const [repoId, worktrees] of Object.entries(worktreesByRepo)) { if (worktrees.some((worktree) => worktree.id === worktreeId)) { diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx index 2f8119f6dc1..aee20c6a123 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx @@ -42,8 +42,12 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { const handleOpenChange = useCallback( (open: boolean) => { - if (open || isDeleting) return - if (worktreeId) clearWorktreeDeleteState(worktreeId) + if (open || isDeleting) { + return + } + if (worktreeId) { + clearWorktreeDeleteState(worktreeId) + } closeModal() }, [clearWorktreeDeleteState, closeModal, isDeleting, worktreeId] @@ -51,7 +55,9 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { const handleDelete = useCallback( async (force = false) => { - if (!worktreeId) return + if (!worktreeId) { + return + } closeModal() const result = await removeWorktree(worktreeId, force) if (!result.ok) { @@ -69,8 +75,9 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { Delete Worktree - Remove {worktree?.displayName} from - git and delete its working tree folder. + Remove{' '} + {worktree?.displayName}{' '} + from git and delete its working tree folder. diff --git a/src/renderer/src/components/sidebar/GroupControls.tsx b/src/renderer/src/components/sidebar/GroupControls.tsx index 96119d79570..ff892427e10 100644 --- a/src/renderer/src/components/sidebar/GroupControls.tsx +++ b/src/renderer/src/components/sidebar/GroupControls.tsx @@ -40,7 +40,9 @@ const GroupControls = React.memo(function GroupControls() { type="single" value={groupBy} onValueChange={(v) => { - if (v) setGroupBy(v as typeof groupBy) + if (v) { + setGroupBy(v as typeof groupBy) + } }} variant="outline" size="sm" diff --git a/src/renderer/src/components/sidebar/SidebarHeader.tsx b/src/renderer/src/components/sidebar/SidebarHeader.tsx index 5ef7b05fb7d..c7d6d2261be 100644 --- a/src/renderer/src/components/sidebar/SidebarHeader.tsx +++ b/src/renderer/src/components/sidebar/SidebarHeader.tsx @@ -20,7 +20,9 @@ const SidebarHeader = React.memo(function SidebarHeader() { variant="ghost" size="icon-xs" onClick={() => { - if (!canCreateWorktree) return + if (!canCreateWorktree) { + return + } openModal('create-worktree') }} aria-label="Add worktree" diff --git a/src/renderer/src/components/sidebar/SidebarToolbar.tsx b/src/renderer/src/components/sidebar/SidebarToolbar.tsx index 4d46089c1cc..df6b5aae108 100644 --- a/src/renderer/src/components/sidebar/SidebarToolbar.tsx +++ b/src/renderer/src/components/sidebar/SidebarToolbar.tsx @@ -22,9 +22,7 @@ const SidebarToolbar = React.memo(function SidebarToolbar() {