Effect-TS patterns for Platform. Use when working with platform in Effect-TS applications.
This skill provides 6 curated Effect-TS patterns for platform. Use this skill when working on tasks related to:
Rule: Use Terminal for user input/output in CLI applications, providing proper buffering and cross-platform character encoding.
Good Example:
This example demonstrates building an interactive CLI application.
import { Terminal, Effect } from "@effect/platform";
interface UserInput {
readonly name: string;
readonly email: string;
readonly age: number;
}
const program = Effect.gen(function* () {
console.log(`\n[INTERACTIVE CLI] User Information Form\n`);
// Example 1: Simple prompts
yield* Terminal.writeLine(`=== User Setup ===`);
yield* Terminal.writeLine(``);
yield* Terminal.write(`What is your name? `);
const name = yield* Terminal.readLine();
yield* Terminal.write(`What is your email? `);
const email = yield* Terminal.readLine();
yield* Terminal.write(`What is your age? `);
const ageStr = yield* Terminal.readLine();
const age = parseInt(ageStr);
// Example 2: Display collected information
yield* Terminal.writeLine(``);
yield* Terminal.writeLine(`=== Summary ===`);
yield* Terminal.writeLine(`Name: ${name}`);
yield* Terminal.writeLine(`Email: ${email}`);
yield* Terminal.writeLine(`Age: ${age}`);
// Example 3: Confirmation
yield* Terminal.writeLine(``);
yield* Terminal.write(`Confirm information? (yes/no) `);
const confirm = yield* Terminal.readLine();
if (confirm.toLowerCase() === "yes") {
yield* Terminal.writeLine(`✓ Information saved`);
} else {
yield* Terminal.writeLine(`✗ Cancelled`);
}
});
Effect.runPromise(program);
Rationale:
Terminal operations:
Pattern: Terminal.readLine().pipe(...)
Direct stdin/stdout causes issues:
Terminal enables:
Real-world example: CLI setup wizard
Rule: Use FileSystem module for safe, resource-managed file operations with proper error handling and cleanup.
Good Example:
This example demonstrates reading, writing, and manipulating files.
import { FileSystem, Effect, Stream } from "@effect/platform";
import * as fs from "fs/promises";
const program = Effect.gen(function* () {
console.log(`\n[FILESYSTEM] Demonstrating file operations\n`);
// Example 1: Write a file
console.log(`[1] Writing file:\n`);
const content = `Hello, Effect-TS!\nThis is a test file.\nCreated at ${new Date().toISOString()}`;
yield* FileSystem.writeFileUtf8("test.txt", content);
yield* Effect.log(`✓ File written: test.txt`);
// Example 2: Read the file
console.log(`\n[2] Reading file:\n`);
const readContent = yield* FileSystem.readFileUtf8("test.txt");
console.log(readContent);
// Example 3: Get file stats
console.log(`\n[3] File stats:\n`);
const stats = yield* FileSystem.stat("test.txt").pipe(
Effect.flatMap((stat) =>
Effect.succeed({
size: stat.size,
isFile: stat.isFile(),
modified: stat.mtimeMs,
})
)
);
console.log(` Size: ${stats.size} bytes`);
console.log(` Is file: ${stats.isFile}`);
console.log(` Modified: ${new Date(stats.modified).toISOString()}`);
// Example 4: Create directory and write multiple files
console.log(`\n[4] Creating directory and files:\n`);
yield* FileSystem.mkdir("test-dir");
yield* Effect.all(
Array.from({ length: 3 }, (_, i) =>
FileSystem.writeFileUtf8(
`test-dir/file-${i + 1}.txt`,
`Content of file ${i + 1}`
)
)
);
yield* Effect.log(`✓ Created directory with 3 files`);
// Example 5: List directory contents
console.log(`\n[5] Listing directory:\n`);
const entries = yield* FileSystem.readDirectory("test-dir");
entries.forEach((entry) => {
console.log(` - ${entry}`);
});
// Example 6: Append to file
console.log(`\n[6] Appending to file:\n`);
const appendContent = `\nAppended line at ${new Date().toISOString()}`;
yield* FileSystem.appendFileUtf8("test.txt", appendContent);
const finalContent = yield* FileSystem.readFileUtf8("test.txt");
console.log(`File now has ${finalContent.split("\n").length} lines`);
// Example 7: Clean up
console.log(`\n[7] Cleaning up:\n`);
yield* Effect.all(
Array.from({ length: 3 }, (_, i) =>
FileSystem.remove(`test-dir/file-${i + 1}.txt`)
)
);
yield* FileSystem.remove("test-dir");
yield* FileSystem.remove("test.txt");
yield* Effect.log(`✓ Cleanup complete`);
});
Effect.runPromise(program);
Rationale:
FileSystem operations:
Pattern: FileSystem.read(path).pipe(...)
Direct file operations without FileSystem create issues:
FileSystem enables:
Real-world example: Process log files
FileSystem.read(path).pipe(...)Rule: Use KeyValueStore for simple persistent storage of key-value pairs, enabling lightweight caching and session management.
Good Example:
This example demonstrates storing and retrieving persistent data.
import { KeyValueStore, Effect } from "@effect/platform";
interface UserSession {
readonly userId: string;
readonly token: string;
readonly expiresAt: number;
}
const program = Effect.gen(function* () {
console.log(`\n[KEYVALUESTORE] Persistent storage example\n`);
const store = yield* KeyValueStore.KeyValueStore;
// Example 1: Store session data
console.log(`[1] Storing session:\n`);
const session: UserSession = {
userId: "user-123",
token: "token-abc-def",
expiresAt: Date.now() + 3600000, // 1 hour
};
yield* store.set("session:user-123", JSON.stringify(session));
yield* Effect.log(`✓ Session stored`);
// Example 2: Retrieve stored data
console.log(`\n[2] Retrieving session:\n`);
const stored = yield* store.get("session:user-123");
if (stored._tag === "Some") {
const retrievedSession = JSON.parse(stored.value) as UserSession;
console.log(` User ID: ${retrievedSession.userId}`);
console.log(` Token: ${retrievedSession.token}`);
console.log(
` Expires: ${new Date(retrievedSession.expiresAt).toISOString()}`
);
}
// Example 3: Check if key exists
console.log(`\n[3] Checking keys:\n`);
const hasSession = yield* store.has("session:user-123");
const hasOther = yield* store.has("session:user-999");
console.log(` Has session:user-123: ${hasSession}`);
console.log(` Has session:user-999: ${hasOther}`);
// Example 4: Store multiple cache entries
console.log(`\n[4] Caching API responses:\n`);
const apiResponses = [
{ endpoint: "/api/users", data: [{ id: 1, name: "Alice" }] },
{ endpoint: "/api/posts", data: [{ id: 1, title: "First Post" }] },
{ endpoint: "/api/comments", data: [] },
];
yield* Effect.all(
apiResponses.map((item) =>
store.set(
`cache:${item.endpoint}`,
JSON.stringify(item.data)
)
)
);
yield* Effect.log(`✓ Cached ${apiResponses.length} endpoints`);
// Example 5: Retrieve cache with expiration
console.log(`\n[5] Checking cached data:\n`);
for (const item of apiResponses) {
const cached = yield* store.get(`cache:${item.endpoint}`);
if (cached._tag === "Some") {
const data = JSON.parse(cached.value);
console.log(
` ${item.endpoint}: ${Array.isArray(data) ? data.length : 1} items`
);
}
}
// Example 6: Remove specific entry
console.log(`\n[6] Removing entry:\n`);
yield* store.remove("cache:/api/comments");
const removed = yield* store.has("cache:/api/comments");
console.log(` Exists after removal: ${removed}`);
// Example 7: Iterate and count entries
console.log(`\n[7] Counting entries:\n`);
const allKeys = yield* store.entries.pipe(
Effect.map((entries) => entries.length)
);
console.log(` Total entries: ${allKeys}`);
});
Effect.runPromise(program);
Rationale:
KeyValueStore operations:
Pattern: KeyValueStore.set(key, value).pipe(...)
Without persistent storage, transient data is lost:
KeyValueStore enables:
Real-world example: Caching API responses
Rule: Use Command to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling.
Good Example:
This example demonstrates executing commands and handling their output.
import { Command, Effect, Chunk } from "@effect/platform";
// Simple command execution
const program = Effect.gen(function* () {
console.log(`\n[COMMAND] Executing shell commands\n`);
// Example 1: List files
console.log(`[1] List files in current directory:\n`);
const lsResult = yield* Command.make("ls", ["-la"]).pipe(
Command.string
);
console.log(lsResult);
// Example 2: Get current date
console.log(`\n[2] Get current date:\n`);
const dateResult = yield* Command.make("date", ["+%Y-%m-%d %H:%M:%S"]).pipe(
Command.string
);
console.log(`Current date: ${dateResult.trim()}`);
// Example 3: Capture exit code
console.log(`\n[3] Check if file exists:\n`);
const fileCheckCmd = yield* Command.make("test", [
"-f",
"/etc/passwd",
]).pipe(
Command.exitCode,
Effect.either
);
if (fileCheckCmd._tag === "Right") {
console.log(`✓ File exists (exit code: 0)`);
} else {
console.log(`✗ File not found (exit code: ${fileCheckCmd.left})`);
}
// Example 4: Execute with custom working directory
console.log(`\n[4] List TypeScript files:\n`);
const findResult = yield* Command.make("find", [
".",
"-name",
"*.ts",
"-type",
"f",
]).pipe(
Command.lines
);
const tsFiles = Chunk.take(findResult, 5); // First 5
Chunk.forEach(tsFiles, (file) => {
console.log(` - ${file}`);
});
if (Chunk.size(findResult) > 5) {
console.log(` ... and ${Chunk.size(findResult) - 5} more`);
}
// Example 5: Handle command failure
console.log(`\n[5] Handle command failure gracefully:\n`);
const failResult = yield* Command.make("false").pipe(
Command.exitCode,
Effect.catchAll((error) =>
Effect.succeed(-1) // Return -1 for any error
)
);
console.log(`Exit code: ${failResult}`);
});
Effect.runPromise(program);
Rationale:
Execute shell commands with Command:
Pattern: Command.exec("command args").pipe(...)
Shell integration without proper handling causes issues:
Command enables:
Real-world example: Build pipeline
Rule: Use Effect's platform-aware path utilities to handle separators, absolute/relative paths, and environment variables consistently.
Good Example:
This example demonstrates cross-platform path manipulation.
import { Effect, FileSystem } from "@effect/platform";
import * as Path from "node:path";
import * as OS from "node:os";
interface PathOperation {
readonly input: string;
readonly description: string;
}
// Platform info
const getPlatformInfo = () =>
Effect.gen(function* () {
const platform = process.platform;
const separator = Path.sep;
const delimiter = Path.delimiter;
const homeDir = OS.homedir();
yield* Effect.log(
`[PLATFORM] OS: ${platform}, Separator: "${separator}", Home: ${homeDir}`
);
return { platform, separator, delimiter, homeDir };
});
const program = Effect.gen(function* () {
console.log(`\n[PATH MANIPULATION] Cross-platform path operations\n`);
const platformInfo = yield* getPlatformInfo();
// Example 1: Path joining (handles separators)
console.log(`\n[1] Joining paths (handles separators automatically):\n`);
const segments = ["data", "reports", "2024"];
const joinedPath = Path.join(...segments);
yield* Effect.log(`[JOIN] Input: ${segments.join(" + ")}`);
yield* Effect.log(`[JOIN] Output: ${joinedPath}`);
// Example 2: Resolving to absolute paths
console.log(`\n[2] Resolving relative → absolute:\n`);
const relativePath = "./config/settings.json";
const absolutePath = Path.resolve(relativePath);
yield* Effect.log(`[RESOLVE] Relative: ${relativePath}`);
yield* Effect.log(`[RESOLVE] Absolute: ${absolutePath}`);
// Example 3: Path parsing
console.log(`\n[3] Parsing path components:\n`);
const filePath = "/home/user/documents/report.pdf";
const parsed = Path.parse(filePath);
yield* Effect.log(`[PARSE] Input: ${filePath}`);
yield* Effect.log(` root: ${parsed.root}`);
yield* Effect.log(` dir: ${parsed.dir}`);
yield* Effect.log(` base: ${parsed.base}`);
yield* Effect.log(` name: ${parsed.name}`);
yield* Effect.log(` ext: ${parsed.ext}`);
// Example 4: Environment variable expansion
console.log(`\n[4] Environment variable expansion:\n`);
const expandPath = (pathStr: string): string => {
let result = pathStr;
// Expand common variables
result = result.replace("$HOME", OS.homedir());
result = result.replace("~", OS.homedir());
result = result.replace("$USER", process.env.USER || "user");
result = result.replace("$PWD", process.cwd());
// Handle Windows-style env vars
result = result.replace(/%USERPROFILE%/g, OS.homedir());
result = result.replace(/%USERNAME%/g, process.env.USERNAME || "user");
result = result.replace(/%TEMP%/g, OS.tmpdir());
return result;
};
const envPaths = [
"$HOME/myapp/data",
"~/documents/file.txt",
"$PWD/config",
"/var/log/app.log",
];
for (const envPath of envPaths) {
const expanded = expandPath(envPath);
yield* Effect.log(
`[EXPAND] ${envPath} → ${expanded}`
);
}
// Example 5: Path normalization (remove redundant separators)
console.log(`\n[5] Path normalization:\n`);
const messyPaths = [
"/home//user///documents",
"C:\\Users\\\\documents\\\\file.txt",
"./config/../config/./settings",
"../data/../../root",
];
for (const messy of messyPaths) {
const normalized = Path.normalize(messy);
yield* Effect.log(
`[NORMALIZE] ${messy}`
);
yield* Effect.log(
`[NORMALIZE] → ${normalized}`
);
}
// Example 6: Safe path construction with base directory
console.log(`\n[6] Safe path construction (path traversal prevention):\n`);
const baseDir = "/var/app/data";
const safeJoin = (base: string, userPath: string): Result<string> => {
// Reject absolute paths from untrusted input
if (Path.isAbsolute(userPath)) {
return { success: false, reason: "Absolute paths not allowed" };
}
// Reject paths with ..
if (userPath.includes("..")) {
return { success: false, reason: "Path traversal attempt detected" };
}
// Resolve and verify within base
const fullPath = Path.resolve(base, userPath);
if (!fullPath.startsWith(base)) {
return { success: false, reason: "Path escapes base directory" };
}
return { success: true, path: fullPath };
};
interface Result<T> {
success: boolean;
reason?: string;
path?: T;
}
const testPaths = [
"reports/2024.json",
"/etc/passwd",
"../../../root",
"data/file.txt",
];
for (const test of testPaths) {
const result = safeJoin(baseDir, test);
if (result.success) {
yield* Effect.log(`[SAFE] ✓ ${test} → ${result.path}`);
} else {
yield* Effect.log(`[SAFE] ✗ ${test} (${result.reason})`);
}
}
// Example 7: Relative path calculation
console.log(`\n[7] Computing relative paths:\n`);
const fromDir = "/home/user/projects/myapp";
const toPath = "/home/user/data/config.json";
const relativePath2 = Path.relative(fromDir, toPath);
yield* Effect.log(`[RELATIVE] From: ${fromDir}`);
yield* Effect.log(`[RELATIVE] To: ${toPath}`);
yield* Effect.log(`[RELATIVE] Relative: ${relativePath2}`);
// Example 8: Common path patterns
console.log(`\n[8] Common patterns:\n`);
// Get file extension
const fileName = "document.tar.gz";
const ext = Path.extname(fileName);
const baseName = Path.basename(fileName);
const dirName = Path.dirname("/home/user/file.txt");
yield* Effect.log(`[PATTERNS] File: ${fileName}`);
yield* Effect.log(` basename: ${baseName}`);
yield* Effect.log(` dirname: ${dirName}`);
yield* Effect.log(` extname: ${ext}`);
// Example 9: Path segments array
console.log(`\n[9] Path segments:\n`);
const segmentPath = "/home/user/documents/report.pdf";
const segments2 = segmentPath.split(Path.sep).filter((s) => s);
yield* Effect.log(`[SEGMENTS] ${segmentPath}`);
yield* Effect.log(`[SEGMENTS] → [${segments2.map((s) => `"${s}"`).join(", ")}]`);
});
Effect.runPromise(program);
Rationale:
Path manipulation requires platform awareness:
\, Unix uses //root vs ./file$HOME, %APPDATA%Pattern: Avoid string concatenation, use path.join(), path.resolve()
String-based path handling causes problems:
Problem 1: Platform inconsistency
"C:\data\file.txt" (Windows)Problem 2: Path traversal attacks
"../../../../etc/passwd"Problem 3: Environment variable expansion
"$HOME/myapp/data"$HOME in pathProblem 4: Symlink resolution
/etc/ssl/certs/ca-bundle.crt (symlink)/usr/share/ca-certificates/ca-bundle.crtSolutions:
Platform-aware API:
path.join() handles separatorspath.resolve() creates absolute pathspath.parse() componentsVariable expansion:
$HOME, ~ → user home$USER → username$PWD → current directoryValidation:
..Rule: Use advanced file system patterns to implement efficient, reliable file operations with proper error handling and resource cleanup.
Good Example:
This example demonstrates advanced file system patterns.
import { Effect, Stream, Ref, FileSystem } from "@effect/platform";
import * as Path from "node:path";
import * as FS from "node:fs";
import * as PromiseFS from "node:fs/promises";
const program = Effect.gen(function* () {
console.log(`\n[ADVANCED FILESYSTEM] Complex file operations\n`);
// Example 1: Atomic file write with temporary file
console.log(`[1] Atomic write (crash-safe):\n`);
const atomicWrite = (
filePath: string,
content: string
): Effect.Effect<void> =>
Effect.gen(function* () {
const tempPath = `${filePath}.tmp`;
try {
// Step 1: Write to temporary file
yield* Effect.promise(() =>
PromiseFS.writeFile(tempPath, content, "utf-8")
);
yield* Effect.log(`[WRITE] Wrote to temporary file`);
// Step 2: Ensure on disk (fsync)
yield* Effect.promise(() =>
PromiseFS.writeFile(tempPath, content, "utf-8")
);
yield* Effect.log(`[FSYNC] Data on disk`);
// Step 3: Atomic rename
yield* Effect.promise(() =>
PromiseFS.rename(tempPath, filePath)
);
yield* Effect.log(`[RENAME] Atomic rename complete`);
} catch (error) {
// Cleanup on failure
try {
yield* Effect.promise(() => PromiseFS.unlink(tempPath));
} catch {
// Ignore cleanup errors
}
yield* Effect.fail(error);
}
});
// Test atomic write
const testFile = "./test-file.txt";
yield* atomicWrite(testFile, "Important configuration\n");
// Verify file
const content = yield* Effect.promise(() =>
PromiseFS.readFile(testFile, "utf-8")
);
yield* Effect.log(`[READ] Got: "${content.trim()}"\n`);
// Example 2: Streaming read (memory efficient)
console.log(`[2] Streaming read (handle large files):\n`);
const streamingRead = (filePath: string) =>
Effect.gen(function* () {
let byteCount = 0;
let lineCount = 0;
const readStream = FS.createReadStream(filePath, {
encoding: "utf-8",
highWaterMark: 64 * 1024, // 64KB chunks
});
yield* Effect.log(`[STREAM] Starting read with 64KB chunks`);
const processLine = (line: string) =>
Effect.gen(function* () {
by
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PaulJPhilp/effect-patterns-platform下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer