a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add MCP config and Effect skill documentation

Add Claude Code configuration:
- MCP server configuration in .mcp.json
- Effect TypeScript skill documentation

+694
+683
.claude/skills/effect.md
··· 1 + --- 2 + name: effect 3 + description: Effect TypeScript library for functional effects, dependency injection, error handling, and async workflows. Use when working with Effect code in Tauri/TypeScript contexts. 4 + --- 5 + 6 + # Effect TypeScript 7 + 8 + Effect is a TypeScript library for building robust applications with functional programming patterns, focusing on type-safe error handling, dependency injection, and composable effects. 9 + 10 + ## Core Concepts 11 + 12 + ### Effect Type 13 + 14 + The `Effect<A, E, R>` type represents a computation that: 15 + - Produces a value of type `A` on success 16 + - May fail with error type `E` 17 + - Requires dependencies (context) of type `R` 18 + 19 + ```typescript 20 + import { Effect } from "effect"; 21 + 22 + // Effect<string, never, never> - success with string, no errors, no dependencies 23 + const hello = Effect.succeed("Hello"); 24 + 25 + // Effect<number, Error, never> - may fail with Error 26 + const divide = (a: number, b: number): Effect.Effect<number, Error> => 27 + b === 0 28 + ? Effect.fail(new Error("Division by zero")) 29 + : Effect.succeed(a / b); 30 + ``` 31 + 32 + ## Effect.gen Pattern 33 + 34 + Use `Effect.gen` for sequential effect composition with generator syntax. This is the primary pattern for working with multiple effects. 35 + 36 + ```typescript 37 + import { Effect } from "effect"; 38 + 39 + export function processData(): Effect.Effect<Result, Error, FileSystem> { 40 + return Effect.gen(function* () { 41 + // Yield effects to unwrap values 42 + const fs = yield* FileSystem.FileSystem; 43 + const config = yield* loadConfig(); 44 + const data = yield* fetchData(config.url); 45 + 46 + // Early return on conditions 47 + if (data.length === 0) { 48 + return yield* Effect.fail(new Error("No data")); 49 + } 50 + 51 + // Use regular TypeScript control flow 52 + const processed = data.map(transform); 53 + yield* saveResults(processed); 54 + 55 + return { count: processed.length, data: processed }; 56 + }); 57 + } 58 + ``` 59 + 60 + ### Key Rules for Effect.gen 61 + 62 + 1. Always yield effects with `yield*` (not `yield`) 63 + 2. Use `return yield* Effect.fail(...)` for early errors 64 + 3. Use regular TypeScript for conditionals, loops, maps 65 + 4. Unwrapped values are regular TypeScript types 66 + 5. Effects execute sequentially in order 67 + 68 + ## Services and Dependency Injection 69 + 70 + ### Defining Services 71 + 72 + Use `Context.Tag` to define injectable services: 73 + 74 + ```typescript 75 + import { Context } from "effect"; 76 + 77 + // Service with single value 78 + export class ConfigService extends Context.Tag("ConfigService")< 79 + ConfigService, 80 + { readonly apiUrl: string; readonly timeout: number } 81 + >() {} 82 + 83 + // Service with methods and state 84 + export class DatabaseService extends Context.Tag("DatabaseService")< 85 + DatabaseService, 86 + { 87 + readonly query: (sql: string) => Effect.Effect<Row[], DatabaseError>; 88 + readonly connection: Connection; 89 + } 90 + >() {} 91 + ``` 92 + 93 + ### Service Naming Convention 94 + 95 + - Service class names end with `Service` (e.g., `FpDirService`, `HonoWebService`) 96 + - Tag names match class names (passed as string to `Context.Tag`) 97 + - Service interfaces use `readonly` fields 98 + - Keep services focused and single-purpose 99 + 100 + ### Using Services 101 + 102 + Access services in Effect.gen: 103 + 104 + ```typescript 105 + export function fetchUser(id: string): Effect.Effect<User, Error, ConfigService | DatabaseService> { 106 + return Effect.gen(function* () { 107 + const config = yield* ConfigService; 108 + const db = yield* DatabaseService; 109 + 110 + const rows = yield* db.query(`SELECT * FROM users WHERE id = '${id}'`); 111 + return parseUser(rows[0]); 112 + }); 113 + } 114 + ``` 115 + 116 + ## Layers 117 + 118 + Layers provide implementations for services. They define how dependencies are constructed and wired together. 119 + 120 + ### Creating Layers 121 + 122 + ```typescript 123 + import { Layer, Effect } from "effect"; 124 + 125 + // Simple layer with static value 126 + export const ConfigLive = Layer.succeed(ConfigService, { 127 + apiUrl: "https://api.example.com", 128 + timeout: 5000, 129 + }); 130 + 131 + // Layer with effectful construction 132 + export const DatabaseLive = Layer.effect( 133 + DatabaseService, 134 + Effect.gen(function* () { 135 + const config = yield* ConfigService; // Can depend on other services 136 + const connection = yield* connectToDatabase(config.dbUrl); 137 + 138 + return { 139 + query: (sql: string) => executeQuery(connection, sql), 140 + connection, 141 + }; 142 + }), 143 + ); 144 + ``` 145 + 146 + ### Layer Composition 147 + 148 + Compose layers using `Layer.provide` and `Layer.provideMerge`: 149 + 150 + ```typescript 151 + import { Layer } from "effect"; 152 + 153 + // Layer that depends on ConfigService 154 + export const DatabaseLive = Layer.effect( 155 + DatabaseService, 156 + Effect.gen(function* () { 157 + const config = yield* ConfigService; 158 + // ... implementation 159 + }), 160 + ); 161 + 162 + // Combine layers - DatabaseService now has ConfigService available 163 + export const AppLive = DatabaseLive.pipe( 164 + Layer.provide(ConfigLive), // Provide ConfigService to DatabaseService 165 + ); 166 + 167 + // Merge multiple independent layers 168 + export const AllServicesLive = Layer.merge( 169 + ConfigLive, 170 + LoggerLive, 171 + CacheLive, 172 + ); 173 + 174 + // ProvideMerge - merge dependency layer with current layer 175 + export const FullStackLive = DatabaseLive.pipe( 176 + Layer.provideMerge(ConfigLive), // Both DatabaseService AND ConfigService available 177 + Layer.provide(Logger.pretty), // Add logging 178 + ); 179 + ``` 180 + 181 + ### Layer Patterns 182 + 183 + ```typescript 184 + // Platform-specific layers (Bun, Node, Browser) 185 + import { BunContext } from "@effect/platform-bun"; 186 + 187 + export const WebLive = HonoWebServiceLive.pipe( 188 + Layer.provideMerge(FpDirLive), 189 + Layer.provide(BunContext.layer), // Provides FileSystem, CommandExecutor, etc. 190 + Layer.provide(Logger.pretty), 191 + ); 192 + 193 + // Conditional logging levels 194 + const FP_DEBUG = process.env.FP_DEBUG === "true"; 195 + 196 + export const ReviewLive = HonoReviewServiceLive.pipe( 197 + Layer.provideMerge(ReviewSubmitServiceLive), 198 + Layer.provideMerge(FpDirLive), 199 + Layer.provide(BunContext.layer), 200 + Layer.provide(Logger.pretty), 201 + Layer.provide(Logger.minimumLogLevel(FP_DEBUG ? LogLevel.Debug : LogLevel.Info)), 202 + ); 203 + ``` 204 + 205 + ## Error Handling 206 + 207 + ### Tagged Errors 208 + 209 + Use `Data.TaggedError` for typed error handling: 210 + 211 + ```typescript 212 + import { Data } from "effect"; 213 + 214 + export class FileNotFoundError extends Data.TaggedError("FileNotFoundError")<{ 215 + readonly path: string; 216 + readonly suggestion?: string; 217 + }> { 218 + get message(): string { 219 + const base = `File not found: ${this.path}`; 220 + return this.suggestion ? `${base}\n Suggestion: ${this.suggestion}` : base; 221 + } 222 + } 223 + 224 + export class ValidationError extends Data.TaggedError("ValidationError")<{ 225 + readonly field: string; 226 + readonly reason: string; 227 + }> { 228 + get message(): string { 229 + return `Invalid ${this.field}: ${this.reason}`; 230 + } 231 + } 232 + ``` 233 + 234 + ### Error Union Types 235 + 236 + Create union types for all errors in a domain: 237 + 238 + ```typescript 239 + export type AppError = 240 + | FileNotFoundError 241 + | ValidationError 242 + | DatabaseError 243 + | NetworkError; 244 + ``` 245 + 246 + ### Handling Errors 247 + 248 + ```typescript 249 + import { Effect } from "effect"; 250 + 251 + // Return errors with Effect.fail 252 + export function readConfig(path: string): Effect.Effect<Config, FileNotFoundError, FileSystem> { 253 + return Effect.gen(function* () { 254 + const fs = yield* FileSystem.FileSystem; 255 + const exists = yield* fs.exists(path); 256 + 257 + if (!exists) { 258 + return yield* Effect.fail( 259 + new FileNotFoundError({ 260 + path, 261 + suggestion: "Run 'init' to create default config", 262 + }), 263 + ); 264 + } 265 + 266 + const content = yield* fs.readFileString(path); 267 + return parseConfig(content); 268 + }); 269 + } 270 + 271 + // Catch and transform errors 272 + const result = pipe( 273 + readConfig("config.json"), 274 + Effect.catchTag("FileNotFoundError", (error) => 275 + Effect.succeed(defaultConfig), // Provide fallback 276 + ), 277 + ); 278 + 279 + // Catch all errors 280 + const safe = pipe( 281 + dangerousOperation(), 282 + Effect.catchAll((error) => Effect.succeed(fallbackValue)), 283 + ); 284 + ``` 285 + 286 + ## Pipe and Composition 287 + 288 + Use `pipe` for function composition and transformations: 289 + 290 + ```typescript 291 + import { pipe, Effect } from "effect"; 292 + 293 + // Transform effect results 294 + const result = pipe( 295 + fetchData(), 296 + Effect.map((data) => data.toUpperCase()), 297 + Effect.tap((data) => Effect.log(`Got: ${data}`)), // Side effect, doesn't change value 298 + Effect.flatMap((data) => saveData(data)), // Chain another effect 299 + ); 300 + 301 + // Provide dependencies 302 + const executed = pipe( 303 + myEffect, 304 + Effect.provide(MyServiceLive), 305 + ); 306 + 307 + // Multiple transformations 308 + const processed = pipe( 309 + Effect.succeed([1, 2, 3]), 310 + Effect.map((arr) => arr.map((x) => x * 2)), 311 + Effect.flatMap((arr) => Effect.forEach(arr, (x) => processItem(x))), 312 + Effect.tap(() => Effect.log("Processing complete")), 313 + ); 314 + ``` 315 + 316 + ### Common Pipe Operators 317 + 318 + ```typescript 319 + // Effect.map - transform success value 320 + Effect.map((value) => transform(value)) 321 + 322 + // Effect.flatMap - chain effects (also called andThen) 323 + Effect.flatMap((value) => nextEffect(value)) 324 + 325 + // Effect.tap - side effect without changing value 326 + Effect.tap((value) => Effect.log(`Value: ${value}`)) 327 + 328 + // Effect.catchTag - handle specific error 329 + Effect.catchTag("ErrorName", (error) => Effect.succeed(fallback)) 330 + 331 + // Effect.catchAll - handle any error 332 + Effect.catchAll((error) => Effect.succeed(fallback)) 333 + 334 + // Effect.provide - provide layer/service 335 + Effect.provide(MyLayer) 336 + 337 + // Effect.asVoid - discard result 338 + Effect.asVoid 339 + ``` 340 + 341 + ## Resource Management 342 + 343 + Use `Effect.acquireRelease` for resources that need cleanup: 344 + 345 + ```typescript 346 + import { Effect } from "effect"; 347 + 348 + export function withServer(port: number): Effect.Effect<void> { 349 + return Effect.scoped( 350 + Effect.gen(function* () { 351 + // Acquire resource with cleanup 352 + const server = yield* Effect.acquireRelease( 353 + Effect.sync(() => 354 + Bun.serve({ 355 + port, 356 + fetch: app.fetch, 357 + }) 358 + ), 359 + (server) => Effect.sync(() => server.stop()), // Cleanup runs on scope exit 360 + ); 361 + 362 + yield* Effect.log(`Server started on port ${server.port}`); 363 + 364 + // Block forever - cleanup still runs on interrupt (Ctrl+C) 365 + return yield* Effect.never; 366 + }), 367 + ); 368 + } 369 + ``` 370 + 371 + ### Scoped Pattern 372 + 373 + - Wrap resource effects with `Effect.scoped` 374 + - Use `Effect.acquireRelease(acquire, release)` for resources 375 + - Release runs on scope exit (normal, error, or interrupt) 376 + - Perfect for servers, file handles, database connections 377 + 378 + ## Advanced Patterns 379 + 380 + ### Option Integration 381 + 382 + ```typescript 383 + import { Option, Effect } from "effect"; 384 + 385 + function findUser(id: string): Effect.Effect<Option.Option<User>, Error, Database> { 386 + return Effect.gen(function* () { 387 + const db = yield* Database; 388 + const rows = yield* db.query(`SELECT * FROM users WHERE id = '${id}'`); 389 + 390 + if (rows.length === 0) { 391 + return Option.none(); 392 + } 393 + 394 + return Option.some(parseUser(rows[0])); 395 + }); 396 + } 397 + 398 + // Work with Option results 399 + const result = pipe( 400 + findUser("123"), 401 + Effect.flatMap( 402 + Option.match({ 403 + onNone: () => Effect.fail(new UserNotFoundError({ id: "123" })), 404 + onSome: (user) => Effect.succeed(user), 405 + }), 406 + ), 407 + ); 408 + 409 + // Check Option values 410 + if (Option.isSome(maybeValue)) { 411 + const value = maybeValue.value; // Type-safe access 412 + } 413 + 414 + if (Option.isNone(maybeValue)) { 415 + // Handle missing value 416 + } 417 + ``` 418 + 419 + ### Schema Validation 420 + 421 + ```typescript 422 + import { Schema, Effect, Option } from "effect"; 423 + 424 + const UserSchema = Schema.Struct({ 425 + id: Schema.UUID, 426 + name: Schema.String, 427 + age: Schema.Number, 428 + email: Schema.optional(Schema.String), 429 + }); 430 + 431 + // Decode with Option (returns None on error) 432 + const maybeUser = Schema.decodeUnknownOption(UserSchema)(data); 433 + 434 + // Decode with Effect (fails with error details) 435 + const user = yield* Schema.decodeUnknown(UserSchema)(data); 436 + 437 + // Encode back to unknown 438 + const encoded = Schema.encode(UserSchema)(user); 439 + ``` 440 + 441 + ### Match Pattern (Tagged Unions) 442 + 443 + ```typescript 444 + import { Data, Match } from "effect"; 445 + 446 + type Result = Data.TaggedEnum<{ 447 + Single: { readonly value: string }; 448 + Multiple: { readonly values: readonly string[] }; 449 + Empty: {}; 450 + }>; 451 + 452 + const { Single, Multiple, Empty, $is } = Data.taggedEnum<Result>(); 453 + 454 + // Pattern match with Match.value 455 + const formatted = Match.value(result).pipe( 456 + Match.tag("Single", (r) => `Found: ${r.value}`), 457 + Match.tag("Multiple", (r) => `Found ${r.values.length} items`), 458 + Match.tag("Empty", () => "Nothing found"), 459 + Match.exhaustive, // Ensures all cases handled 460 + ); 461 + 462 + // Type guard with $is 463 + if ($is("Single")(result)) { 464 + console.log(result.value); // Type narrowed to Single 465 + } 466 + ``` 467 + 468 + ### Deferred (Promises-like) 469 + 470 + ```typescript 471 + import { Deferred, Effect } from "effect"; 472 + 473 + export function waitForSubmit(): Effect.Effect<Data, never, SubmitService> { 474 + return Effect.gen(function* () { 475 + const { submitDeferred } = yield* SubmitService; 476 + 477 + // Wait for deferred to complete (like Promise.resolve) 478 + const data = yield* Deferred.await(submitDeferred); 479 + 480 + return data; 481 + }); 482 + } 483 + 484 + // Create and complete deferred 485 + const layer = Layer.effect( 486 + SubmitService, 487 + Effect.gen(function* () { 488 + const submitDeferred = yield* Deferred.make<Data, never>(); 489 + return { submitDeferred }; 490 + }), 491 + ); 492 + 493 + // Complete from elsewhere 494 + yield* Deferred.succeed(submitDeferred, data); 495 + yield* Deferred.fail(submitDeferred, error); 496 + ``` 497 + 498 + ## Platform Integrations 499 + 500 + ### File System 501 + 502 + ```typescript 503 + import * as FileSystem from "@effect/platform/FileSystem"; 504 + import { Effect } from "effect"; 505 + 506 + export function readConfig(): Effect.Effect<Config, Error, FileSystem.FileSystem> { 507 + return Effect.gen(function* () { 508 + const fs = yield* FileSystem.FileSystem; 509 + 510 + const exists = yield* fs.exists("/path/to/config.json"); 511 + const content = yield* fs.readFileString("/path/to/config.json"); 512 + const dirs = yield* fs.readDirectory("/path/to/dir"); 513 + 514 + yield* fs.writeFileString("/output.txt", "data"); 515 + yield* fs.makeDirectory("/new/dir"); 516 + 517 + return parseConfig(content); 518 + }); 519 + } 520 + ``` 521 + 522 + ### Command Execution 523 + 524 + ```typescript 525 + import * as Command from "@effect/platform/Command"; 526 + import { Effect, pipe } from "effect"; 527 + 528 + export function openBrowser(url: string): Effect.Effect<void, PlatformError, CommandExecutor> { 529 + return pipe( 530 + Command.make("open", url), // Create command 531 + Command.start, // Start execution 532 + Effect.asVoid, // Discard result 533 + ); 534 + } 535 + 536 + export function runGit(args: string[]): Effect.Effect<string, Error, CommandExecutor> { 537 + return pipe( 538 + Command.make("git", ...args), 539 + Command.stdout("string"), 540 + ); 541 + } 542 + ``` 543 + 544 + ### Logging 545 + 546 + ```typescript 547 + import { Effect, Logger, LogLevel } from "effect"; 548 + 549 + // In effect code 550 + yield* Effect.log("Starting process"); 551 + yield* Effect.logDebug("Debug info"); 552 + yield* Effect.logError("Error occurred"); 553 + 554 + // Configure logger in layer 555 + export const AppLive = MyServiceLive.pipe( 556 + Layer.provide(Logger.pretty), 557 + Layer.provide(Logger.minimumLogLevel(LogLevel.Info)), 558 + ); 559 + ``` 560 + 561 + ## Running Effects 562 + 563 + ### In Node/Bun Applications 564 + 565 + ```typescript 566 + import { Effect } from "effect"; 567 + import { BunContext } from "@effect/platform-bun"; 568 + 569 + // Run effect with runtime 570 + const program = Effect.gen(function* () { 571 + const result = yield* myEffect(); 572 + return result; 573 + }).pipe( 574 + Effect.provide(MyServiceLive), 575 + Effect.provide(BunContext.layer), 576 + ); 577 + 578 + // Execute (returns Promise) 579 + Effect.runPromise(program).then(console.log).catch(console.error); 580 + 581 + // Top-level await 582 + const result = await Effect.runPromise(program); 583 + ``` 584 + 585 + ### In Tauri Commands 586 + 587 + Tauri commands must return Promises. Convert Effects to Promises: 588 + 589 + ```typescript 590 + import { Effect } from "effect"; 591 + 592 + #[tauri::command] 593 + async fn my_command(data: String) -> Result<Response, String> { 594 + const effect = Effect.gen(function* () { 595 + const service = yield* MyService; 596 + const result = yield* service.process(data); 597 + return result; 598 + }).pipe( 599 + Effect.provide(MyServiceLive), 600 + ); 601 + 602 + // Convert to Promise for Tauri 603 + try { 604 + const result = await Effect.runPromise(effect); 605 + Ok(result) 606 + } catch (error) { 607 + Err(error.message) 608 + } 609 + } 610 + ``` 611 + 612 + ## Best Practices 613 + 614 + ### Do's 615 + 616 + 1. Use `Effect.gen` for sequential composition 617 + 2. Define services with `Context.Tag` 618 + 3. Use tagged errors for domain errors 619 + 4. Compose layers for dependency injection 620 + 5. Use `pipe` for transformations 621 + 6. Use `Effect.acquireRelease` for resources 622 + 7. Keep services focused and single-purpose 623 + 8. Use `readonly` in service definitions 624 + 9. Leverage type inference (avoid explicit types when possible) 625 + 626 + ### Don'ts 627 + 628 + 1. Don't use `yield` without `*` (must be `yield*`) 629 + 2. Don't mix Promise/async-await with Effect (convert at boundaries) 630 + 3. Don't throw exceptions (use `Effect.fail`) 631 + 4. Don't use `any` or `unknown` in service types 632 + 5. Don't create circular service dependencies 633 + 6. Don't put business logic in layers (only construction) 634 + 635 + ### Common Patterns 636 + 637 + ```typescript 638 + // Sequential operations 639 + const result = yield* fetchData(); 640 + const processed = transform(result); 641 + yield* saveData(processed); 642 + 643 + // Parallel operations 644 + const [users, posts] = yield* Effect.all([fetchUsers(), fetchPosts()]); 645 + 646 + // Conditional effects 647 + const data = condition 648 + ? yield* fetchFromApi() 649 + : yield* fetchFromCache(); 650 + 651 + // Retry with backoff 652 + const result = yield* pipe( 653 + unreliableOperation(), 654 + Effect.retry(Schedule.exponential("100 millis")), 655 + ); 656 + 657 + // Timeout 658 + const result = yield* pipe( 659 + slowOperation(), 660 + Effect.timeout("5 seconds"), 661 + ); 662 + ``` 663 + 664 + ## Common Dependencies 665 + 666 + ```json 667 + { 668 + "dependencies": { 669 + "effect": "^3.x.x", 670 + "@effect/platform": "^0.x.x", 671 + "@effect/platform-node": "^0.x.x", 672 + "@effect/platform-bun": "^0.x.x", 673 + "@effect/schema": "^0.x.x" 674 + } 675 + } 676 + ``` 677 + 678 + ## Resources 679 + 680 + - Official docs: https://effect.website 681 + - API reference: https://effect-ts.github.io/effect 682 + - Discord: https://discord.gg/effect-ts 683 + - Examples: https://github.com/Effect-TS/effect/tree/main/packages/effect/examples
+11
.mcp.json
··· 1 + { 2 + "mcpServers": { 3 + "shadcn": { 4 + "command": "npx", 5 + "args": [ 6 + "shadcn@latest", 7 + "mcp" 8 + ] 9 + } 10 + } 11 + }