From 4a8844f0af9d9f91ca7e4fb10b94e9e72e17f655 Mon Sep 17 00:00:00 2001 From: Matt Galligan Date: Sat, 21 Feb 2026 22:17:32 -0500 Subject: [PATCH 1/2] refactor(cli): convert modify.ts and add.ts to typed error flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace throw new Error(...) with typed contract errors (ValidationError, NotFoundError) in modify and add command handlers. Update Result.tryPromise catch handlers to preserve error category instead of wrapping all errors as InternalError. 🤘🏻 In-collaboration-with: [Claude Code](https://claude.com/claude-code) --- packages/cli/src/commands/add.ts | 58 +++++++++++++++++++---------- packages/cli/src/commands/modify.ts | 51 ++++++++++++++++++------- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index e79efca..3909e7b 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -2,7 +2,12 @@ import { readFile } from "node:fs/promises"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { type InsertionResult, type InsertionSpec, @@ -128,16 +133,22 @@ function assertAddOptionConflicts( contentArg?: string ): void { if (options.position && (options.before || options.after)) { - throw new Error("Use --position or --before/--after (not both)."); + throw ValidationError.fromMessage( + "Use --position or --before/--after (not both)." + ); } if (options.before && options.after) { - throw new Error("Cannot combine --before and --after."); + throw ValidationError.fromMessage("Cannot combine --before and --after."); } if (options.type && typeArg) { - throw new Error("Cannot combine --type with positional ."); + throw ValidationError.fromMessage( + "Cannot combine --type with positional ." + ); } if (options.content && contentArg) { - throw new Error("Cannot combine --content with positional ."); + throw ValidationError.fromMessage( + "Cannot combine --content with positional ." + ); } } @@ -192,7 +203,7 @@ function assertValidPosition( value: string ): asserts value is "before" | "after" { if (value !== "before" && value !== "after") { - throw new Error("--position must be 'before' or 'after'"); + throw ValidationError.create("position", "must be 'before' or 'after'"); } } @@ -230,7 +241,7 @@ function applyOrderAndId( if (options.order !== undefined && options.order !== null) { const parsedOrder = Number.parseInt(String(options.order), 10); if (!Number.isFinite(parsedOrder)) { - throw new Error("--order expects an integer"); + throw ValidationError.create("order", "expects an integer"); } state.order = parsedOrder; } @@ -263,7 +274,7 @@ function validateFromMode(state: InsertParseState): void { state.id !== undefined; if (hasInlineData) { - throw new Error( + throw ValidationError.fromMessage( "Positional and inline flags cannot be combined with --from" ); } @@ -275,13 +286,18 @@ function ensureRequiredFields(state: InsertParseState): { content: string; } { if (!state.fileLine) { - throw new Error("add requires a FILE:LINE positional argument"); + throw ValidationError.fromMessage( + "add requires a FILE:LINE positional argument" + ); } if (!state.type) { - throw new Error("--type is required when not using --from"); + throw ValidationError.create("type", "is required when not using --from"); } if (!state.content) { - throw new Error("--content is required when not using --from"); + throw ValidationError.create( + "content", + "is required when not using --from" + ); } return { fileLine: state.fileLine, @@ -358,13 +374,17 @@ export type AddCommandResult = { export function runAddCommand( parsed: ParsedAddArgs, context: CommandContext -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runAddCommandInner(parsed, context), - catch: (cause) => - new InternalError({ - message: `Add failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Add failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -377,7 +397,7 @@ async function runAddCommandInner( : parsed.specs; if (specs.length === 0) { - throw new Error("No insertions provided"); + throw ValidationError.fromMessage("No insertions provided"); } const idManager = parsed.options.write @@ -423,7 +443,7 @@ async function loadSpecsFromSource(path: string): Promise { const issuePath = issue.path.length > 0 ? issue.path.join(".") : "root"; logger.error(` - ${issuePath}: ${issue.message}`); } - throw new Error("JSON validation failed"); + throw ValidationError.fromMessage("JSON validation failed"); } throw error; } @@ -442,7 +462,7 @@ function normalizeInsertionSpecs(parsed: unknown): unknown { return [parsed]; } - throw new Error( + throw ValidationError.fromMessage( "Invalid JSON: expected InsertionSpec, InsertionSpec[], or { insertions: InsertionSpec[] }" ); } diff --git a/packages/cli/src/commands/modify.ts b/packages/cli/src/commands/modify.ts index b43cb72..4d3de1c 100644 --- a/packages/cli/src/commands/modify.ts +++ b/packages/cli/src/commands/modify.ts @@ -1,7 +1,13 @@ // tldr ::: edit command implementation for wm CLI import { readFile, writeFile } from "node:fs/promises"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + NotFoundError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { fingerprintContent, fingerprintContext, @@ -119,13 +125,17 @@ export function runModifyCommand( targetArg: string | undefined, options: ModifyOptions, io: ModifyIo = DEFAULT_IO -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runModifyCommandInner(context, targetArg, options, io), - catch: (cause) => - new InternalError({ - message: `Modify failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Modify failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -139,7 +149,11 @@ async function runModifyCommandInner( const snapshot = await loadWaymarkSnapshot(target); const originalFirstLine = snapshot.lines[snapshot.lineIndex]; if (!originalFirstLine) { - throw new Error(`Line ${snapshot.lineIndex + 1} not found in file`); + throw NotFoundError.create( + "line", + String(snapshot.lineIndex + 1), + { file: target.file } + ); } const originalContent = extractFirstLineContent(originalFirstLine); const existingId = extractTrailingId(originalContent); @@ -271,7 +285,10 @@ async function loadWaymarkSnapshot(target: ModifyTarget): Promise { const records = parse(fileContent, { file: target.file }); const record = records.find((entry) => entry.startLine === target.line); if (!record) { - throw new Error(`No waymark found at ${target.file}:${target.line}`); + throw NotFoundError.create( + "waymark", + `${target.file}:${target.line}` + ); } const lineIndex = record.startLine - 1; return { @@ -346,7 +363,7 @@ function determineType(current: string, requested?: string): string { } const trimmed = requested.trim(); if (!trimmed) { - throw new Error("Waymark type cannot be empty"); + throw ValidationError.create("type", "cannot be empty"); } return trimmed; } @@ -458,10 +475,14 @@ async function resolveTarget( idOption?: string ): Promise { if (targetArg && idOption) { - throw new Error("Cannot specify both file:line and --id"); + throw ValidationError.fromMessage( + "Cannot specify both file:line and --id" + ); } if (!(targetArg || idOption)) { - throw new Error("Must provide a target (file:line) or --id"); + throw ValidationError.fromMessage( + "Must provide a target (file:line) or --id" + ); } if (idOption) { @@ -469,7 +490,9 @@ async function resolveTarget( } if (!targetArg) { - throw new Error("Target argument is required when --id is not provided"); + throw ValidationError.fromMessage( + "Target argument is required when --id is not provided" + ); } return parseFileLineTarget(targetArg, { @@ -486,7 +509,7 @@ async function resolveTargetFromId( const index = new JsonIdIndex({ workspaceRoot }); const entry = await index.get(normalized); if (!entry) { - throw new Error(`Waymark ID ${normalized} not found in index`); + throw NotFoundError.create("waymark ID", normalized); } return { file: entry.file, @@ -678,7 +701,7 @@ function ensureModificationsSpecified(options: ModifyOptions): void { const hasContent = options.content !== undefined; if (!(hasType || hasSignals || hasContent)) { - throw new Error( + throw ValidationError.fromMessage( "No modifications specified. Use --type, --flagged, --starred, --clear-signals, --content, or run without arguments for interactive prompts." ); } From 16bb9a70999eacba130936e70d709b2c232bbe7a Mon Sep 17 00:00:00 2001 From: Matt Galligan Date: Sat, 21 Feb 2026 22:22:40 -0500 Subject: [PATCH 2/2] refactor(cli): convert remaining CLI handlers to typed error flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert throws in init.ts, remove.ts, scan.ts, update.ts, fmt.ts, lint.ts, check.ts, and doctor.ts to use typed contract errors (ValidationError, InternalError) instead of generic Error. Update all Result.tryPromise catch handlers to preserve error categories via AnyKitError duck-typing. 🤘🏻 In-collaboration-with: [Claude Code](https://claude.com/claude-code) --- packages/cli/src/commands/check.ts | 16 +++++++----- packages/cli/src/commands/doctor.ts | 16 +++++++----- packages/cli/src/commands/fmt.ts | 39 +++++++++++++++++++---------- packages/cli/src/commands/init.ts | 39 +++++++++++++++++++---------- packages/cli/src/commands/lint.ts | 23 +++++++++++------ packages/cli/src/commands/modify.ts | 17 ++++--------- packages/cli/src/commands/remove.ts | 29 +++++++++++++-------- packages/cli/src/commands/scan.ts | 29 +++++++++++++-------- packages/cli/src/commands/update.ts | 18 +++++++------ 9 files changed, 142 insertions(+), 84 deletions(-) diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index 2236127..0c6ce80 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -1,7 +1,7 @@ // tldr ::: cross-file content integrity validation for waymarks import { ANSI } from "@outfitter/cli/colors"; -import { InternalError, Result } from "@outfitter/contracts"; +import { type AnyKitError, InternalError, Result } from "@outfitter/contracts"; import { parse, type WaymarkRecord } from "@waymarks/core"; import type { CommandContext } from "../types.ts"; import { expandInputPaths } from "../utils/fs.ts"; @@ -332,13 +332,17 @@ async function parseFiles( export function runCheckCommand( context: CommandContext, options: CheckCommandOptions -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runCheckCommandInner(context, options), - catch: (cause) => - new InternalError({ - message: `Check failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Check failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 80b42bb..9e53ac2 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -5,7 +5,7 @@ import { readFile, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; import { ANSI } from "@outfitter/cli/colors"; -import { InternalError, Result } from "@outfitter/contracts"; +import { type AnyKitError, InternalError, Result } from "@outfitter/contracts"; import type { CommandContext } from "../types"; import { logger } from "../utils/logger"; import { wrap } from "../utils/theme"; @@ -63,13 +63,17 @@ export type DoctorCommandOptions = { export function runDoctorCommand( context: CommandContext, options: DoctorCommandOptions -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runDoctorCommandInner(context, options), - catch: (cause) => - new InternalError({ - message: `Doctor diagnostics failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Doctor diagnostics failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } diff --git a/packages/cli/src/commands/fmt.ts b/packages/cli/src/commands/fmt.ts index 58b9662..e1ccae4 100644 --- a/packages/cli/src/commands/fmt.ts +++ b/packages/cli/src/commands/fmt.ts @@ -2,7 +2,12 @@ import { readFile, writeFile } from "node:fs/promises"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import type { FormatResult, WaymarkConfig } from "@waymarks/core"; import { formatText } from "@waymarks/core"; @@ -26,13 +31,17 @@ const IGNORE_FILE_MARKER_PATTERN = export function formatFile( options: { filePath: string; write: boolean }, context: CommandContext -): Promise> { +): Promise> { return Result.tryPromise({ try: () => formatFileInner(options, context), - catch: (cause) => - new InternalError({ - message: `Format failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Format failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -102,16 +111,20 @@ async function filterFilesWithWaymarks(paths: string[]): Promise { export function expandFormatPaths( inputs: string[], config: WaymarkConfig -): Promise> { +): Promise> { return Result.tryPromise({ try: async () => { const expanded = await expandInputPaths(inputs, config); return await filterFilesWithWaymarks(expanded); }, - catch: (cause) => - new InternalError({ - message: `Path expansion failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Path expansion failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -122,7 +135,7 @@ export function expandFormatPaths( */ export function parseFormatArgs(argv: string[]): FormatCommandOptions { if (argv.length === 0) { - throw new Error("fmt requires at least one file path"); + throw ValidationError.fromMessage("fmt requires at least one file path"); } const yes = argv.includes("--yes") || argv.includes("-y"); @@ -130,7 +143,7 @@ export function parseFormatArgs(argv: string[]): FormatCommandOptions { const filePaths = remaining.filter((path) => path.length > 0); if (filePaths.length === 0) { - throw new Error("fmt requires at least one file path"); + throw ValidationError.fromMessage("fmt requires at least one file path"); } return { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 9930913..5531839 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -4,7 +4,13 @@ import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; -import { CancelledError, InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + CancelledError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { promptSelect } from "../utils/clack-prompts.ts"; import { logger } from "../utils/logger.ts"; import { assertPromptAllowed } from "../utils/prompts.ts"; @@ -31,13 +37,17 @@ const CONFIG_SCOPES: ConfigScope[] = ["project", "user"]; */ export function runInitCommand( options: InitCommandOptions = {} -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runInitCommandInner(options), - catch: (cause) => - new InternalError({ - message: `Init failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Init failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -101,7 +111,7 @@ async function runInitCommandInner( // Check if config already exists if (existsSync(configPath) && !force) { - throw new Error( + throw ValidationError.fromMessage( `Config already exists at ${configPath}\nUse --force to overwrite` ); } @@ -136,8 +146,9 @@ async function runInitCommandInner( function validateFormat(format: string): ConfigFormat { if (!CONFIG_FORMATS.includes(format as ConfigFormat)) { - throw new Error( - `Invalid format "${format}". Use one of: ${CONFIG_FORMATS.join(", ")}` + throw ValidationError.create( + "format", + `"${format}" is invalid. Use one of: ${CONFIG_FORMATS.join(", ")}` ); } return format as ConfigFormat; @@ -145,8 +156,9 @@ function validateFormat(format: string): ConfigFormat { function validatePreset(preset: string): ConfigPreset { if (!CONFIG_PRESETS.includes(preset as ConfigPreset)) { - throw new Error( - `Invalid preset "${preset}". Use one of: ${CONFIG_PRESETS.join(", ")}` + throw ValidationError.create( + "preset", + `"${preset}" is invalid. Use one of: ${CONFIG_PRESETS.join(", ")}` ); } return preset as ConfigPreset; @@ -154,8 +166,9 @@ function validatePreset(preset: string): ConfigPreset { function validateScope(scope: string): ConfigScope { if (!CONFIG_SCOPES.includes(scope as ConfigScope)) { - throw new Error( - `Invalid scope "${scope}". Use one of: ${CONFIG_SCOPES.join(", ")}` + throw ValidationError.create( + "scope", + `"${scope}" is invalid. Use one of: ${CONFIG_SCOPES.join(", ")}` ); } return scope as ConfigScope; diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 812f0ee..8720845 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -1,7 +1,12 @@ // tldr ::: lint command helpers for waymark CLI import { readFile } from "node:fs/promises"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { isValidType, parse, @@ -402,7 +407,7 @@ export function parseLintArgs(argv: string[]): LintCommandOptions { const json = argv.includes("--json"); const filePaths = argv.filter((arg) => !arg.startsWith("-")); if (filePaths.length === 0) { - throw new Error("lint requires at least one file path"); + throw ValidationError.fromMessage("lint requires at least one file path"); } return { filePaths, json }; } @@ -418,13 +423,17 @@ export function lintFiles( filePaths: string[], allowTypes: string[], config: WaymarkConfig -): Promise> { +): Promise> { return Result.tryPromise({ try: () => lintFilesInner(filePaths, allowTypes, config), - catch: (cause) => - new InternalError({ - message: `Lint failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Lint failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } diff --git a/packages/cli/src/commands/modify.ts b/packages/cli/src/commands/modify.ts index 4d3de1c..4a873a8 100644 --- a/packages/cli/src/commands/modify.ts +++ b/packages/cli/src/commands/modify.ts @@ -149,11 +149,9 @@ async function runModifyCommandInner( const snapshot = await loadWaymarkSnapshot(target); const originalFirstLine = snapshot.lines[snapshot.lineIndex]; if (!originalFirstLine) { - throw NotFoundError.create( - "line", - String(snapshot.lineIndex + 1), - { file: target.file } - ); + throw NotFoundError.create("line", String(snapshot.lineIndex + 1), { + file: target.file, + }); } const originalContent = extractFirstLineContent(originalFirstLine); const existingId = extractTrailingId(originalContent); @@ -285,10 +283,7 @@ async function loadWaymarkSnapshot(target: ModifyTarget): Promise { const records = parse(fileContent, { file: target.file }); const record = records.find((entry) => entry.startLine === target.line); if (!record) { - throw NotFoundError.create( - "waymark", - `${target.file}:${target.line}` - ); + throw NotFoundError.create("waymark", `${target.file}:${target.line}`); } const lineIndex = record.startLine - 1; return { @@ -475,9 +470,7 @@ async function resolveTarget( idOption?: string ): Promise { if (targetArg && idOption) { - throw ValidationError.fromMessage( - "Cannot specify both file:line and --id" - ); + throw ValidationError.fromMessage("Cannot specify both file:line and --id"); } if (!(targetArg || idOption)) { throw ValidationError.fromMessage( diff --git a/packages/cli/src/commands/remove.ts b/packages/cli/src/commands/remove.ts index fbb3e0e..72bdfa1 100644 --- a/packages/cli/src/commands/remove.ts +++ b/packages/cli/src/commands/remove.ts @@ -2,7 +2,12 @@ import { readFile } from "node:fs/promises"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { type RemovalResult, type RemovalSpec, @@ -179,7 +184,7 @@ function finalizeRemoveState(state: RemoveParseState): ParsedRemoveArgs { if (state.from) { if (state.positional.length > 0 || state.ids.length > 0) { - throw new Error( + throw ValidationError.fromMessage( "Cannot combine --from with positional arguments or --id" ); } @@ -201,7 +206,7 @@ function finalizeRemoveState(state: RemoveParseState): ParsedRemoveArgs { } if (specs.length === 0) { - throw new Error("No removal targets provided"); + throw ValidationError.fromMessage("No removal targets provided"); } return { specs, options }; @@ -276,13 +281,17 @@ export function runRemoveCommand( parsed: ParsedRemoveArgs, context: CommandContext, execution: { writeOverride?: boolean } = {} -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runRemoveCommandInner(parsed, context, execution), - catch: (cause) => - new InternalError({ - message: `Remove failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Remove failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -449,7 +458,7 @@ function parseRemovalPayload(parsed: unknown): LoadedRemovePayload { return { specs: [RemovalSpecSchema.parse(parsed)] }; } - throw new Error( + throw ValidationError.fromMessage( "Invalid JSON: expected RemovalSpec, RemovalSpec[], or { removals: RemovalSpec[] }" ); } @@ -464,7 +473,7 @@ async function loadSpecsFromSource(path: string): Promise { } catch (error) { if (error instanceof ZodError) { logZodValidationErrors(error); - throw new Error("JSON validation failed"); + throw ValidationError.fromMessage("JSON validation failed"); } throw error; } diff --git a/packages/cli/src/commands/scan.ts b/packages/cli/src/commands/scan.ts index 58fb5da..cd2b57d 100644 --- a/packages/cli/src/commands/scan.ts +++ b/packages/cli/src/commands/scan.ts @@ -2,7 +2,12 @@ import { readFile, stat } from "node:fs/promises"; import { performance } from "node:perf_hooks"; -import { InternalError, Result } from "@outfitter/contracts"; +import { + type AnyKitError, + InternalError, + Result, + ValidationError, +} from "@outfitter/contracts"; import { canHaveWaymarks, parse, @@ -119,13 +124,17 @@ export function scanRecords( filePaths: string[], config: WaymarkConfig, options: ScanRuntimeOptions = {} -): Promise> { +): Promise> { return Result.tryPromise({ try: () => scanRecordsInner(filePaths, config, options), - catch: (cause) => - new InternalError({ - message: `Scan failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Scan failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -241,7 +250,7 @@ function createCache(options: ScanRuntimeOptions): WaymarkCache | undefined { */ export function parseScanArgs(argv: string[]): ParsedScanArgs { if (argv.length === 0) { - throw new Error("scan requires a file path"); + throw ValidationError.fromMessage("scan requires a file path"); } let format: ScanOutputFormat = "text"; @@ -259,10 +268,10 @@ export function parseScanArgs(argv: string[]): ParsedScanArgs { if (arg.startsWith("--")) { const nextFormat = formatFlags[arg]; if (!nextFormat) { - throw new Error(`Unknown flag for scan: ${arg}`); + throw ValidationError.fromMessage(`Unknown flag for scan: ${arg}`); } if (formatSet) { - throw new Error("scan accepts only one format flag"); + throw ValidationError.fromMessage("scan accepts only one format flag"); } format = nextFormat; formatSet = true; @@ -272,7 +281,7 @@ export function parseScanArgs(argv: string[]): ParsedScanArgs { } if (positional.length === 0) { - throw new Error("scan requires a file path"); + throw ValidationError.fromMessage("scan requires a file path"); } return { filePaths: positional, format }; diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 57c7ea8..e5552a1 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -4,7 +4,7 @@ import { spawn } from "node:child_process"; import { realpathSync } from "node:fs"; import { sep } from "node:path"; -import { InternalError, Result } from "@outfitter/contracts"; +import { type AnyKitError, InternalError, Result } from "@outfitter/contracts"; import { logger } from "../utils/logger.ts"; import { confirm } from "../utils/prompts.ts"; @@ -205,13 +205,17 @@ async function confirmUpdateCommand( */ export function runUpdateCommand( options: UpdateCommandOptions = {} -): Promise> { +): Promise> { return Result.tryPromise({ try: () => runUpdateCommandInner(options), - catch: (cause) => - new InternalError({ - message: `Update failed: ${cause instanceof Error ? cause.message : String(cause)}`, - }), + catch: (cause) => { + if (cause instanceof Error && "category" in cause) { + return cause as AnyKitError; + } + return InternalError.create( + `Update failed: ${cause instanceof Error ? cause.message : String(cause)}` + ); + }, }); } @@ -266,7 +270,7 @@ async function runUpdateCommandInner( const childExitCode = await runChild(command, NPM_UPDATE_ARGS); if (childExitCode !== 0) { - throw new Error(`npm install exited with code ${childExitCode}`); + throw InternalError.create(`npm install exited with code ${childExitCode}`); } return {