Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 39 additions & 19 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <type>.");
throw ValidationError.fromMessage(
"Cannot combine --type with positional <type>."
);
}
if (options.content && contentArg) {
throw new Error("Cannot combine --content with positional <content>.");
throw ValidationError.fromMessage(
"Cannot combine --content with positional <content>."
);
}
}

Expand Down Expand Up @@ -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'");
}
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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"
);
}
Expand All @@ -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,
Expand Down Expand Up @@ -358,13 +374,17 @@ export type AddCommandResult = {
export function runAddCommand(
parsed: ParsedAddArgs,
context: CommandContext
): Promise<Result<AddCommandResult, InternalError>> {
): Promise<Result<AddCommandResult, AnyKitError>> {
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)}`
);
},
});
}

Expand All @@ -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
Expand Down Expand Up @@ -423,7 +443,7 @@ async function loadSpecsFromSource(path: string): Promise<InsertionSpec[]> {
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;
}
Expand All @@ -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[] }"
);
}
Expand Down
16 changes: 10 additions & 6 deletions packages/cli/src/commands/check.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -332,13 +332,17 @@ async function parseFiles(
export function runCheckCommand(
context: CommandContext,
options: CheckCommandOptions
): Promise<Result<CheckReport, InternalError>> {
): Promise<Result<CheckReport, AnyKitError>> {
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)}`
);
},
});
}

Expand Down
16 changes: 10 additions & 6 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -63,13 +63,17 @@ export type DoctorCommandOptions = {
export function runDoctorCommand(
context: CommandContext,
options: DoctorCommandOptions
): Promise<Result<DoctorReport, InternalError>> {
): Promise<Result<DoctorReport, AnyKitError>> {
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)}`
);
},
});
}

Expand Down
39 changes: 26 additions & 13 deletions packages/cli/src/commands/fmt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -26,13 +31,17 @@ const IGNORE_FILE_MARKER_PATTERN =
export function formatFile(
options: { filePath: string; write: boolean },
context: CommandContext
): Promise<Result<FormatResult, InternalError>> {
): Promise<Result<FormatResult, AnyKitError>> {
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)}`
);
},
});
}

Expand Down Expand Up @@ -102,16 +111,20 @@ async function filterFilesWithWaymarks(paths: string[]): Promise<string[]> {
export function expandFormatPaths(
inputs: string[],
config: WaymarkConfig
): Promise<Result<string[], InternalError>> {
): Promise<Result<string[], AnyKitError>> {
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)}`
);
},
});
}

Expand All @@ -122,15 +135,15 @@ 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");
const remaining = argv.filter((arg) => !arg.startsWith("-"));
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 {
Expand Down
39 changes: 26 additions & 13 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,13 +37,17 @@ const CONFIG_SCOPES: ConfigScope[] = ["project", "user"];
*/
export function runInitCommand(
options: InitCommandOptions = {}
): Promise<Result<void, InternalError>> {
): Promise<Result<void, AnyKitError>> {
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)}`
);
},
});
}

Expand Down Expand Up @@ -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`
);
}
Expand Down Expand Up @@ -136,26 +146,29 @@ 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;
}

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;
}

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;
Expand Down
Loading