first commit

This commit is contained in:
Ichitux
2026-04-05 03:08:53 +02:00
commit 1082d36c12
28015 changed files with 3767672 additions and 0 deletions

21
node_modules/better-result/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dillon Mulroy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

511
node_modules/better-result/README.md generated vendored Normal file
View File

@@ -0,0 +1,511 @@
# better-result
Lightweight Result type for TypeScript with generator-based composition.
## Install
**New to better-result?**
```sh
npx better-result init
```
**Upgrading from v1?**
```sh
npx better-result migrate
```
## Quick Start
```ts
import { Result } from "better-result";
// Wrap throwing functions
const parsed = Result.try(() => JSON.parse(input));
// Check and use
if (Result.isOk(parsed)) {
console.log(parsed.value);
} else {
console.error(parsed.error);
}
// Or use pattern matching
const message = parsed.match({
ok: (data) => `Got: ${data.name}`,
err: (e) => `Failed: ${e.message}`,
});
```
## Contents
- [Creating Results](#creating-results)
- [Transforming Results](#transforming-results)
- [Handling Errors](#handling-errors)
- [Extracting Values](#extracting-values)
- [Generator Composition](#generator-composition)
- [Retry Support](#retry-support)
- [UnhandledException](#unhandledexception)
- [Panic](#panic)
- [Tagged Errors](#tagged-errors)
- [Serialization](#serialization)
- [API Reference](#api-reference)
- [Agents & AI](#agents--ai)
## Creating Results
```ts
// Success
const ok = Result.ok(42);
// Error
const err = Result.err(new Error("failed"));
// From throwing function
const result = Result.try(() => riskyOperation());
// From promise
const result = await Result.tryPromise(() => fetch(url));
// With custom error handling
const result = Result.try({
try: () => JSON.parse(input),
catch: (e) => new ParseError(e),
});
```
## Transforming Results
```ts
const result = Result.ok(2)
.map((x) => x * 2) // Ok(4)
.andThen(
(
x, // Chain Result-returning functions
) => (x > 0 ? Result.ok(x) : Result.err("negative")),
);
// Standalone functions (data-first or data-last)
Result.map(result, (x) => x + 1);
Result.map((x) => x + 1)(result); // Pipeable
```
## Handling Errors
```ts
// Transform error type
const result = fetchUser(id).mapError((e) => new AppError(`Failed to fetch user: ${e.message}`));
// Recover from specific errors
const result = fetchUser(id).match({
ok: (user) => Result.ok(user),
err: (e) => (e._tag === "NotFoundError" ? Result.ok(defaultUser) : Result.err(e)),
});
```
## Extracting Values
```ts
// Unwrap (throws on Err)
const value = result.unwrap();
const value = result.unwrap("custom error message");
// With fallback
const value = result.unwrapOr(defaultValue);
// Pattern match
const value = result.match({
ok: (v) => v,
err: (e) => fallback,
});
```
## Generator Composition
Chain multiple Results without nested callbacks or early returns:
```ts
const result = Result.gen(function* () {
const a = yield* parseNumber(inputA); // Unwraps or short-circuits
const b = yield* parseNumber(inputB);
const c = yield* divide(a, b);
return Result.ok(c);
});
// Result<number, ParseError | DivisionError>
```
Async version with `Result.await`:
```ts
const result = await Result.gen(async function* () {
const user = yield* Result.await(fetchUser(id));
const posts = yield* Result.await(fetchPosts(user.id));
return Result.ok({ user, posts });
});
```
Errors from all yielded Results are automatically collected into the final error union type.
### Normalizing Error Types
Use `mapError` on the output of `Result.gen()` to unify multiple error types into a single type:
```ts
class ParseError extends TaggedError("ParseError")<{ message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {}
class AppError extends TaggedError("AppError")<{ source: string; message: string }>() {}
const result = Result.gen(function* () {
const parsed = yield* parseInput(input); // Err: ParseError
const valid = yield* validate(parsed); // Err: ValidationError
return Result.ok(valid);
}).mapError((e): AppError => new AppError({ source: e._tag, message: e.message }));
// Result<ValidatedData, AppError> - error union normalized to single type
```
## Retry Support
```ts
const result = await Result.tryPromise(() => fetch(url), {
retry: {
times: 3,
delayMs: 100,
backoff: "exponential", // or "linear" | "constant"
},
});
```
### Conditional Retry
Retry only for specific error types using `shouldRetry`:
```ts
class NetworkError extends TaggedError("NetworkError")<{ message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {}
const result = await Result.tryPromise(
{
try: () => fetchData(url),
catch: (e) =>
e instanceof TypeError // Network failures often throw TypeError
? new NetworkError({ message: (e as Error).message })
: new ValidationError({ message: String(e) }),
},
{
retry: {
times: 3,
delayMs: 100,
backoff: "exponential",
shouldRetry: (e) => e._tag === "NetworkError", // Only retry network errors
},
},
);
```
### Async Retry Decisions
For retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the `catch` handler instead of making `shouldRetry` async:
```ts
class ApiError extends TaggedError("ApiError")<{
message: string;
rateLimited: boolean;
}>() {}
const result = await Result.tryPromise(
{
try: () => callApi(url),
catch: async (e) => {
// Fetch async state in catch handler
const retryAfter = await redis.get(`ratelimit:${userId}`);
return new ApiError({
message: (e as Error).message,
rateLimited: retryAfter !== null,
});
},
},
{
retry: {
times: 3,
delayMs: 100,
backoff: "exponential",
shouldRetry: (e) => !e.rateLimited, // Sync predicate uses enriched error
},
},
);
```
## UnhandledException
When `Result.try()` or `Result.tryPromise()` catches an exception without a custom handler, the error type is `UnhandledException`:
```ts
import { Result, UnhandledException } from "better-result";
// Automatic — error type is UnhandledException
const result = Result.try(() => JSON.parse(input));
// ^? Result<unknown, UnhandledException>
// Custom handler — you control the error type
const result = Result.try({
try: () => JSON.parse(input),
catch: (e) => new ParseError(e),
});
// ^? Result<unknown, ParseError>
// Same for async
await Result.tryPromise(() => fetch(url));
// ^? Promise<Result<Response, UnhandledException>>
```
Access the original exception via `.cause`:
```ts
if (Result.isError(result)) {
const original = result.error.cause;
if (original instanceof SyntaxError) {
// Handle JSON parse error
}
}
```
## Panic
Thrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.
```ts
import { Panic } from "better-result";
// Callback throws → Panic
Result.ok(1).map(() => {
throw new Error("bug");
}); // throws Panic
// Generator cleanup throws → Panic
Result.gen(function* () {
try {
yield* Result.err("expected failure");
} finally {
throw new Error("cleanup bug");
}
}); // throws Panic
// Catch handler throws → Panic
Result.try({
try: () => riskyOp(),
catch: () => {
throw new Error("bug in handler");
},
}); // throws Panic
```
**Why Panic?** `Err` is for recoverable domain errors. Panic is for bugs — like Rust's `panic!()`. If your `.map()` callback throws, that's not an error to handle, it's a defect to fix. Returning `Err` would collapse type safety (`Result<T, E>` becomes `Result<T, E | unknown>`).
**Panic properties:**
| Property | Type | Description |
| --------- | --------- | ----------------------------- |
| `message` | `string` | Describes where/what panicked |
| `cause` | `unknown` | The exception that was thrown |
Panic also provides `toJSON()` for error reporting services (Sentry, etc.).
## Tagged Errors
Build exhaustive error handling with discriminated unions:
```ts
import { TaggedError, matchError, matchErrorPartial } from "better-result";
// Factory API: TaggedError("Tag")<Props>()
class NotFoundError extends TaggedError("NotFoundError")<{
id: string;
message: string;
}>() {}
class ValidationError extends TaggedError("ValidationError")<{
field: string;
message: string;
}>() {}
type AppError = NotFoundError | ValidationError;
// Create errors with object args
const err = new NotFoundError({ id: "123", message: "User not found" });
// Exhaustive matching
matchError(error, {
NotFoundError: (e) => `Missing: ${e.id}`,
ValidationError: (e) => `Bad field: ${e.field}`,
});
// Partial matching with fallback
matchErrorPartial(
error,
{ NotFoundError: (e) => `Missing: ${e.id}` },
(e) => `Unknown: ${e.message}`,
);
// Type guards
TaggedError.is(value); // any tagged error
NotFoundError.is(value); // specific class
```
For errors with computed messages, add a custom constructor:
```ts
class NetworkError extends TaggedError("NetworkError")<{
url: string;
status: number;
message: string;
}>() {
constructor(args: { url: string; status: number }) {
super({ ...args, message: `Request to ${args.url} failed: ${args.status}` });
}
}
new NetworkError({ url: "/api", status: 404 });
```
## Serialization
Convert Results to plain objects for RPC, storage, or server actions:
```ts
import { Result, SerializedResult, ResultDeserializationError } from "better-result";
// Serialize to plain object
const result = Result.ok(42);
const serialized = Result.serialize(result);
// { status: "ok", value: 42 }
// Deserialize back to Result instance
const deserialized = Result.deserialize<number, never>(serialized);
// Ok(42) - can use .map(), .andThen(), etc.
// Invalid input returns ResultDeserializationError
const invalid = Result.deserialize({ foo: "bar" });
if (Result.isError(invalid) && ResultDeserializationError.is(invalid.error)) {
console.log("Bad input:", invalid.error.value);
}
// Typed boundary for Next.js server actions
async function createUser(data: FormData): Promise<SerializedResult<User, ValidationError>> {
const result = await validateAndCreate(data);
return Result.serialize(result);
}
// Client-side
const serialized = await createUser(formData);
const result = Result.deserialize<User, ValidationError>(serialized);
```
## API Reference
### Result
| Method | Description |
| -------------------------------- | --------------------------------------- |
| `Result.ok(value)` | Create success |
| `Result.err(error)` | Create error |
| `Result.try(fn)` | Wrap throwing function |
| `Result.tryPromise(fn, config?)` | Wrap async function with optional retry |
| `Result.isOk(result)` | Type guard for Ok |
| `Result.isError(result)` | Type guard for Err |
| `Result.gen(fn)` | Generator composition |
| `Result.await(promise)` | Wrap Promise<Result> for generators |
| `Result.serialize(result)` | Convert Result to plain object |
| `Result.deserialize(value)` | Rehydrate serialized Result (returns `Err<ResultDeserializationError>` on invalid input) |
| `Result.partition(results)` | Split array into [okValues, errValues] |
| `Result.flatten(result)` | Flatten nested Result |
### Instance Methods
| Method | Description |
| --------------------- | ------------------------------------- |
| `.isOk()` | Type guard, narrows to Ok |
| `.isErr()` | Type guard, narrows to Err |
| `.map(fn)` | Transform success value |
| `.mapError(fn)` | Transform error value |
| `.andThen(fn)` | Chain Result-returning function |
| `.andThenAsync(fn)` | Chain async Result-returning function |
| `.match({ ok, err })` | Pattern match |
| `.unwrap(message?)` | Extract value or throw |
| `.unwrapOr(fallback)` | Extract value or return fallback |
| `.tap(fn)` | Side effect on success |
| `.tapAsync(fn)` | Async side effect on success |
### TaggedError
| Method | Description |
| -------------------------------------- | ---------------------------------- |
| `TaggedError(tag)<Props>()` | Factory for tagged error class |
| `TaggedError.is(value)` | Type guard for any TaggedError |
| `matchError(err, handlers)` | Exhaustive pattern match by `_tag` |
| `matchErrorPartial(err, handlers, fb)` | Partial match with fallback |
| `isTaggedError(value)` | Type guard (standalone function) |
| `panic(message, cause?)` | Throw unrecoverable Panic |
| `isPanic(value)` | Type guard for Panic |
### Type Helpers
| Type | Description |
| ------------------------ | ---------------------------- |
| `InferOk<R>` | Extract Ok type from Result |
| `InferErr<R>` | Extract Err type from Result |
| `SerializedResult<T, E>` | Plain object form of Result |
| `SerializedOk<T>` | Plain object form of Ok |
| `SerializedErr<E>` | Plain object form of Err |
## Agents & AI
better-result ships with skills for AI coding agents (OpenCode, Claude Code, Codex).
### Quick Start
```sh
npx better-result init
```
Interactive setup that:
1. Installs the better-result package
2. Optionally fetches source code via [opensrc](https://github.com/vercel-labs/opensrc) for better AI context
3. Installs the adoption skill + `/adopt-better-result` command for your agent
4. Optionally launches your agent
### What the skill does
The `/adopt-better-result` command guides your AI agent through:
- Converting try/catch to Result.try/tryPromise
- Defining TaggedError classes for domain errors
- Refactoring to generator composition
- Migrating null checks to Result types
### Supported agents
| Agent | Config detected | Skill location |
| -------- | ----------------------- | -------------------------------------- |
| OpenCode | `.opencode/` | `.opencode/skill/better-result-adopt/` |
| Claude | `.claude/`, `CLAUDE.md` | `.claude/skills/better-result-adopt/` |
| Codex | `.codex/`, `AGENTS.md` | `.codex/skills/better-result-adopt/` |
### Manual usage
If you prefer not to use the interactive CLI:
```sh
# Install package
npm install better-result
# Add source for AI context (optional)
npx opensrc better-result
# Then copy skills/ directory to your agent's skill folder
```
## License
MIT

827
node_modules/better-result/bin/cli.mjs generated vendored Executable file
View File

@@ -0,0 +1,827 @@
#!/usr/bin/env node
import {
readdirSync,
existsSync,
statSync,
readFileSync,
mkdirSync,
cpSync,
writeFileSync,
} from "node:fs";
import { execSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as p from "@clack/prompts";
import color from "picocolors";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SKILLS_DIR = join(__dirname, "..", "skills");
/** Create a clickable terminal hyperlink */
function link(text, url) {
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
}
/** @typedef {"opencode" | "claude" | "codex"} Agent */
/**
* Agent configuration for skill/command paths
*/
const AGENT_CONFIG = {
opencode: {
skillDir: ".opencode/skill",
commandDir: ".opencode/command",
cli: "opencode",
configFiles: [".opencode", "opencode.json"],
},
claude: {
skillDir: ".claude/skills",
commandDir: ".claude/commands",
cli: "claude",
configFiles: [".claude", "claude.json", "CLAUDE.md"],
},
codex: {
skillDir: ".codex/skills",
commandDir: ".codex/commands",
cli: "codex",
configFiles: [".codex", "codex.json", "AGENTS.md"],
},
};
/** @typedef {"npm" | "bun" | "pnpm"} PackageManager */
// ─────────────────────────────────────────────────────────────────────────────
// Detection
// ─────────────────────────────────────────────────────────────────────────────
/** Check if a CLI binary is available */
function hasBinary(name) {
try {
execSync(`which ${name}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
/**
* Detect package manager from lockfiles
* @returns {PackageManager | null}
*/
function detectPackageManager() {
const cwd = process.cwd();
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
return "bun";
}
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
return "pnpm";
}
if (existsSync(join(cwd, "package-lock.json"))) {
return "npm";
}
return null;
}
/**
* Detect harness from repo config files
* @returns {Agent | null}
*/
function detectConfiguredAgent() {
const cwd = process.cwd();
for (const [agent, config] of Object.entries(AGENT_CONFIG)) {
for (const file of config.configFiles) {
if (existsSync(join(cwd, file))) {
return /** @type {Agent} */ (agent);
}
}
}
return null;
}
/**
* Get all agents with binary available
* @returns {Agent[]}
*/
function getAvailableAgents() {
return /** @type {Agent[]} */ (
Object.keys(AGENT_CONFIG).filter((agent) =>
hasBinary(AGENT_CONFIG[/** @type {Agent} */ (agent)].cli),
)
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Skills
// ─────────────────────────────────────────────────────────────────────────────
/** Recursively find all SKILL.md files and return skill info */
function discoverSkills() {
const skills = [];
function walk(dir, prefix = "") {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir)) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
const skillFile = join(fullPath, "SKILL.md");
if (existsSync(skillFile)) {
const id = prefix ? `${prefix}/${entry}` : entry;
skills.push({ id, path: fullPath, skillFile });
} else {
walk(fullPath, prefix ? `${prefix}/${entry}` : entry);
}
}
}
}
walk(SKILLS_DIR);
return skills;
}
/** Parse YAML frontmatter from SKILL.md */
function parseSkillMeta(skillFile) {
const content = readFileSync(skillFile, "utf-8");
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return { name: "unknown", description: "" };
const yaml = match[1];
const name = yaml.match(/name:\s*(.+)/)?.[1]?.trim() || "unknown";
const description = yaml.match(/description:\s*(.+)/)?.[1]?.trim() || "";
return { name, description };
}
// ─────────────────────────────────────────────────────────────────────────────
// Installation
// ─────────────────────────────────────────────────────────────────────────────
/** Install skill to target agent */
function installSkill(skillPath, skillName, agent) {
const cwd = process.cwd();
const config = AGENT_CONFIG[agent];
const targetSkillDir = join(cwd, config.skillDir, skillName);
if (!existsSync(skillPath)) {
return { success: false, message: `Skill source not found: ${skillPath}` };
}
mkdirSync(dirname(targetSkillDir), { recursive: true });
if (existsSync(targetSkillDir)) {
return { success: true, message: `${targetSkillDir}`, existed: true };
}
cpSync(skillPath, targetSkillDir, { recursive: true });
return { success: true, message: `${targetSkillDir}`, existed: false };
}
/** Install command wrapper for skill */
function installCommand(skillName, agent) {
const cwd = process.cwd();
const config = AGENT_CONFIG[agent];
const commandDir = join(cwd, config.commandDir);
const commandFile = join(commandDir, "adopt-better-result.md");
mkdirSync(commandDir, { recursive: true });
if (existsSync(commandFile)) {
return { success: true, message: `${commandFile}`, existed: true };
}
const commandContent = `---
description: Use ${skillName} skill for better-result migration/adoption
---
Load and use the ${skillName} skill to help with this codebase.
First, invoke the skill:
\`\`\`
skill({ name: '${skillName}' })
\`\`\`
Then follow the skill instructions.
<user-request>
$ARGUMENTS
</user-request>
`;
writeFileSync(commandFile, commandContent);
return { success: true, message: `${commandFile}`, existed: false };
}
// ─────────────────────────────────────────────────────────────────────────────
// Launch
// ─────────────────────────────────────────────────────────────────────────────
/**
* Launch agent CLI interactively
* @param {Agent} agent
*/
function launchAgent(agent) {
const config = AGENT_CONFIG[agent];
try {
execSync(config.cli, {
stdio: "inherit",
cwd: process.cwd(),
env: process.env,
});
process.exit(0);
} catch (err) {
process.exit(err.status ?? 0);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Version Detection
// ─────────────────────────────────────────────────────────────────────────────
/**
* @typedef {{ installed: false } | { installed: true; version: string; major: number }} VersionInfo
*/
/**
* Detect installed better-result version from package.json
* @returns {VersionInfo}
*/
function detectInstalledVersion() {
const cwd = process.cwd();
const pkgPath = join(cwd, "package.json");
if (!existsSync(pkgPath)) {
return { installed: false };
}
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const versionSpec = deps["better-result"];
if (!versionSpec) {
return { installed: false };
}
// Try to get actual installed version from node_modules
const installedPkgPath = join(cwd, "node_modules", "better-result", "package.json");
if (existsSync(installedPkgPath)) {
const installedPkg = JSON.parse(readFileSync(installedPkgPath, "utf-8"));
const version = installedPkg.version;
const major = parseInt(version.split(".")[0], 10);
return { installed: true, version, major };
}
// Fallback: parse version from spec (strip ^, ~, etc.)
const version = versionSpec.replace(/^[\^~>=<]+/, "");
const major = parseInt(version.split(".")[0], 10);
return { installed: true, version, major };
} catch {
return { installed: false };
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Help
// ─────────────────────────────────────────────────────────────────────────────
function printHelp() {
console.log(`
${color.bold("better-result")} - Lightweight Result type for TypeScript
${color.dim("Usage:")}
npx better-result init Interactive setup (new projects)
npx better-result migrate Detect version & guide migration
npx better-result --help Show this help
`);
}
// ─────────────────────────────────────────────────────────────────────────────
// Migrate Command
// ─────────────────────────────────────────────────────────────────────────────
async function runMigrate() {
console.log();
p.intro(color.bgCyan(color.black(" better-result migrate ")));
const versionInfo = detectInstalledVersion();
if (!versionInfo.installed) {
p.log.warn("better-result is not installed in this project");
const shouldInit = await p.confirm({
message: "Would you like to set up better-result now?",
initialValue: true,
});
if (p.isCancel(shouldInit) || !shouldInit) {
p.cancel("Migration cancelled.");
process.exit(0);
}
// Run init flow
return runInit();
}
p.log.info(`Detected version: ${color.cyan(versionInfo.version)}`);
if (versionInfo.major >= 2) {
p.log.success("Already on v2 - no migration needed!");
p.outro(color.green("You're up to date."));
process.exit(0);
}
// v1 detected - guide to v2 migration
p.log.step(`Migration available: v${versionInfo.major} → v2`);
console.log();
console.log(color.dim(" Breaking changes in v2:"));
console.log(color.dim(" • TaggedError: class-based → factory-based API"));
console.log(color.dim(" • TaggedError.match → matchError (standalone fn)"));
console.log(color.dim(" • TaggedError.matchPartial → matchErrorPartial"));
console.log(color.dim(" • New: Panic for unrecoverable errors"));
console.log();
const onCancel = () => {
p.cancel("Migration cancelled.");
process.exit(0);
};
// Detect defaults
const detectedPM = detectPackageManager();
const configuredAgent = detectConfiguredAgent();
const availableAgents = getAvailableAgents();
const detectedAgent = configuredAgent ?? (availableAgents.length > 0 ? availableAgents[0] : null);
// Build package manager options
const pmList = /** @type {PackageManager[]} */ (["npm", "bun", "pnpm"]);
const pmOptions = pmList.map((pm) => ({
value: pm,
label: pm === detectedPM ? `${pm} ${color.dim("(detected)")}` : pm,
}));
if (detectedPM) {
const idx = pmOptions.findIndex((o) => o.value === detectedPM);
if (idx > 0) {
const [detected] = pmOptions.splice(idx, 1);
pmOptions.unshift(detected);
}
}
// Build agent options
const agents = /** @type {Agent[]} */ (["opencode", "claude", "codex"]);
const agentOptions = agents.map((agent) => ({
value: agent,
label: agent === detectedAgent ? `${agent} ${color.dim("(detected)")}` : agent,
}));
if (detectedAgent) {
const idx = agentOptions.findIndex((o) => o.value === detectedAgent);
if (idx > 0) {
const [detected] = agentOptions.splice(idx, 1);
agentOptions.unshift(detected);
}
}
const responses = await p.group(
{
pm: () =>
p.select({
message: "Package manager",
options: pmOptions,
initialValue: detectedPM ?? "npm",
}),
installSkill: () =>
p.confirm({
message: "Install AI migration skill? (recommended for large codebases)",
initialValue: true,
}),
},
{ onCancel },
);
const selectedPM = /** @type {PackageManager} */ (responses.pm);
/** @type {Agent | null} */
let selectedAgent = null;
let shouldLaunch = false;
if (responses.installSkill) {
const toolResponses = await p.group(
{
agent: () =>
p.select({
message: "AI coding agent",
options: agentOptions,
initialValue: detectedAgent ?? "opencode",
}),
launch: () =>
p.confirm({
message: "Launch agent after install?",
initialValue: true,
}),
},
{ onCancel },
);
selectedAgent = /** @type {Agent} */ (toolResponses.agent);
shouldLaunch = Boolean(toolResponses.launch);
}
// Upgrade package
const s = p.spinner();
s.start("Upgrading better-result to v2");
try {
const installCmd =
selectedPM === "npm"
? "npm install better-result@latest"
: selectedPM === "bun"
? "bun add better-result@latest"
: "pnpm add better-result@latest";
execSync(installCmd, { stdio: "ignore", cwd: process.cwd() });
s.stop("Upgraded to v2");
} catch {
s.stop(color.yellow("Upgrade failed - try manually"));
}
// Install migration skill + command
if (selectedAgent) {
const skills = discoverSkills();
const migrateSkill = skills.find((s) => s.id === "migrations/v2");
if (migrateSkill) {
const meta = parseSkillMeta(migrateSkill.skillFile);
const s2 = p.spinner();
s2.start(`Installing migration skill for ${selectedAgent}`);
const skillResult = installSkill(migrateSkill.path, meta.name, selectedAgent);
installMigrateCommand(meta.name, selectedAgent);
s2.stop("Migration skill installed");
p.log.success(`Skill: ${color.dim(skillResult.message)}`);
}
}
/** Strip ANSI codes for length calculation */
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
/** Print a styled box with title and message */
function box(title, lines) {
const titlePlain = stripAnsi(title);
const maxLen = Math.max(...lines.map((l) => stripAnsi(l).length), titlePlain.length + 4);
const padding = maxLen - titlePlain.length - 2;
const top = `${color.dim("┌──")} ${title} ${color.dim("─".repeat(Math.max(0, padding)) + "┐")}`;
const bot = color.dim(`${"─".repeat(maxLen + 2)}`);
console.log(top);
for (const line of lines) {
const plainLen = stripAnsi(line).length;
console.log(`${color.dim("│")} ${line}${" ".repeat(maxLen - plainLen)} ${color.dim("│")}`);
}
console.log(bot);
}
const commandHintLines = [
`${color.white("Run")} ${color.bold(color.cyan("/migrate-better-result"))} ${color.white("to start migration.")}`,
"",
color.yellow("The AI will update TaggedError classes to v2 syntax."),
color.dim("See MIGRATION.md for manual migration steps."),
];
// Launch or show next steps
if (selectedAgent && shouldLaunch) {
if (!hasBinary(AGENT_CONFIG[selectedAgent].cli)) {
p.log.warn(`${selectedAgent} binary not found`);
box(color.bold("Next steps"), commandHintLines);
p.outro(`Install ${selectedAgent}, then run: ${color.cyan(AGENT_CONFIG[selectedAgent].cli)}`);
process.exit(0);
}
console.log();
box(color.green("Once the agent opens"), commandHintLines);
console.log();
const confirmLaunch = await p.text({
message: `Press ${color.bold(color.cyan("Enter"))} to launch ${color.bold(selectedAgent)}...`,
placeholder: "",
defaultValue: "",
});
if (p.isCancel(confirmLaunch)) {
p.cancel("Migration cancelled.");
process.exit(0);
}
launchAgent(selectedAgent);
} else if (selectedAgent) {
const manualLines = [
`${color.white("Run:")} ${color.bold(color.cyan(AGENT_CONFIG[selectedAgent].cli))}`,
"",
...commandHintLines,
];
console.log();
box(color.bold("Next steps"), manualLines);
p.outro(color.green("Ready to migrate!"));
process.exit(0);
} else {
console.log();
p.log.info(`See ${color.cyan("MIGRATION.md")} for manual migration steps.`);
p.outro(color.green("Package upgraded to v2!"));
process.exit(0);
}
}
/** Install migrate command wrapper */
function installMigrateCommand(skillName, agent) {
const cwd = process.cwd();
const config = AGENT_CONFIG[agent];
const commandDir = join(cwd, config.commandDir);
const commandFile = join(commandDir, "migrate-better-result.md");
mkdirSync(commandDir, { recursive: true });
if (existsSync(commandFile)) {
return { success: true, message: `${commandFile}`, existed: true };
}
const commandContent = `---
description: Migrate TaggedError classes from better-result v1 to v2
---
Load and use the ${skillName} skill to migrate this codebase.
First, invoke the skill:
\`\`\`
skill({ name: '${skillName}' })
\`\`\`
Then follow the skill instructions to migrate TaggedError classes.
<user-request>
$ARGUMENTS
</user-request>
`;
writeFileSync(commandFile, commandContent);
return { success: true, message: `${commandFile}`, existed: false };
}
async function main() {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
printHelp();
process.exit(0);
}
const command = args[0];
if (command === "migrate") {
return runMigrate();
}
if (command !== "init") {
printHelp();
process.exit(command ? 1 : 0);
}
return runInit();
}
// ─────────────────────────────────────────────────────────────────────────────
// Init Command
// ─────────────────────────────────────────────────────────────────────────────
async function runInit() {
// ─────────────────────────────────────────────────────────────────────────
// Interactive init flow
// ─────────────────────────────────────────────────────────────────────────
console.log();
p.intro(color.bgCyan(color.black(" better-result ")));
const skills = discoverSkills();
const adoptSkill = skills.find((s) => s.id === "adopt");
if (!adoptSkill) {
p.log.error("adopt skill not found in package");
process.exit(1);
}
const meta = parseSkillMeta(adoptSkill.skillFile);
// Detect defaults
const detectedPM = detectPackageManager();
const configuredAgent = detectConfiguredAgent();
const availableAgents = getAvailableAgents();
const detectedAgent = configuredAgent ?? (availableAgents.length > 0 ? availableAgents[0] : null);
// Build package manager options
const pmList = /** @type {PackageManager[]} */ (["npm", "bun", "pnpm"]);
const pmOptions = pmList.map((pm) => ({
value: pm,
label: pm === detectedPM ? `${pm} ${color.dim("(detected)")}` : pm,
}));
if (detectedPM) {
const idx = pmOptions.findIndex((o) => o.value === detectedPM);
if (idx > 0) {
const [detected] = pmOptions.splice(idx, 1);
pmOptions.unshift(detected);
}
}
// Build agent options
const agents = /** @type {Agent[]} */ (["opencode", "claude", "codex"]);
const agentOptions = agents.map((agent) => ({
value: agent,
label: agent === detectedAgent ? `${agent} ${color.dim("(detected)")}` : agent,
}));
if (detectedAgent) {
const idx = agentOptions.findIndex((o) => o.value === detectedAgent);
if (idx > 0) {
const [detected] = agentOptions.splice(idx, 1);
agentOptions.unshift(detected);
}
}
const onCancel = () => {
p.cancel("Setup cancelled.");
process.exit(0);
};
const responses = await p.group(
{
pm: () =>
p.select({
message: "Package manager",
options: pmOptions,
initialValue: detectedPM ?? "npm",
}),
installTools: () =>
p.confirm({
message: "Install AI agent skill + command?",
initialValue: true,
}),
},
{ onCancel },
);
const selectedPM = /** @type {PackageManager} */ (responses.pm);
// Conditional: agent selection + opensrc + launch option
/** @type {Agent | null} */
let selectedAgent = null;
let shouldLaunch = false;
let installOpensrc = false;
if (responses.installTools) {
const toolResponses = await p.group(
{
agent: () =>
p.select({
message: "AI coding agent",
options: agentOptions,
initialValue: detectedAgent ?? "opencode",
}),
opensrc: () =>
p.confirm({
message: `Add source code for better AI context? ${color.dim(`(${link("opensrc", "https://github.com/vercel-labs/opensrc")})`)}`,
initialValue: true,
}),
launch: () =>
p.confirm({
message: "Launch agent after install?",
initialValue: true,
}),
},
{ onCancel },
);
selectedAgent = /** @type {Agent} */ (toolResponses.agent);
installOpensrc = Boolean(toolResponses.opensrc);
shouldLaunch = Boolean(toolResponses.launch);
}
// Install package
const s = p.spinner();
s.start("Installing better-result package");
try {
const installCmd =
selectedPM === "npm"
? "npm install better-result"
: selectedPM === "bun"
? "bun add better-result"
: "pnpm add better-result";
execSync(installCmd, { stdio: "ignore", cwd: process.cwd() });
s.stop("Package installed");
} catch {
s.stop("Package install failed (may already be installed)");
}
// Install source code via opensrc for better AI context
if (installOpensrc) {
const s3 = p.spinner();
s3.start("Fetching source code for AI context");
try {
// --modify to auto-update .gitignore, tsconfig, AGENTS.md
execSync("npx -y opensrc better-result --modify", { stdio: "ignore", cwd: process.cwd() });
s3.stop("Source code added to opensrc/");
} catch (err) {
s3.stop(color.yellow("Source fetch failed (opensrc may not be available)"));
}
}
// Install skill + command (if selected)
if (selectedAgent) {
const s2 = p.spinner();
s2.start(`Installing skill + command for ${selectedAgent}`);
const skillResult = installSkill(adoptSkill.path, meta.name, selectedAgent);
const commandResult = installCommand(meta.name, selectedAgent);
s2.stop("Skill + command installed");
p.log.success(
`Skill: ${color.dim(skillResult.message)}${skillResult.existed ? color.dim(" (existed)") : ""}`,
);
p.log.success(
`Command: ${color.dim(commandResult.message)}${commandResult.existed ? color.dim(" (existed)") : ""}`,
);
}
/** Strip ANSI codes for length calculation */
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
/** Print a styled box with title and message */
function box(title, lines) {
const titlePlain = stripAnsi(title);
const maxLen = Math.max(...lines.map((l) => stripAnsi(l).length), titlePlain.length + 4);
const padding = maxLen - titlePlain.length - 2;
const top = `${color.dim("┌──")} ${title} ${color.dim("─".repeat(Math.max(0, padding)) + "┐")}`;
const bot = color.dim(`${"─".repeat(maxLen + 2)}`);
console.log(top);
for (const line of lines) {
const plainLen = stripAnsi(line).length;
console.log(`${color.dim("│")} ${line}${" ".repeat(maxLen - plainLen)} ${color.dim("│")}`);
}
console.log(bot);
}
const commandHintLines = [
`${color.white("Run")} ${color.bold(color.cyan("/adopt-better-result"))} ${color.white("to start adoption.")}`,
"",
color.yellow("This will analyze your codebase and suggest changes."),
color.dim("It may take a few minutes depending on project size."),
];
// Launch or show next steps
if (selectedAgent && shouldLaunch) {
if (!hasBinary(AGENT_CONFIG[selectedAgent].cli)) {
p.log.warn(`${selectedAgent} binary not found`);
box(color.bold("Next steps"), commandHintLines);
p.outro(`Install ${selectedAgent}, then run: ${color.cyan(AGENT_CONFIG[selectedAgent].cli)}`);
process.exit(0);
}
console.log();
box(color.green("Once the agent opens"), commandHintLines);
console.log();
const confirmLaunch = await p.text({
message: `Press ${color.bold(color.cyan("Enter"))} to launch ${color.bold(selectedAgent)}...`,
placeholder: "",
defaultValue: "",
});
if (p.isCancel(confirmLaunch)) {
p.cancel("Setup cancelled.");
process.exit(0);
}
launchAgent(selectedAgent);
} else if (selectedAgent) {
const manualLines = [
`${color.white("Run:")} ${color.bold(color.cyan(AGENT_CONFIG[selectedAgent].cli))}`,
"",
...commandHintLines,
];
console.log();
box(color.bold("Next steps"), manualLines);
p.outro(color.green("Done!"));
process.exit(0);
} else {
p.outro(color.green("Done!"));
process.exit(0);
}
}
main().catch((err) => {
p.log.error(err.message);
process.exit(1);
});

749
node_modules/better-result/dist/index.d.mts generated vendored Normal file
View File

@@ -0,0 +1,749 @@
//#region src/error.d.ts
/** Any tagged error (for generic constraints) */
type AnyTaggedError = Error & {
readonly _tag: string;
};
/**
* Factory for tagged error classes.
*
* @example
* class NotFoundError extends TaggedError("NotFoundError")<{
* id: string;
* message: string;
* }>() {}
*
* const err = new NotFoundError({ id: "123", message: "Not found: 123" });
* err._tag // "NotFoundError"
* err.id // "123"
* err.message // "Not found: 123"
*
* // Check if any tagged error
* TaggedError.is(err) // true
*/
declare const TaggedError: {
<Tag extends string>(tag: Tag): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props>;
/** Type guard for any TaggedError instance */
is(value: unknown): value is AnyTaggedError;
};
/** Instance type produced by TaggedError factory */
type TaggedErrorInstance<Tag extends string, Props> = Error & {
readonly _tag: Tag;
toJSON(): object;
} & Readonly<Props>;
/** Class type produced by TaggedError factory */
type TaggedErrorClass<Tag extends string, Props> = {
new (...args: keyof Props extends never ? [args?: {}] : [args: Props]): TaggedErrorInstance<Tag, Props>;
/** Type guard for this error class */
is(value: unknown): value is TaggedErrorInstance<Tag, Props>;
};
/** Handler map for exhaustive matching */
type MatchHandlers<E extends AnyTaggedError, R> = { [K in E["_tag"]]: (err: Extract<E, {
_tag: K;
}>) => R };
/** Partial handler map for non-exhaustive matching */
type PartialMatchHandlers<E extends AnyTaggedError, R> = Partial<MatchHandlers<E, R>>;
/** Extract handled tags from a handlers object */
type HandledTags<E extends AnyTaggedError, H> = Extract<keyof H, E["_tag"]>;
/**
* Exhaustive pattern match on tagged error union.
*
* @example
* // Data-first
* matchError(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* });
*
* // Data-last (pipeable)
* pipe(err, matchError({
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* }));
*/
declare const matchError: {
<E extends AnyTaggedError, R>(err: E, handlers: MatchHandlers<E, R>): R;
<E extends AnyTaggedError, R>(handlers: MatchHandlers<E, R>): (err: E) => R;
};
/**
* Partial pattern match with fallback for unhandled tags.
*
* @example
* matchErrorPartial(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* }, (e) => `Unknown: ${e.message}`);
*/
declare const matchErrorPartial: {
<E extends AnyTaggedError, R, const H extends PartialMatchHandlers<E, R>>(err: E, handlers: H, fallback: (e: Exclude<E, {
_tag: NoInfer<HandledTags<E, H>>;
}>) => R): R;
<E extends AnyTaggedError, R, const H extends PartialMatchHandlers<E, R> = PartialMatchHandlers<E, R>>(handlers: H, fallback: (e: Exclude<E, {
_tag: NoInfer<HandledTags<E, H>>;
}>) => R): (err: E) => R;
};
/**
* Type guard for tagged error instances.
*
* @example
* if (isTaggedError(value)) { value._tag }
*/
declare const isTaggedError: (value: unknown) => value is AnyTaggedError;
declare const UnhandledException_base: TaggedErrorClass<"UnhandledException", {
message: string;
cause: unknown;
}>;
/**
* Wraps exceptions caught by Result.try/tryPromise.
* Custom constructor derives message from cause.
*/
declare class UnhandledException extends UnhandledException_base {
constructor(args: {
cause: unknown;
});
}
declare const Panic_base: TaggedErrorClass<"Panic", {
message: string;
cause?: unknown;
}>;
/**
* Unrecoverable error — user code threw inside Result operations.
*
* @example
* // Panic in generator cleanup:
* Result.gen(function* () {
* try {
* yield* Result.err("expected error");
* } finally {
* throw new Error("cleanup failed"); // Panic!
* }
* });
*
* // Panic in combinator:
* Result.ok(1).map(() => { throw new Error("oops"); }); // Panic!
*/
declare class Panic extends Panic_base {}
declare const ResultDeserializationError_base: TaggedErrorClass<"ResultDeserializationError", {
message: string;
value: unknown;
}>;
/**
* Returned when Result.deserialize receives invalid input.
*
* @example
* const result = Result.deserialize(invalidData);
* if (Result.isError(result) && ResultDeserializationError.is(result.error)) {
* console.log("Invalid input:", result.error.value);
* }
*/
declare class ResultDeserializationError extends ResultDeserializationError_base {
constructor(args: {
value: unknown;
});
}
/**
* Type guard for Panic instances.
*
* @example
* if (isPanic(value)) { value.cause }
*/
declare const isPanic: (value: unknown) => value is Panic;
/**
* Throw an unrecoverable Panic.
*
* @example
* panic("something went wrong", cause);
*/
declare const panic: (message: string, cause?: unknown) => never;
//#endregion
//#region src/result.d.ts
/**
* Successful result variant.
*
* @template A Success value type.
* @template E Error type (phantom - for type unification).
*
* @example
* const result = new Ok(42);
* result.value // 42
* result.status // "ok"
*/
declare class Ok<A, E = never> {
readonly value: A;
readonly status: "ok";
constructor(value: A);
/** Returns true, narrowing Result to Ok. */
isOk(): this is Ok<A, E>;
/** Returns false, narrowing Result to Err. */
isErr(): this is Err<A, E>;
/**
* Transforms success value.
*
* @template B Transformed type.
* @param fn Transformation function.
* @returns Ok with transformed value.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).map(x => x * 2) // Ok(4)
*/
map<B>(fn: (a: A) => B): Ok<B, E>;
/**
* No-op on Ok, returns self with new phantom error type.
*
* @template E2 New error type.
* @param _fn Ignored.
* @returns Self with updated phantom E type.
*/
mapError<E2>(_fn: (e: never) => E2): Ok<A, E2>;
/**
* Chains Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Function returning Result.
* @returns Result from fn.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).andThen(x => x > 0 ? ok(x) : err("negative")) // Ok(2)
*/
andThen<B, E2>(fn: (a: A) => Result<B, E2>): Result<B, E | E2>;
/**
* Chains async Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Async function returning Result.
* @returns Promise of Result from fn.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(1).andThenAsync(async x => ok(await fetchData(x)))
*/
andThenAsync<B, E2>(fn: (a: A) => Promise<Result<B, E2>>): Promise<Result<B, E | E2>>;
/**
* Pattern matches on Result.
*
* @template T Return type.
* @param handlers Ok and err handlers.
* @returns Result of ok handler.
* @throws {Panic} If handler throws.
*
* @example
* ok(2).match({ ok: x => x * 2, err: () => 0 }) // 4
*/
match<T>(handlers: {
ok: (a: A) => T;
err: (e: never) => T;
}): T;
/**
* Extracts value.
*
* @param _message Ignored.
* @returns The value.
*
* @example
* ok(42).unwrap() // 42
*/
unwrap(_message?: string): A;
/**
* Returns value, ignoring fallback.
*
* @template B Fallback type.
* @param _fallback Ignored.
* @returns The value.
*
* @example
* ok(42).unwrapOr(0) // 42
*/
unwrapOr<B>(_fallback: B): A;
/**
* Runs side effect, returns self.
*
* @param fn Side effect function.
* @returns Self.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).tap(console.log).map(x => x * 2) // logs 2, returns Ok(4)
*/
tap(fn: (a: A) => void): Ok<A, E>;
/**
* Runs async side effect, returns self.
*
* @param fn Async side effect function.
* @returns Promise of self.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(2).tapAsync(async x => await log(x))
*/
tapAsync(fn: (a: A) => Promise<void>): Promise<Ok<A, E>>;
/**
* Makes Ok yieldable in Result.gen blocks.
* Immediately returns the value without yielding.
* Yield type Err<never, E> matches Err's for proper union inference.
*/
[Symbol.iterator](): Generator<Err<never, E>, A, unknown>;
}
/**
* Error result variant.
*
* @template T Success type (phantom - for type unification with Ok).
* @template E Error value type.
*
* @example
* const result = new Err("failed");
* result.error // "failed"
* result.status // "error"
*/
declare class Err<T, E> {
readonly error: E;
readonly status: "error";
constructor(error: E);
/** Returns false, narrowing Result to Ok. */
isOk(): this is Ok<never, E>;
/** Returns true, narrowing Result to Err. */
isErr(): this is Err<T, E>;
/**
* No-op on Err, returns self with new phantom T.
*
* @template U New phantom success type.
* @param _fn Ignored.
* @returns Self.
*/
map<U>(_fn: (a: never) => U): Err<U, E>;
/**
* Transforms error value.
*
* @template E2 Transformed error type.
* @param fn Transformation function.
* @returns Err with transformed error.
* @throws {Panic} If fn throws.
*
* @example
* err("fail").mapError(e => new Error(e)) // Err(Error("fail"))
*/
mapError<E2>(fn: (e: E) => E2): Err<T, E2>;
/**
* No-op on Err, returns self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Self.
*/
andThen<U, E2>(_fn: (a: never) => Result<U, E2>): Err<U, E | E2>;
/**
* No-op on Err, returns Promise of self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Promise of self.
*/
andThenAsync<U, E2>(_fn: (a: never) => Promise<Result<U, E2>>): Promise<Err<U, E | E2>>;
/**
* Pattern matches on Result.
*
* @template R Return type.
* @param handlers Ok and err handlers.
* @returns Result of err handler.
* @throws {Panic} If handler throws.
*
* @example
* err("fail").match({ ok: x => x, err: e => e.length }) // 4
*/
match<R>(handlers: {
ok: (a: never) => R;
err: (e: E) => R;
}): R;
/**
* Throws error with optional message.
*
* @param message Error message.
* @throws Always throws.
*
* @example
* err("fail").unwrap() // throws Error
* err("fail").unwrap("custom") // throws Error("custom")
*/
unwrap(message?: string): never;
/**
* Returns fallback value.
*
* @template U Fallback type.
* @param fallback Fallback value.
* @returns Fallback.
*
* @example
* err("fail").unwrapOr(42) // 42
*/
unwrapOr<U>(fallback: U): T | U;
/**
* No-op on Err, returns self.
*
* @param _fn Ignored.
* @returns Self.
*/
tap(_fn: (a: never) => void): Err<T, E>;
/**
* No-op on Err, returns Promise of self.
*
* @param _fn Ignored.
* @returns Promise of self.
*/
tapAsync(_fn: (a: never) => Promise<void>): Promise<Err<T, E>>;
/**
* Makes Err yieldable in Result.gen blocks.
* Yields Err<never, E> for proper union inference across multiple yields.
*/
[Symbol.iterator](): Generator<Err<never, E>, never, unknown>;
}
/**
* Discriminated union representing operation success or failure.
*
* Both Ok and Err carry phantom types for the "other" variant:
* - Ok<T, E>: T is value, E is phantom error type
* - Err<T, E>: T is phantom success type, E is error
*
* This symmetric structure enables proper type inference in generator-based composition.
*
* @template T Success value type.
* @template E Error value type.
*
* @example
* type ParseResult = Result<number, ParseError>;
*/
type Result<T, E> = Ok<T, E> | Err<T, E>;
/**
* Extracts error type E from yield union in Result.gen.
* Yields are always Err<never, E>, so we match on that pattern.
* Distributive conditional: InferYieldErr<Err<never, A> | Err<never, B>> = A | B
*/
type InferYieldErr<Y> = Y extends Err<never, infer E> ? E : never;
/**
* Infer the Ok value type from a Result.
* Distributive: InferOk<Ok<A, X> | Ok<B, Y>> = A | B
*/
type InferOk<R> = R extends Ok<infer T, unknown> ? T : never;
/**
* Infer the Err value type from a Result.
* Distributive: InferErr<Err<X, A> | Err<Y, B>> = A | B
*/
type InferErr<R> = R extends Err<unknown, infer E> ? E : never;
/**
* Constraint for any union of Ok/Err types.
* Used in Result.gen to accept flexible return types from generators.
*/
type AnyResult = Ok<unknown, unknown> | Err<unknown, unknown>;
declare function ok(): Ok<void, never>;
declare function ok<A, E = never>(value: A): Ok<A, E>;
type RetryConfig<E = unknown> = {
retry?: {
times: number;
delayMs: number;
backoff: "linear" | "constant" | "exponential";
/** Predicate to determine if an error should trigger a retry. Defaults to always retry. */
shouldRetry?: (error: E) => boolean;
};
};
declare function resultAwait<T, E>(promise: Promise<Result<T, E>>): AsyncGenerator<Err<never, E>, T, unknown>;
/** Shape of a serialized Ok over RPC. */
interface SerializedOk<T> {
status: "ok";
value: T;
}
/** Shape of a serialized Err over RPC. */
interface SerializedErr<E> {
status: "error";
error: E;
}
/** Shape of a serialized Result over RPC. */
type SerializedResult<T, E> = SerializedOk<T> | SerializedErr<E>;
/**
* Utilities for creating and handling Result types.
*
* @example
* const result = Result.try(() => JSON.parse(str));
* const value = result.map(x => x.id).unwrapOr("default");
*/
declare const Result: {
/**
* Creates successful result.
*
* @example
* Result.ok(42) // Ok<number, never>
* Result.ok() // Ok<void, never> - for side-effectful operations
*/
readonly ok: typeof ok;
/**
* Type guard for Ok.
*
* @example
* if (Result.isOk(result)) { result.value }
*/
readonly isOk: <A, E>(result: Result<A, E>) => result is Ok<A, E>;
/**
* Creates error result.
*
* @example
* Result.err("failed") // Err("failed")
*/
readonly err: <T = never, E = unknown>(error: E) => Err<T, E>;
/**
* Type guard for Err.
*
* @example
* if (Result.isError(result)) { result.error }
*/
readonly isError: <T, E>(result: Result<T, E>) => result is Err<T, E>;
/**
* Executes sync function, wraps result/error in Result.
*
* @example
* Result.try(() => JSON.parse(str))
* Result.try({ try: () => parse(x), catch: e => new ParseError(e) })
*/
readonly try: {
<A>(thunk: () => Awaited<A>, config?: {
retry?: {
times: number;
};
}): Result<A, UnhandledException>;
<A, E>(options: {
try: () => Awaited<A>;
catch: (cause: unknown) => Awaited<E>;
}, config?: {
retry?: {
times: number;
};
}): Result<A, E>;
};
/**
* Executes async function, wraps result/error in Result with retry support.
*
* @example
* // Basic retry
* await Result.tryPromise(() => fetch(url), {
* retry: { times: 3, delayMs: 100, backoff: "exponential" }
* })
*
* @example
* // Retry only for specific error types (user-defined TaggedError classes)
* await Result.tryPromise({
* try: () => fetch(url),
* catch: e => e instanceof TypeError ? new RetryableError(e) : new FatalError(e)
* }, {
* retry: {
* times: 3,
* delayMs: 100,
* backoff: "exponential",
* shouldRetry: e => e._tag === "RetryableError"
* }
* })
*
* @example
* // Async retry decisions: enrich error in catch handler
* await Result.tryPromise({
* try: () => callApi(url),
* catch: async (e) => {
* const limited = await redis.get(`ratelimit:${userId}`);
* return new ApiError({ cause: e, rateLimited: !!limited });
* }
* }, {
* retry: { times: 3, delayMs: 100, backoff: "exponential", shouldRetry: e => !e.rateLimited }
* })
*/
readonly tryPromise: {
<A>(thunk: () => Promise<A>, config?: RetryConfig<UnhandledException>): Promise<Result<A, UnhandledException>>;
<A, E>(options: {
try: () => Promise<A>;
catch: (cause: unknown) => E | Promise<E>;
}, config?: RetryConfig<E>): Promise<Result<A, E>>;
};
/**
* Transforms success value, passes error through.
*
* @example
* Result.map(ok(2), x => x * 2) // Ok(4)
* Result.map(x => x * 2)(ok(2)) // Ok(4)
*/
readonly map: {
<A, B, E>(result: Result<A, E>, fn: (a: A) => B): Result<B, E>;
<A, B>(fn: (a: A) => B): <E>(result: Result<A, E>) => Result<B, E>;
};
/**
* Transforms error value, passes success through.
*
* @example
* Result.mapError(err("fail"), e => new Error(e)) // Err(Error("fail"))
*/
readonly mapError: {
<A, E, E2>(result: Result<A, E>, fn: (e: E) => E2): Result<A, E2>;
<E, E2>(fn: (e: E) => E2): <A>(result: Result<A, E>) => Result<A, E2>;
};
/**
* Chains Result-returning function on success.
*
* @example
* Result.andThen(ok(2), x => x > 0 ? ok(x) : err("neg")) // Ok(2)
*/
readonly andThen: {
<A, B, E, E2>(result: Result<A, E>, fn: (a: A) => Result<B, E2>): Result<B, E | E2>;
<A, B, E2>(fn: (a: A) => Result<B, E2>): <E>(result: Result<A, E>) => Result<B, E | E2>;
};
/**
* Chains async Result-returning function on success.
*
* @example
* await Result.andThenAsync(ok(1), async x => ok(await fetch(x)))
*/
readonly andThenAsync: {
<A, B, E, E2>(result: Result<A, E>, fn: (a: A) => Promise<Result<B, E2>>): Promise<Result<B, E | E2>>;
<A, B, E2>(fn: (a: A) => Promise<Result<B, E2>>): <E>(result: Result<A, E>) => Promise<Result<B, E | E2>>;
};
/**
* Pattern matches on Result.
*
* @example
* Result.match(ok(2), { ok: x => x * 2, err: () => 0 }) // 4
*/
readonly match: {
<A, E, T>(result: Result<A, E>, handlers: {
ok: (a: A) => T;
err: (e: E) => T;
}): T;
<A, E, T>(handlers: {
ok: (a: A) => T;
err: (e: E) => T;
}): (result: Result<A, E>) => T;
};
/**
* Runs side effect on success value, returns original result.
*
* @example
* Result.tap(ok(2), console.log) // logs 2, returns Ok(2)
*/
readonly tap: {
<A, E>(result: Result<A, E>, fn: (a: A) => void): Result<A, E>;
<A>(fn: (a: A) => void): <E>(result: Result<A, E>) => Result<A, E>;
};
/**
* Runs async side effect on success value, returns original result.
*
* @example
* await Result.tapAsync(ok(2), async x => await log(x))
*/
readonly tapAsync: {
<A, E>(result: Result<A, E>, fn: (a: A) => Promise<void>): Promise<Result<A, E>>;
<A>(fn: (a: A) => Promise<void>): <E>(result: Result<A, E>) => Promise<Result<A, E>>;
};
/**
* Extracts value or throws.
*
* @example
* Result.unwrap(ok(42)) // 42
* Result.unwrap(err("fail")) // throws Error
*/
readonly unwrap: <A, E>(result: Result<A, E>, message?: string) => A;
/**
* Extracts value or returns fallback.
*
* @example
* Result.unwrapOr(ok(42), 0) // 42
* Result.unwrapOr(err("fail"), 0) // 0
*/
readonly unwrapOr: {
<A, E, B>(result: Result<A, E>, fallback: B): A | B;
<B>(fallback: B): <A, E>(result: Result<A, E>) => A | B;
};
/**
* Generator-based composition for Result types.
* Errors from yielded Results form a union; use mapError to normalize.
*
* @example
* const result = Result.gen(function* () {
* const a = yield* getA(); // Err: ErrorA
* const b = yield* getB(a); // Err: ErrorB
* return Result.ok({ a, b });
* });
* // Result<{a, b}, ErrorA | ErrorB>
*
* @example
* // Normalize error types with mapError
* const result = Result.gen(function* () {
* const a = yield* getA();
* const b = yield* getB(a);
* return Result.ok({ a, b });
* }).mapError(e => new UnifiedError(e._tag, e.message));
* // Result<{a, b}, UnifiedError>
*
* @example
* // Async with Result.await
* const result = await Result.gen(async function* () {
* const a = yield* Result.await(fetchA());
* const b = yield* Result.await(fetchB(a));
* return Result.ok({ a, b });
* });
*/
readonly gen: {
<Yield extends Err<never, unknown>, R extends AnyResult>(body: () => Generator<Yield, R, unknown>): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends Err<never, unknown>, R extends AnyResult, This>(body: (this: This) => Generator<Yield, R, unknown>, thisArg: This): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends Err<never, unknown>, R extends AnyResult>(body: () => AsyncGenerator<Yield, R, unknown>): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
<Yield extends Err<never, unknown>, R extends AnyResult, This>(body: (this: This) => AsyncGenerator<Yield, R, unknown>, thisArg: This): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
};
/**
* Wraps Promise<Result> to be yieldable in async Result.gen blocks.
*
* @example
* yield* Result.await(fetchUser(id))
*/
readonly await: typeof resultAwait;
/**
* Converts a Result to a plain object for serialization (e.g., RPC, server actions).
*
* @example
* const serialized = Result.serialize(ok(42)); // { status: "ok", value: 42 }
*/
readonly serialize: <T, E>(result: Result<T, E>) => SerializedResult<T, E>;
/**
* Rehydrates serialized Result from RPC back into Ok/Err instances.
* Returns `Err<ResultDeserializationError>` if the input is not a valid serialized Result.
*
* @example
* // Valid serialized Result
* const result = Result.deserialize<User, AppError>(rpcResponse);
* if (Result.isOk(result)) {
* console.log(result.value); // User
* }
*
* // Invalid input returns ResultDeserializationError
* const invalid = Result.deserialize({ foo: "bar" });
* if (Result.isError(invalid) && ResultDeserializationError.is(invalid.error)) {
* console.log("Bad input:", invalid.error.value);
* }
*/
readonly deserialize: <T, E>(value: unknown) => Result<T, E | ResultDeserializationError>;
/**
* @deprecated Use `Result.deserialize` instead. Will be removed in 3.0.
*/
readonly hydrate: <T, E>(value: unknown) => Result<T, E | ResultDeserializationError>;
/**
* Splits array of Results into tuple of [okValues, errorValues].
*
* @example
* partition([ok(1), err("a"), ok(2)]) // [[1, 2], ["a"]]
*/
readonly partition: <T, E>(results: readonly Result<T, E>[]) => [T[], E[]];
/**
* Flattens nested Result into single Result.
*
* @example
* const nested = Result.ok(Result.ok(42));
* Result.flatten(nested) // Ok(42)
*/
readonly flatten: <T, E, E2>(result: Result<Result<T, E>, E2>) => Result<T, E | E2>;
};
//#endregion
export { Err, type InferErr, type InferOk, Ok, Panic, Result, ResultDeserializationError, type SerializedErr, type SerializedOk, type SerializedResult, TaggedError, type TaggedErrorClass, type TaggedErrorInstance, UnhandledException, isPanic, isTaggedError, matchError, matchErrorPartial, panic };
//# sourceMappingURL=index.d.mts.map

1
node_modules/better-result/dist/index.d.mts.map generated vendored Normal file

File diff suppressed because one or more lines are too long

737
node_modules/better-result/dist/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,737 @@
//#region src/dual.ts
/**
* Creates data-first/data-last dual function.
*
* @template DataLast Curried (data-last) signature.
* @template DataFirst Uncurried (data-first) signature.
* @param arity Number of args for data-first form.
* @param body Implementation function.
* @returns Function supporting both calling conventions.
*
* @example
* const add: {
* (a: number, b: number): number;
* (b: number): (a: number) => number;
* } = dual(2, (a: number, b: number) => a + b);
*
* add(1, 2); // 3 (data-first)
* add(2)(1); // 3 (data-last)
*/
function dual(arity, body) {
if (arity === 2) return ((...args) => {
if (args.length >= 2) return body(args[0], args[1]);
return (self) => body(self, args[0]);
});
if (arity === 3) return ((...args) => {
if (args.length >= 3) return body(args[0], args[1], args[2]);
return (self) => body(self, args[0], args[1]);
});
if (arity === 4) return ((...args) => {
if (args.length >= 4) return body(args[0], args[1], args[2], args[3]);
return (self) => body(self, args[0], args[1], args[2]);
});
return ((...args) => {
if (args.length >= arity) return body(...args);
return (self) => body(self, ...args);
});
}
//#endregion
//#region src/error.ts
/** Serialize cause for JSON output */
const serializeCause = (cause) => {
if (cause instanceof Error) return {
name: cause.name,
message: cause.message,
stack: cause.stack
};
return cause;
};
/** Type guard for any tagged error */
const isAnyTaggedError = (value) => {
return value instanceof Error && "_tag" in value && typeof value._tag === "string";
};
/**
* Factory for tagged error classes.
*
* @example
* class NotFoundError extends TaggedError("NotFoundError")<{
* id: string;
* message: string;
* }>() {}
*
* const err = new NotFoundError({ id: "123", message: "Not found: 123" });
* err._tag // "NotFoundError"
* err.id // "123"
* err.message // "Not found: 123"
*
* // Check if any tagged error
* TaggedError.is(err) // true
*/
const TaggedError = Object.assign((tag) => () => {
class Base extends Error {
_tag = tag;
/** Type guard for this error class */
static is(value) {
return value instanceof Base;
}
constructor(args) {
const message = args && "message" in args && typeof args.message === "string" ? args.message : void 0;
const cause = args && "cause" in args ? args.cause : void 0;
super(message, cause !== void 0 ? { cause } : void 0);
if (args) Object.assign(this, args);
Object.setPrototypeOf(this, new.target.prototype);
this.name = tag;
if (cause instanceof Error && cause.stack) {
const indented = cause.stack.replace(/\n/g, "\n ");
this.stack = `${this.stack}\nCaused by: ${indented}`;
}
}
toJSON() {
return {
...this,
_tag: this._tag,
name: this.name,
message: this.message,
cause: serializeCause(this.cause),
stack: this.stack
};
}
}
return Base;
}, { is: isAnyTaggedError });
/**
* Exhaustive pattern match on tagged error union.
*
* @example
* // Data-first
* matchError(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* });
*
* // Data-last (pipeable)
* pipe(err, matchError({
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* }));
*/
const matchError = dual(2, (err$1, handlers) => {
const handler = handlers[err$1._tag];
return handler(err$1);
});
/**
* Partial pattern match with fallback for unhandled tags.
*
* @example
* matchErrorPartial(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* }, (e) => `Unknown: ${e.message}`);
*/
const matchErrorPartial = dual(3, (err$1, handlers, fallback) => {
const handler = handlers[err$1._tag];
if (typeof handler === "function") return handler(err$1);
return fallback(err$1);
});
/**
* Type guard for tagged error instances.
*
* @example
* if (isTaggedError(value)) { value._tag }
*/
const isTaggedError = isAnyTaggedError;
/**
* Wraps exceptions caught by Result.try/tryPromise.
* Custom constructor derives message from cause.
*/
var UnhandledException = class extends TaggedError("UnhandledException")() {
constructor(args) {
const message = args.cause instanceof Error ? `Unhandled exception: ${args.cause.message}` : `Unhandled exception: ${String(args.cause)}`;
super({
message,
cause: args.cause
});
}
};
/**
* Unrecoverable error — user code threw inside Result operations.
*
* @example
* // Panic in generator cleanup:
* Result.gen(function* () {
* try {
* yield* Result.err("expected error");
* } finally {
* throw new Error("cleanup failed"); // Panic!
* }
* });
*
* // Panic in combinator:
* Result.ok(1).map(() => { throw new Error("oops"); }); // Panic!
*/
var Panic = class extends TaggedError("Panic")() {};
/**
* Returned when Result.deserialize receives invalid input.
*
* @example
* const result = Result.deserialize(invalidData);
* if (Result.isError(result) && ResultDeserializationError.is(result.error)) {
* console.log("Invalid input:", result.error.value);
* }
*/
var ResultDeserializationError = class extends TaggedError("ResultDeserializationError")() {
constructor(args) {
super({
message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
value: args.value
});
}
};
/**
* Type guard for Panic instances.
*
* @example
* if (isPanic(value)) { value.cause }
*/
const isPanic = (value) => {
return value instanceof Panic;
};
/**
* Throw an unrecoverable Panic.
*
* @example
* panic("something went wrong", cause);
*/
const panic = (message, cause) => {
throw new Panic({
message,
cause
});
};
//#endregion
//#region src/result.ts
/** Executes fn, panics if it throws. */
const tryOrPanic = (fn, message) => {
try {
return fn();
} catch (cause) {
throw panic(message, cause);
}
};
/** Async version of tryOrPanic. */
const tryOrPanicAsync = async (fn, message) => {
try {
return await fn();
} catch (cause) {
throw panic(message, cause);
}
};
/**
* Successful result variant.
*
* @template A Success value type.
* @template E Error type (phantom - for type unification).
*
* @example
* const result = new Ok(42);
* result.value // 42
* result.status // "ok"
*/
var Ok = class Ok {
status = "ok";
constructor(value) {
this.value = value;
}
/** Returns true, narrowing Result to Ok. */
isOk() {
return true;
}
/** Returns false, narrowing Result to Err. */
isErr() {
return false;
}
/**
* Transforms success value.
*
* @template B Transformed type.
* @param fn Transformation function.
* @returns Ok with transformed value.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).map(x => x * 2) // Ok(4)
*/
map(fn) {
return tryOrPanic(() => new Ok(fn(this.value)), "map callback threw");
}
/**
* No-op on Ok, returns self with new phantom error type.
*
* @template E2 New error type.
* @param _fn Ignored.
* @returns Self with updated phantom E type.
*/
mapError(_fn) {
return this;
}
/**
* Chains Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Function returning Result.
* @returns Result from fn.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).andThen(x => x > 0 ? ok(x) : err("negative")) // Ok(2)
*/
andThen(fn) {
return tryOrPanic(() => fn(this.value), "andThen callback threw");
}
/**
* Chains async Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Async function returning Result.
* @returns Promise of Result from fn.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(1).andThenAsync(async x => ok(await fetchData(x)))
*/
andThenAsync(fn) {
return tryOrPanicAsync(() => fn(this.value), "andThenAsync callback threw");
}
/**
* Pattern matches on Result.
*
* @template T Return type.
* @param handlers Ok and err handlers.
* @returns Result of ok handler.
* @throws {Panic} If handler throws.
*
* @example
* ok(2).match({ ok: x => x * 2, err: () => 0 }) // 4
*/
match(handlers) {
return tryOrPanic(() => handlers.ok(this.value), "match ok handler threw");
}
/**
* Extracts value.
*
* @param _message Ignored.
* @returns The value.
*
* @example
* ok(42).unwrap() // 42
*/
unwrap(_message) {
return this.value;
}
/**
* Returns value, ignoring fallback.
*
* @template B Fallback type.
* @param _fallback Ignored.
* @returns The value.
*
* @example
* ok(42).unwrapOr(0) // 42
*/
unwrapOr(_fallback) {
return this.value;
}
/**
* Runs side effect, returns self.
*
* @param fn Side effect function.
* @returns Self.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).tap(console.log).map(x => x * 2) // logs 2, returns Ok(4)
*/
tap(fn) {
return tryOrPanic(() => {
fn(this.value);
return this;
}, "tap callback threw");
}
/**
* Runs async side effect, returns self.
*
* @param fn Async side effect function.
* @returns Promise of self.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(2).tapAsync(async x => await log(x))
*/
tapAsync(fn) {
return tryOrPanicAsync(async () => {
await fn(this.value);
return this;
}, "tapAsync callback threw");
}
/**
* Makes Ok yieldable in Result.gen blocks.
* Immediately returns the value without yielding.
* Yield type Err<never, E> matches Err's for proper union inference.
*/
*[Symbol.iterator]() {
return this.value;
}
};
/**
* Error result variant.
*
* @template T Success type (phantom - for type unification with Ok).
* @template E Error value type.
*
* @example
* const result = new Err("failed");
* result.error // "failed"
* result.status // "error"
*/
var Err = class Err {
status = "error";
constructor(error) {
this.error = error;
}
/** Returns false, narrowing Result to Ok. */
isOk() {
return false;
}
/** Returns true, narrowing Result to Err. */
isErr() {
return true;
}
/**
* No-op on Err, returns self with new phantom T.
*
* @template U New phantom success type.
* @param _fn Ignored.
* @returns Self.
*/
map(_fn) {
return this;
}
/**
* Transforms error value.
*
* @template E2 Transformed error type.
* @param fn Transformation function.
* @returns Err with transformed error.
* @throws {Panic} If fn throws.
*
* @example
* err("fail").mapError(e => new Error(e)) // Err(Error("fail"))
*/
mapError(fn) {
return tryOrPanic(() => new Err(fn(this.error)), "mapError callback threw");
}
/**
* No-op on Err, returns self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Self.
*/
andThen(_fn) {
return this;
}
/**
* No-op on Err, returns Promise of self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Promise of self.
*/
andThenAsync(_fn) {
return Promise.resolve(this);
}
/**
* Pattern matches on Result.
*
* @template R Return type.
* @param handlers Ok and err handlers.
* @returns Result of err handler.
* @throws {Panic} If handler throws.
*
* @example
* err("fail").match({ ok: x => x, err: e => e.length }) // 4
*/
match(handlers) {
return tryOrPanic(() => handlers.err(this.error), "match err handler threw");
}
/**
* Throws error with optional message.
*
* @param message Error message.
* @throws Always throws.
*
* @example
* err("fail").unwrap() // throws Error
* err("fail").unwrap("custom") // throws Error("custom")
*/
unwrap(message) {
return panic(message ?? `Unwrap called on Err: ${String(this.error)}`, this.error);
}
/**
* Returns fallback value.
*
* @template U Fallback type.
* @param fallback Fallback value.
* @returns Fallback.
*
* @example
* err("fail").unwrapOr(42) // 42
*/
unwrapOr(fallback) {
return fallback;
}
/**
* No-op on Err, returns self.
*
* @param _fn Ignored.
* @returns Self.
*/
tap(_fn) {
return this;
}
/**
* No-op on Err, returns Promise of self.
*
* @param _fn Ignored.
* @returns Promise of self.
*/
tapAsync(_fn) {
return Promise.resolve(this);
}
/**
* Makes Err yieldable in Result.gen blocks.
* Yields Err<never, E> for proper union inference across multiple yields.
*/
*[Symbol.iterator]() {
yield this;
return panic("Unreachable: Err yielded in Result.gen but generator continued", this.error);
}
};
function ok(value) {
return new Ok(value);
}
const isOk = (result) => {
return result.status === "ok";
};
const err = (error) => new Err(error);
const isError = (result) => {
return result.status === "error";
};
const tryFn = (options, config) => {
const execute = () => {
if (typeof options === "function") try {
return ok(options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
try {
return ok(options.try());
} catch (originalCause) {
try {
return err(options.catch(originalCause));
} catch (catchHandlerError) {
throw panic("Result.try catch handler threw", catchHandlerError);
}
}
};
const times = config?.retry?.times ?? 0;
let result = execute();
for (let retry = 0; retry < times && result.status === "error"; retry++) result = execute();
return result;
};
const tryPromise = async (options, config) => {
const execute = async () => {
if (typeof options === "function") try {
return ok(await options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
try {
return ok(await options.try());
} catch (originalCause) {
try {
return err(await options.catch(originalCause));
} catch (catchHandlerError) {
throw panic("Result.tryPromise catch handler threw", catchHandlerError);
}
}
};
const retry = config?.retry;
if (!retry) return execute();
const getDelay = (retryAttempt) => {
switch (retry.backoff) {
case "constant": return retry.delayMs;
case "linear": return retry.delayMs * (retryAttempt + 1);
case "exponential": return retry.delayMs * 2 ** retryAttempt;
}
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let result = await execute();
const shouldRetryFn = retry.shouldRetry ?? (() => true);
for (let attempt = 0; attempt < retry.times; attempt++) {
if (result.status !== "error") break;
const error = result.error;
if (!tryOrPanic(() => shouldRetryFn(error), "shouldRetry predicate threw")) break;
await sleep(getDelay(attempt));
result = await execute();
}
return result;
};
const map = dual(2, (result, fn) => {
return result.map(fn);
});
const mapError = dual(2, (result, fn) => {
return result.mapError(fn);
});
const andThen = dual(2, (result, fn) => {
return result.andThen(fn);
});
const andThenAsync = dual(2, (result, fn) => {
return result.andThenAsync(fn);
});
const match = dual(2, (result, handlers) => {
return result.match(handlers);
});
const tap = dual(2, (result, fn) => {
return result.tap(fn);
});
const tapAsync = dual(2, (result, fn) => {
return result.tapAsync(fn);
});
const unwrap = (result, message) => {
return result.unwrap(message);
};
/** Validates that a value is a Result instance. Throws with helpful message if not. */
function assertIsResult(value) {
if (value !== null && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "error")) return;
return panic("Result.gen body must return Result.ok() or Result.err(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)));
}
const unwrapOr = dual(2, (result, fallback) => {
return result.unwrapOr(fallback);
});
const gen = ((body, thisArg) => {
const iterator = body.call(thisArg);
if (Symbol.asyncIterator in iterator) return (async () => {
const asyncIter = iterator;
let state$1;
try {
state$1 = await asyncIter.next();
} catch (cause) {
throw panic("generator body threw", cause);
}
assertIsResult(state$1.value);
if (!state$1.done) try {
await asyncIter.return?.(void 0);
} catch (cause) {
throw panic("generator cleanup threw", cause);
}
return state$1.value;
})();
const syncIter = iterator;
let state;
try {
state = syncIter.next();
} catch (cause) {
throw panic("generator body threw", cause);
}
assertIsResult(state.value);
if (!state.done) try {
syncIter.return?.(void 0);
} catch (cause) {
throw panic("generator cleanup threw", cause);
}
return state.value;
});
async function* resultAwait(promise) {
return yield* await promise;
}
function isSerializedResult(obj) {
return obj !== null && typeof obj === "object" && "status" in obj && (obj.status === "ok" && "value" in obj || obj.status === "error" && "error" in obj);
}
const serialize = (result) => {
return result.status === "ok" ? {
status: "ok",
value: result.value
} : {
status: "error",
error: result.error
};
};
const deserialize = (value) => {
if (isSerializedResult(value)) return value.status === "ok" ? new Ok(value.value) : new Err(value.error);
return err(new ResultDeserializationError({ value }));
};
/**
* @deprecated Use `Result.deserialize` instead. Will be removed in 3.0.
*/
const hydrate = (value) => {
return deserialize(value);
};
const partition = (results) => {
const oks = [];
const errs = [];
for (const r of results) if (r.status === "ok") oks.push(r.value);
else errs.push(r.error);
return [oks, errs];
};
/**
* Flattens nested Result into single Result.
*
* @example
* const nested: Result<Result<number, E1>, E2> = Result.ok(Result.ok(42));
* const flat: Result<number, E1 | E2> = Result.flatten(nested); // Ok(42)
*/
const flatten = (result) => {
if (result.status === "ok") return result.value;
return result;
};
/**
* Utilities for creating and handling Result types.
*
* @example
* const result = Result.try(() => JSON.parse(str));
* const value = result.map(x => x.id).unwrapOr("default");
*/
const Result = {
ok,
isOk,
err,
isError,
try: tryFn,
tryPromise,
map,
mapError,
andThen,
andThenAsync,
match,
tap,
tapAsync,
unwrap,
unwrapOr,
gen,
await: resultAwait,
serialize,
deserialize,
hydrate,
partition,
flatten
};
//#endregion
export { Err, Ok, Panic, Result, ResultDeserializationError, TaggedError, UnhandledException, isPanic, isTaggedError, matchError, matchErrorPartial, panic };
//# sourceMappingURL=index.mjs.map

1
node_modules/better-result/dist/index.mjs.map generated vendored Normal file

File diff suppressed because one or more lines are too long

61
node_modules/better-result/package.json generated vendored Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "better-result",
"version": "2.7.0",
"description": "Lightweight Result type with generator-based composition",
"keywords": [
"error-handling",
"functional",
"generator",
"result",
"typescript"
],
"homepage": "https://github.com/dmmulroy/better-result#readme",
"bugs": {
"url": "https://github.com/dmmulroy/better-result/issues"
},
"license": "MIT",
"author": "Dillon Mulroy <dillon.mulroy@gmail.com> (https://github.com/dmmulroy)",
"repository": {
"type": "git",
"url": "https://github.com/dmmulroy/better-result"
},
"bin": {
"better-result": "./bin/cli.mjs"
},
"files": [
"bin",
"dist",
"skills",
"src"
],
"type": "module",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
}
},
"scripts": {
"build": "tsdown",
"prepublishOnly": "bun run build",
"test": "bun test src/",
"check": "tsc --noEmit",
"lint": "oxlint .",
"fmt": "oxfmt .",
"fmt:check": "oxfmt --check ."
},
"dependencies": {
"@clack/prompts": "^0.11.0"
},
"devDependencies": {
"@types/bun": "latest",
"oxfmt": "^0.23.0",
"oxlint": "^1.38.0",
"tsdown": "^0.19.0-beta.5",
"typescript": "^5.0.0"
}
}

162
node_modules/better-result/skills/adopt/SKILL.md generated vendored Normal file
View File

@@ -0,0 +1,162 @@
---
name: better-result-adopt
description: Migrate codebase from try/catch or Promise-based error handling to better-result. Use when adopting Result types, converting thrown exceptions to typed errors, or refactoring existing error handling to railway-oriented programming.
---
# better-result Adoption
Migrate existing error handling (try/catch, Promise rejections, thrown exceptions) to typed Result-based error handling with better-result.
## When to Use
- Adopting better-result in existing codebase
- Converting try/catch blocks to Result types
- Replacing thrown exceptions with typed errors
- Migrating Promise-based code to Result.tryPromise
- Introducing railway-oriented programming patterns
## Migration Strategy
### 1. Start at Boundaries
Begin migration at I/O boundaries (API calls, DB queries, file ops) and work inward. Don't attempt full-codebase migration at once.
### 2. Identify Error Categories
Before migrating, categorize errors in target code:
| Category | Example | Migration Target |
| -------------- | ---------------------- | ----------------------------------------------- |
| Domain errors | NotFound, Validation | TaggedError + Result.err |
| Infrastructure | Network, DB connection | Result.tryPromise + TaggedError |
| Bugs/defects | null deref, type error | Let throw (becomes Panic if in Result callback) |
### 3. Migration Order
1. Define TaggedError classes for domain errors
2. Wrap throwing functions with Result.try/tryPromise
3. Convert imperative error checks to Result chains
4. Refactor callbacks to generator composition
## Pattern Transformations
### Try/Catch to Result.try
```typescript
// BEFORE
function parseConfig(json: string): Config {
try {
return JSON.parse(json);
} catch (e) {
throw new ParseError(e);
}
}
// AFTER
function parseConfig(json: string): Result<Config, ParseError> {
return Result.try({
try: () => JSON.parse(json) as Config,
catch: (e) => new ParseError({ cause: e, message: `Parse failed: ${e}` }),
});
}
```
### Async/Await to Result.tryPromise
```typescript
// BEFORE
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new ApiError(res.status);
return res.json();
}
// AFTER
async function fetchUser(id: string): Promise<Result<User, ApiError | UnhandledException>> {
return Result.tryPromise({
try: async () => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new ApiError({ status: res.status, message: `API ${res.status}` });
return res.json() as Promise<User>;
},
catch: (e) => (e instanceof ApiError ? e : new UnhandledException({ cause: e })),
});
}
```
### Null Checks to Result
```typescript
// BEFORE
function findUser(id: string): User | null {
return users.find((u) => u.id === id) ?? null;
}
// Caller must check: if (user === null) ...
// AFTER
function findUser(id: string): Result<User, NotFoundError> {
const user = users.find((u) => u.id === id);
return user
? Result.ok(user)
: Result.err(new NotFoundError({ id, message: `User ${id} not found` }));
}
// Caller: yield* findUser(id) in Result.gen, or .match()
```
### Callback Hell to Generator
```typescript
// BEFORE
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId);
if (!order) throw new NotFoundError(orderId);
const validated = validateOrder(order);
if (!validated.ok) throw new ValidationError(validated.errors);
const result = await submitOrder(validated.data);
return result;
} catch (e) {
if (e instanceof NotFoundError) return { error: "not_found" };
if (e instanceof ValidationError) return { error: "invalid" };
throw e;
}
}
// AFTER
async function processOrder(orderId: string): Promise<Result<OrderResult, OrderError>> {
return Result.gen(async function* () {
const order = yield* Result.await(fetchOrder(orderId));
const validated = yield* validateOrder(order);
const result = yield* Result.await(submitOrder(validated));
return Result.ok(result);
});
}
// Error type is union of all yielded errors
```
## Defining TaggedErrors
See [references/tagged-errors.md](references/tagged-errors.md) for TaggedError patterns.
## Workflow
1. **Check for source reference**: Look for `opensrc/` directory - if present, read the better-result source code for implementation details and patterns
2. **Audit**: Find try/catch, Promise.catch, thrown errors in target module
3. **Define errors**: Create TaggedError classes for domain errors
4. **Wrap boundaries**: Use Result.try/tryPromise at I/O points
5. **Chain operations**: Convert if/else error checks to .andThen or Result.gen
6. **Update signatures**: Change return types to Result<T, E>
7. **Update callers**: Propagate Result handling up call stack
8. **Test**: Verify error paths with .match or type narrowing
## Common Pitfalls
- **Over-wrapping**: Don't wrap every function. Start at boundaries, propagate inward.
- **Losing error info**: Always include cause/context in TaggedError constructors.
- **Mixing paradigms**: Once a module returns Result, callers should too (or explicitly .unwrap).
- **Ignoring Panic**: Callbacks that throw become Panic. Fix the bug, don't catch Panic.
## References
- [TaggedError Patterns](references/tagged-errors.md) - Defining and matching typed errors
- `opensrc/` directory (if present) - Full better-result source code for deeper context

View File

@@ -0,0 +1,188 @@
# TaggedError Patterns
## Defining Errors
### Simple Error (no computed message)
```typescript
import { TaggedError } from "better-result";
class NotFoundError extends TaggedError("NotFoundError")<{
resource: string;
id: string;
message: string;
}>() {}
// Usage
new NotFoundError({ resource: "User", id: "123", message: "User 123 not found" });
```
### Error with Computed Message
Keep constructor for derived message:
```typescript
class NotFoundError extends TaggedError("NotFoundError")<{
resource: string;
id: string;
message: string;
}>() {
constructor(args: { resource: string; id: string }) {
super({ ...args, message: `${args.resource} not found: ${args.id}` });
}
}
// Usage: new NotFoundError({ resource: "User", id: "123" })
```
### Error with Cause
Wrap underlying exceptions:
```typescript
class DatabaseError extends TaggedError("DatabaseError")<{
operation: string;
message: string;
cause: unknown;
}>() {
constructor(args: { operation: string; cause: unknown }) {
const msg = args.cause instanceof Error ? args.cause.message : String(args.cause);
super({ ...args, message: `DB ${args.operation} failed: ${msg}` });
}
}
// Usage in Result.tryPromise
Result.tryPromise({
try: () => db.query(sql),
catch: (e) => new DatabaseError({ operation: "query", cause: e }),
});
```
### Error with Validation/Runtime Props
```typescript
class RateLimitError extends TaggedError("RateLimitError")<{
retryAfter: number;
message: string;
}>() {
constructor(args: { retryAfterMs: number }) {
super({
retryAfter: args.retryAfterMs,
message: `Rate limited, retry after ${args.retryAfterMs}ms`,
});
}
}
```
## Error Unions
Group related errors for function signatures:
```typescript
// Domain errors
class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string }>() {}
class ValidationError extends TaggedError("ValidationError")<{ field: string; message: string }>() {}
class AuthError extends TaggedError("AuthError")<{ reason: string; message: string }>() {}
// Union type
type AppError = NotFoundError | ValidationError | AuthError;
// Function signature
function processRequest(req: Request): Result<Response, AppError> { ... }
```
## Matching Errors
### Exhaustive Match
Compiler ensures all error types handled:
```typescript
import { matchError } from "better-result";
const message = matchError(error, {
NotFoundError: (e) => `Missing: ${e.id}`,
ValidationError: (e) => `Invalid: ${e.field}`,
AuthError: (e) => `Unauthorized: ${e.reason}`,
});
```
### Partial Match with Fallback
Handle subset, catch-all for rest:
```typescript
import { matchErrorPartial } from "better-result";
const message = matchErrorPartial(
error,
{ NotFoundError: (e) => `Missing: ${e.id}` },
(e) => `Error: ${e.message}`, // fallback for ValidationError, AuthError
);
```
### Type Guards
```typescript
import { isTaggedError, TaggedError } from "better-result";
// Check any tagged error
if (isTaggedError(value)) {
console.log(value._tag);
}
// Check specific error class
if (NotFoundError.is(value)) {
console.log(value.id); // narrowed to NotFoundError
}
// Also available
TaggedError.is(value); // same as isTaggedError
```
### In Result.match
```typescript
result.match({
ok: (value) => handleSuccess(value),
err: (e) =>
matchError(e, {
NotFoundError: (e) => handleNotFound(e),
ValidationError: (e) => handleValidation(e),
}),
});
```
## Pipeable Style
matchError/matchErrorPartial support data-last for pipelines:
```typescript
const handler = matchError({
NotFoundError: (e) => `Missing: ${e.id}`,
ValidationError: (e) => `Invalid: ${e.field}`,
});
pipe(error, handler);
```
## Converting Existing Errors
```typescript
// FROM: class hierarchy
class NotFoundError extends AppError {
constructor(public id: string) {
super(`Not found: ${id}`);
}
}
// TO: TaggedError
class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string }>() {
constructor(args: { id: string }) {
super({ ...args, message: `Not found: ${args.id}` });
}
}
// FROM: string/generic errors
throw "User not found";
// TO: typed Result
return Result.err(new NotFoundError({ id, message: "User not found" }));
```

View File

@@ -0,0 +1,248 @@
---
name: better-result-migrate-v2
description: Migrate better-result TaggedError from v1 (class-based) to v2 (factory-based) API
---
# better-result-migrate
Migrate `better-result` TaggedError classes from v1 (class-based) to v2 (factory-based) API.
## When to Use
- Upgrading `better-result` from v1 to v2
- User asks to migrate TaggedError classes
- User mentions TaggedError v1/v2 migration
## V1 API (old)
```typescript
class FooError extends TaggedError {
readonly _tag = "FooError" as const;
constructor(readonly id: string) {
super(`Foo: ${id}`);
}
}
// Static methods on TaggedError
TaggedError.match(err, { ... })
TaggedError.matchPartial(err, { ... }, fallback)
TaggedError.isTaggedError(value)
```
## V2 API (new)
```typescript
class FooError extends TaggedError("FooError")<{
id: string;
message: string;
}>() {}
// Standalone functions
matchError(err, { ... })
matchErrorPartial(err, { ... }, fallback)
isTaggedError(value)
TaggedError.is(value) // also available
FooError.is(value) // class-specific check
```
## Migration Rules
### 1. Simple class (no constructor logic)
```typescript
// BEFORE
class FooError extends TaggedError {
readonly _tag = "FooError" as const;
constructor(readonly id: string) {
super(`Foo: ${id}`);
}
}
// AFTER
class FooError extends TaggedError("FooError")<{
id: string;
message: string;
}>() {}
// Usage changes:
// BEFORE: new FooError("123")
// AFTER: new FooError({ id: "123", message: "Foo: 123" })
```
### 2. Class with computed message
Keep custom constructor to derive message:
```typescript
// BEFORE
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError" as const;
constructor(
readonly resource: string,
readonly id: string,
) {
super(`${resource} not found: ${id}`);
}
}
// AFTER
class NotFoundError extends TaggedError("NotFoundError")<{
resource: string;
id: string;
message: string;
}>() {
constructor(args: { resource: string; id: string }) {
super({ ...args, message: `${args.resource} not found: ${args.id}` });
}
}
// Usage: new NotFoundError({ resource: "User", id: "123" })
```
### 3. Class with validation
Keep validation in custom constructor:
```typescript
// BEFORE
class ValidationError extends TaggedError {
readonly _tag = "ValidationError" as const;
constructor(readonly field: string) {
if (!field) throw new Error("field required");
super(`Invalid: ${field}`);
}
}
// AFTER
class ValidationError extends TaggedError("ValidationError")<{
field: string;
message: string;
}>() {
constructor(args: { field: string }) {
if (!args.field) throw new Error("field required");
super({ ...args, message: `Invalid: ${args.field}` });
}
}
```
### 4. Class with additional runtime properties
```typescript
// BEFORE
class TimestampedError extends TaggedError {
readonly _tag = "TimestampedError" as const;
readonly timestamp = Date.now();
constructor(readonly reason: string) {
super(reason);
}
}
// AFTER
class TimestampedError extends TaggedError("TimestampedError")<{
reason: string;
timestamp: number;
message: string;
}>() {
constructor(args: { reason: string }) {
super({ ...args, message: args.reason, timestamp: Date.now() });
}
}
```
### 5. Static method migrations
| V1 | V2 |
| --------------------------------------------------- | -------------------------------------------- |
| `TaggedError.match(err, handlers)` | `matchError(err, handlers)` |
| `TaggedError.matchPartial(err, handlers, fallback)` | `matchErrorPartial(err, handlers, fallback)` |
| `TaggedError.isTaggedError(x)` | `isTaggedError(x)` or `TaggedError.is(x)` |
### 6. Import updates
```typescript
// BEFORE
import { TaggedError } from "better-result";
// AFTER
import { TaggedError, matchError, matchErrorPartial, isTaggedError } from "better-result";
```
## Workflow
1. **Find TaggedError classes**: Search for `extends TaggedError` in the codebase
2. **Analyze each class**:
- Extract `_tag` value
- Identify constructor params and their types
- Check for constructor logic (validation, computed message, side effects)
3. **Transform class**:
- Simple: Remove constructor, add props to type parameter
- Complex: Keep custom constructor, transform to object args
4. **Update usages**: Change `new FooError(a, b)` to `new FooError({ a, b, message })`
5. **Migrate static methods**: `TaggedError.match``matchError`, etc.
6. **Update imports**: Add `matchError`, `matchErrorPartial`, `isTaggedError`
## Example Full Migration
**Input:**
```typescript
import { TaggedError } from "better-result";
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError" as const;
constructor(readonly id: string) {
super(`Not found: ${id}`);
}
}
class NetworkError extends TaggedError {
readonly _tag = "NetworkError" as const;
constructor(
readonly url: string,
readonly status: number,
) {
super(`Request to ${url} failed with ${status}`);
}
}
type AppError = NotFoundError | NetworkError;
const handleError = (err: AppError) =>
TaggedError.match(err, {
NotFoundError: (e) => `Missing: ${e.id}`,
NetworkError: (e) => `Failed: ${e.url}`,
});
```
**Output:**
```typescript
import { TaggedError, matchError } from "better-result";
class NotFoundError extends TaggedError("NotFoundError")<{
id: string;
message: string;
}>() {
constructor(args: { id: string }) {
super({ ...args, message: `Not found: ${args.id}` });
}
}
class NetworkError extends TaggedError("NetworkError")<{
url: string;
status: number;
message: string;
}>() {
constructor(args: { url: string; status: number }) {
super({ ...args, message: `Request to ${args.url} failed with ${args.status}` });
}
}
type AppError = NotFoundError | NetworkError;
const handleError = (err: AppError) =>
matchError(err, {
NotFoundError: (e) => `Missing: ${e.id}`,
NetworkError: (e) => `Failed: ${e.url}`,
});
```

58
node_modules/better-result/src/dual.ts generated vendored Normal file
View File

@@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Creates data-first/data-last dual function.
*
* @template DataLast Curried (data-last) signature.
* @template DataFirst Uncurried (data-first) signature.
* @param arity Number of args for data-first form.
* @param body Implementation function.
* @returns Function supporting both calling conventions.
*
* @example
* const add: {
* (a: number, b: number): number;
* (b: number): (a: number) => number;
* } = dual(2, (a: number, b: number) => a + b);
*
* add(1, 2); // 3 (data-first)
* add(2)(1); // 3 (data-last)
*/
export function dual<
DataLast extends (...args: Array<any>) => any,
DataFirst extends (...args: Array<any>) => any,
>(arity: Parameters<DataFirst>["length"], body: DataFirst): DataLast & DataFirst {
if (arity === 2) {
return ((...args: Array<any>) => {
if (args.length >= 2) {
return body(args[0], args[1]);
}
return (self: any) => body(self, args[0]);
}) as DataLast & DataFirst;
}
if (arity === 3) {
return ((...args: Array<any>) => {
if (args.length >= 3) {
return body(args[0], args[1], args[2]);
}
return (self: any) => body(self, args[0], args[1]);
}) as DataLast & DataFirst;
}
if (arity === 4) {
return ((...args: Array<any>) => {
if (args.length >= 4) {
return body(args[0], args[1], args[2], args[3]);
}
return (self: any) => body(self, args[0], args[1], args[2]);
}) as DataLast & DataFirst;
}
return ((...args: Array<any>) => {
if (args.length >= arity) {
return body(...args);
}
return (self: any) => body(self, ...args);
}) as DataLast & DataFirst;
}

368
node_modules/better-result/src/error.test.ts generated vendored Normal file
View File

@@ -0,0 +1,368 @@
import { describe, expect, it } from "bun:test";
import {
TaggedError,
UnhandledException,
matchError,
matchErrorPartial,
isTaggedError,
} from "./error";
class NotFoundError extends TaggedError("NotFoundError")<{
id: string;
message: string;
}>() {}
class ValidationError extends TaggedError("ValidationError")<{
field: string;
message: string;
}>() {}
class NetworkError extends TaggedError("NetworkError")<{
url: string;
message: string;
}>() {}
type AppError = NotFoundError | ValidationError | NetworkError;
describe("TaggedError", () => {
describe("construction", () => {
it("sets name to tag", () => {
const error = new NotFoundError({ id: "123", message: "Not found: 123" });
expect(error.name).toBe("NotFoundError");
});
it("sets message", () => {
const error = new NotFoundError({ id: "123", message: "Not found: 123" });
expect(error.message).toBe("Not found: 123");
});
it("has _tag discriminator", () => {
const error = new NotFoundError({ id: "123", message: "Not found" });
expect(error._tag).toBe("NotFoundError");
});
it("preserves custom properties", () => {
const error = new NotFoundError({ id: "abc", message: "Not found" });
expect(error.id).toBe("abc");
});
it("chains cause in stack trace", () => {
const cause = new Error("root cause");
class ErrorWithCause extends TaggedError("ErrorWithCause")<{
message: string;
cause: unknown;
}>() {}
const error = new ErrorWithCause({ message: "wrapper", cause });
expect(error.stack).toContain("Caused by:");
expect(error.stack).toContain("root cause");
});
it("indents nested causes", () => {
const inner = new Error("inner");
class MiddleError extends TaggedError("MiddleError")<{
message: string;
cause: unknown;
}>() {}
class OuterError extends TaggedError("OuterError")<{
message: string;
cause: unknown;
}>() {}
const middle = new MiddleError({ message: "middle", cause: inner });
const outer = new OuterError({ message: "outer", cause: middle });
expect(outer.stack).toContain("Caused by:");
// Should have nested indentation
const lines = outer.stack?.split("\n") ?? [];
const causedByLines = lines.filter((l) => l.includes("Caused by:"));
expect(causedByLines.length).toBeGreaterThanOrEqual(1);
});
});
describe("isTaggedError", () => {
it("returns true for TaggedError", () => {
expect(isTaggedError(new NotFoundError({ id: "x", message: "not found" }))).toBe(true);
});
it("returns false for plain Error", () => {
expect(isTaggedError(new Error())).toBe(false);
});
it("returns false for non-errors", () => {
expect(isTaggedError({ _tag: "fake" })).toBe(false);
});
});
describe("static is() method", () => {
it("returns true for own instance", () => {
const err = new NotFoundError({ id: "123", message: "not found" });
expect(NotFoundError.is(err)).toBe(true);
});
it("returns false for different TaggedError", () => {
const err = new ValidationError({ field: "email", message: "invalid" });
expect(NotFoundError.is(err)).toBe(false);
});
it("returns false for plain Error", () => {
expect(NotFoundError.is(new Error())).toBe(false);
});
it("returns false for non-errors", () => {
expect(NotFoundError.is({ _tag: "NotFoundError" })).toBe(false);
});
it("FooError.is(fooError) is true", () => {
class FooError extends TaggedError("FooError")<{ message: string }>() {}
const fooError = new FooError({ message: "foo" });
expect(FooError.is(fooError)).toBe(true);
});
it("BarError.is(fooError) is false", () => {
class FooError extends TaggedError("FooError")<{ message: string }>() {}
class BarError extends TaggedError("BarError")<{ message: string }>() {}
const fooError = new FooError({ message: "foo" });
expect(BarError.is(fooError)).toBe(false);
});
it("isTaggedError(fooError) is true for any TaggedError", () => {
class FooError extends TaggedError("FooError")<{ message: string }>() {}
class BarError extends TaggedError("BarError")<{ message: string }>() {}
const fooError = new FooError({ message: "foo" });
const barError = new BarError({ message: "bar" });
expect(isTaggedError(fooError)).toBe(true);
expect(isTaggedError(barError)).toBe(true);
});
it("TaggedError.is(fooError) is true for any TaggedError", () => {
class FooError extends TaggedError("FooError")<{ message: string }>() {}
class BarError extends TaggedError("BarError")<{ message: string }>() {}
const fooError = new FooError({ message: "foo" });
const barError = new BarError({ message: "bar" });
expect(TaggedError.is(fooError)).toBe(true);
expect(TaggedError.is(barError)).toBe(true);
});
it("TaggedError.is returns false for plain Error", () => {
expect(TaggedError.is(new Error())).toBe(false);
});
});
describe("matchError", () => {
const matchAppError = (error: AppError) =>
matchError(error, {
NotFoundError: (e) => `missing: ${e.id}`,
ValidationError: (e) => `invalid: ${e.field}`,
NetworkError: (e) => `network: ${e.url}`,
});
it("matches NotFoundError", () => {
const error: AppError = new NotFoundError({ id: "123", message: "not found" });
expect(matchAppError(error)).toBe("missing: 123");
});
it("matches ValidationError", () => {
const error: AppError = new ValidationError({ field: "email", message: "invalid" });
expect(matchAppError(error)).toBe("invalid: email");
});
it("matches NetworkError", () => {
const error: AppError = new NetworkError({
url: "https://api.example.com",
message: "failed",
});
expect(matchAppError(error)).toBe("network: https://api.example.com");
});
it("works data-last (pipeable)", () => {
const error: AppError = new NotFoundError({ id: "456", message: "not found" });
const matcher = matchError<AppError, string>({
NotFoundError: (e) => `missing: ${e.id}`,
ValidationError: (e) => `invalid: ${e.field}`,
NetworkError: (e) => `network: ${e.url}`,
});
expect(matcher(error)).toBe("missing: 456");
});
it("provides type narrowing in handlers", () => {
const error = new NotFoundError({ id: "789", message: "not found" }) as AppError;
const result = matchError(error, {
NotFoundError: (e) => {
// Type is narrowed: e.id exists, e.field would error
const id: string = e.id;
const tag: "NotFoundError" = e._tag;
return { id, tag };
},
ValidationError: (e) => {
// Type is narrowed: e.field exists
const field: string = e.field;
const tag: "ValidationError" = e._tag;
return { field, tag };
},
NetworkError: (e) => {
// Type is narrowed: e.url exists
const url: string = e.url;
const tag: "NetworkError" = e._tag;
return { url, tag };
},
});
expect(result).toEqual({ id: "789", tag: "NotFoundError" });
});
});
describe("matchErrorPartial", () => {
const matchPartialAppError = (error: AppError) =>
matchErrorPartial(
error,
{
NotFoundError: (e) => `missing: ${e.id}`,
},
(e) => `fallback: ${e._tag}`,
);
it("matches known tag", () => {
const error: AppError = new NotFoundError({ id: "123", message: "not found" });
expect(matchPartialAppError(error)).toBe("missing: 123");
});
it("falls back for unhandled tag", () => {
const error: AppError = new NetworkError({
url: "https://api.example.com",
message: "failed",
});
expect(matchPartialAppError(error)).toBe("fallback: NetworkError");
});
it("narrows fallback type to exclude handled errors (data-first)", () => {
// Wrapper function ensures E is inferred as AppError from the parameter type
const matchTwoHandlers = (error: AppError) =>
matchErrorPartial(
error,
{
NotFoundError: (e) => `not found: ${e.id}`,
NetworkError: (e) => `network: ${e.url}`,
},
(e) => {
// Fallback only receives ValidationError since others are handled
const _check: ValidationError = e;
// @ts-expect-error - e should NOT have 'id' property (NotFoundError excluded)
void e.id;
// @ts-expect-error - e should NOT have 'url' property (NetworkError excluded)
void e.url;
return `validation: ${_check.field}`;
},
);
const error = new ValidationError({ field: "email", message: "invalid" });
expect(matchTwoHandlers(error)).toBe("validation: email");
});
it("fallback type excludes single handled error (data-first)", () => {
// Wrapper function ensures E is inferred as AppError from the parameter type
const matchOneHandler = (error: AppError) =>
matchErrorPartial(
error,
{
NotFoundError: (e) => `not found: ${e.id}`,
},
(e) => {
// Fallback receives ValidationError | NetworkError
type Expected = ValidationError | NetworkError;
const _check: Expected = e;
// @ts-expect-error - e should NOT have 'id' property (NotFoundError excluded)
void e.id;
return `other: ${_check._tag}`;
},
);
const error = new NetworkError({ url: "https://example.com", message: "timeout" });
expect(matchOneHandler(error)).toBe("other: NetworkError");
});
it("data-last form narrows fallback type", () => {
// Data-last: only need <E, R> - H is inferred from inline handlers object
const matcher = matchErrorPartial<AppError, string>(
{
NotFoundError: (e) => `not found: ${e.id}`,
ValidationError: (e) => `validation: ${e.field}`,
},
(e) => {
// Only NetworkError remains - type is properly narrowed
const _check: NetworkError = e;
return `network: ${_check.url}`;
},
);
const error: AppError = new NetworkError({ url: "https://api.test.com", message: "failed" });
expect(matcher(error)).toBe("network: https://api.test.com");
});
it("data-last form with explicit H for stored handlers", () => {
// When handlers are stored in a variable, use `as const` or explicit H
const handlers = {
NotFoundError: (e: NotFoundError) => `not found: ${e.id}`,
} as const;
const matcher = matchErrorPartial<AppError, string, typeof handlers>(
handlers,
(e) => {
// ValidationError | NetworkError remains
type Expected = ValidationError | NetworkError;
const _check: Expected = e;
// @ts-expect-error - e should NOT have 'id' property (NotFoundError excluded)
void e.id;
return `other: ${_check._tag}`;
},
);
const error: AppError = new ValidationError({ field: "email", message: "invalid" });
expect(matcher(error)).toBe("other: ValidationError");
});
it("handles all errors leaving never in fallback", () => {
// Wrapper function ensures E is inferred as AppError from the parameter type
const matchAllHandlers = (error: AppError) =>
matchErrorPartial(
error,
{
NotFoundError: (e) => `not found: ${e.id}`,
ValidationError: (e) => `validation: ${e.field}`,
NetworkError: (e) => `network: ${e.url}`,
},
(_e) => {
// When all errors are handled, fallback receives never
type FallbackType = typeof _e;
type IsNever = [FallbackType] extends [never] ? true : false;
const _proof: IsNever = true;
void _proof;
return "unreachable";
},
);
const error = new NotFoundError({ id: "123", message: "missing" });
expect(matchAllHandlers(error)).toBe("not found: 123");
});
});
});
describe("UnhandledException", () => {
it("wraps Error cause", () => {
const cause = new Error("original");
const error = new UnhandledException({ cause });
expect(error._tag).toBe("UnhandledException");
expect(error.message).toBe("Unhandled exception: original");
expect(error.cause).toBe(cause);
});
it("wraps non-Error cause", () => {
const error = new UnhandledException({ cause: "string error" });
expect(error.message).toBe("Unhandled exception: string error");
});
it("handles null cause", () => {
const error = new UnhandledException({ cause: null });
expect(error.message).toBe("Unhandled exception: null");
});
});

269
node_modules/better-result/src/error.ts generated vendored Normal file
View File

@@ -0,0 +1,269 @@
import { dual } from "./dual";
/** Serialize cause for JSON output */
const serializeCause = (cause: unknown): unknown => {
if (cause instanceof Error) {
return { name: cause.name, message: cause.message, stack: cause.stack };
}
return cause;
};
/** Any tagged error (for generic constraints) */
type AnyTaggedError = Error & { readonly _tag: string };
/** Type guard for any tagged error */
const isAnyTaggedError = (value: unknown): value is AnyTaggedError => {
return value instanceof Error && "_tag" in value && typeof value._tag === "string";
};
/**
* Factory for tagged error classes.
*
* @example
* class NotFoundError extends TaggedError("NotFoundError")<{
* id: string;
* message: string;
* }>() {}
*
* const err = new NotFoundError({ id: "123", message: "Not found: 123" });
* err._tag // "NotFoundError"
* err.id // "123"
* err.message // "Not found: 123"
*
* // Check if any tagged error
* TaggedError.is(err) // true
*/
export const TaggedError: {
<Tag extends string>(
tag: Tag,
): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props>;
/** Type guard for any TaggedError instance */
is(value: unknown): value is AnyTaggedError;
} = Object.assign(
<Tag extends string>(tag: Tag) =>
<Props extends Record<string, unknown> = {}>(): TaggedErrorClass<Tag, Props> => {
class Base extends Error {
readonly _tag: Tag = tag;
/** Type guard for this error class */
static is(value: unknown): value is Base {
return value instanceof Base;
}
constructor(args?: Props) {
const message =
args && "message" in args && typeof args.message === "string"
? args.message
: undefined;
const cause = args && "cause" in args ? args.cause : undefined;
super(message, cause !== undefined ? { cause } : undefined);
if (args) {
Object.assign(this, args);
}
Object.setPrototypeOf(this, new.target.prototype);
this.name = tag;
if (cause instanceof Error && cause.stack) {
const indented = cause.stack.replace(/\n/g, "\n ");
this.stack = `${this.stack}\nCaused by: ${indented}`;
}
}
toJSON(): object {
return {
...this,
_tag: this._tag,
name: this.name,
message: this.message,
cause: serializeCause(this.cause),
stack: this.stack,
};
}
}
// SAFETY: Cast needed for factory pattern - Props are assigned via Object.assign
return Base as unknown as TaggedErrorClass<Tag, Props>;
},
{ is: isAnyTaggedError },
);
/** Instance type produced by TaggedError factory */
export type TaggedErrorInstance<Tag extends string, Props> = Error & {
readonly _tag: Tag;
toJSON(): object;
} & Readonly<Props>;
/** Class type produced by TaggedError factory */
export type TaggedErrorClass<Tag extends string, Props> = {
new (
...args: keyof Props extends never ? [args?: {}] : [args: Props]
): TaggedErrorInstance<Tag, Props>;
/** Type guard for this error class */
is(value: unknown): value is TaggedErrorInstance<Tag, Props>;
};
/** Handler map for exhaustive matching */
type MatchHandlers<E extends AnyTaggedError, R> = {
[K in E["_tag"]]: (err: Extract<E, { _tag: K }>) => R;
};
/** Partial handler map for non-exhaustive matching */
type PartialMatchHandlers<E extends AnyTaggedError, R> = Partial<MatchHandlers<E, R>>;
/** Extract handled tags from a handlers object */
type HandledTags<E extends AnyTaggedError, H> = Extract<keyof H, E["_tag"]>;
/**
* Exhaustive pattern match on tagged error union.
*
* @example
* // Data-first
* matchError(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* });
*
* // Data-last (pipeable)
* pipe(err, matchError({
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* }));
*/
export const matchError: {
<E extends AnyTaggedError, R>(err: E, handlers: MatchHandlers<E, R>): R;
<E extends AnyTaggedError, R>(handlers: MatchHandlers<E, R>): (err: E) => R;
} = dual(2, <E extends AnyTaggedError, R>(err: E, handlers: MatchHandlers<E, R>): R => {
const handler = handlers[err._tag as E["_tag"]];
// SAFETY: handler exists if handlers satisfies MatchHandlers<E, R>
return handler(err as Extract<E, { _tag: (typeof err)["_tag"] }>);
});
/**
* Partial pattern match with fallback for unhandled tags.
*
* @example
* matchErrorPartial(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* }, (e) => `Unknown: ${e.message}`);
*/
export const matchErrorPartial: {
<E extends AnyTaggedError, R, const H extends PartialMatchHandlers<E, R>>(
err: E,
handlers: H,
fallback: (e: Exclude<E, { _tag: NoInfer<HandledTags<E, H>> }>) => R,
): R;
<
E extends AnyTaggedError,
R,
const H extends PartialMatchHandlers<E, R> = PartialMatchHandlers<E, R>,
>(
handlers: H,
fallback: (e: Exclude<E, { _tag: NoInfer<HandledTags<E, H>> }>) => R,
): (err: E) => R;
} = dual(
3,
<E extends AnyTaggedError, R, H extends PartialMatchHandlers<E, R>>(
err: E,
handlers: H,
fallback: (e: Exclude<E, { _tag: HandledTags<E, H> }>) => R,
): R => {
type K = HandledTags<E, H>;
const handler = handlers[err._tag as K];
if (typeof handler === "function") {
// SAFETY: handler exists and matches the tag
return handler(err as Parameters<NonNullable<typeof handler>>[0]);
}
// SAFETY: If no handler matched, err is in the Exclude type
return fallback(err as Exclude<E, { _tag: K }>);
},
);
/**
* Type guard for tagged error instances.
*
* @example
* if (isTaggedError(value)) { value._tag }
*/
export const isTaggedError = isAnyTaggedError;
/**
* Wraps exceptions caught by Result.try/tryPromise.
* Custom constructor derives message from cause.
*/
export class UnhandledException extends TaggedError("UnhandledException")<{
message: string;
cause: unknown;
}>() {
constructor(args: { cause: unknown }) {
const message =
args.cause instanceof Error
? `Unhandled exception: ${args.cause.message}`
: `Unhandled exception: ${String(args.cause)}`;
super({ message, cause: args.cause });
}
}
/**
* Unrecoverable error — user code threw inside Result operations.
*
* @example
* // Panic in generator cleanup:
* Result.gen(function* () {
* try {
* yield* Result.err("expected error");
* } finally {
* throw new Error("cleanup failed"); // Panic!
* }
* });
*
* // Panic in combinator:
* Result.ok(1).map(() => { throw new Error("oops"); }); // Panic!
*/
export class Panic extends TaggedError("Panic")<{
message: string;
cause?: unknown;
}>() {}
/**
* Returned when Result.deserialize receives invalid input.
*
* @example
* const result = Result.deserialize(invalidData);
* if (Result.isError(result) && ResultDeserializationError.is(result.error)) {
* console.log("Invalid input:", result.error.value);
* }
*/
export class ResultDeserializationError extends TaggedError("ResultDeserializationError")<{
message: string;
value: unknown;
}>() {
constructor(args: { value: unknown }) {
super({
message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
value: args.value,
});
}
}
/**
* Type guard for Panic instances.
*
* @example
* if (isPanic(value)) { value.cause }
*/
export const isPanic = (value: unknown): value is Panic => {
return value instanceof Panic;
};
/**
* Throw an unrecoverable Panic.
*
* @example
* panic("something went wrong", cause);
*/
export const panic = (message: string, cause?: unknown): never => {
throw new Panic({ message, cause });
};

14
node_modules/better-result/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,14 @@
export { Result, Ok, Err } from "./result";
export type { InferOk, InferErr, SerializedResult, SerializedOk, SerializedErr } from "./result";
export {
Panic,
panic,
isPanic,
TaggedError,
UnhandledException,
ResultDeserializationError,
matchError,
matchErrorPartial,
isTaggedError,
} from "./error";
export type { TaggedErrorInstance, TaggedErrorClass } from "./error";

1763
node_modules/better-result/src/result.test.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

1024
node_modules/better-result/src/result.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff