Skip to content
Merged
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
945 changes: 84 additions & 861 deletions AGENTS.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@luxass/utils": "2.7.3",
"commit-parser": "1.3.0",
"farver": "1.0.0-beta.1",
"mri": "1.2.0",
"prompts": "2.4.2",
"semver": "7.7.4",
"tinyexec": "1.0.2"
Expand Down
9 changes: 0 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/core/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export async function checkoutBranch(
return ok(true);
}

console.warn(`Unexpected git checkout output: ${output}`);
logger.warn(`Unexpected git checkout output: ${output}`);
return ok(false);
} catch (error) {
const gitError = toGitError("checkoutBranch", error);
Expand Down Expand Up @@ -651,7 +651,7 @@ export async function getGroupedFilesByCommitSha(
* @param workspaceRoot - The root directory of the workspace
* @returns Result indicating success or failure
*/
export async function createPackageTag(
async function createPackageTag(
packageName: string,
version: string,
workspaceRoot: string,
Expand All @@ -678,7 +678,7 @@ export async function createPackageTag(
* @param workspaceRoot - The root directory of the workspace
* @returns Result indicating success or failure
*/
export async function pushTag(
async function pushTag(
tagName: string,
workspaceRoot: string,
): Promise<Result<void, GitError>> {
Expand Down
10 changes: 5 additions & 5 deletions src/core/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,31 @@ export interface GitHubPullRequest {
};
}

export type CommitStatusState = "error" | "failure" | "pending" | "success";
type CommitStatusState = "error" | "failure" | "pending" | "success";

export interface CommitStatusOptions {
interface CommitStatusOptions {
state: CommitStatusState;
targetUrl?: string;
description?: string;
context: string;
}

export interface UpsertPullRequestOptions {
interface UpsertPullRequestOptions {
title: string;
body: string;
head?: string;
base?: string;
pullNumber?: number;
}

export interface UpsertReleaseOptions {
interface UpsertReleaseOptions {
tagName: string;
name: string;
body?: string;
prerelease?: boolean;
}

export interface GitHubRelease {
interface GitHubRelease {
id: number;
tagName: string;
name: string;
Expand Down
6 changes: 3 additions & 3 deletions src/core/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { logger, runIfNotDry } from "#shared/utils";
import { err, ok } from "#types";
import semver from "semver";

export interface NPMError {
interface NPMError {
type: "npm";
operation: string;
message: string;
Expand All @@ -15,7 +15,7 @@ export interface NPMError {
status?: number;
}

export interface NPMPackageMetadata {
interface NPMPackageMetadata {
"name": string;
"dist-tags": Record<string, string>;
"versions": Record<string, unknown>;
Expand Down Expand Up @@ -70,7 +70,7 @@ function getRegistryURL(): string {
* @param packageName - The package name (e.g., "lodash" or "@scope/name")
* @returns Result with package metadata or error
*/
export async function getPackageMetadata(
async function getPackageMetadata(
packageName: string,
): Promise<Result<NPMPackageMetadata, NPMError>> {
try {
Expand Down
42 changes: 0 additions & 42 deletions src/core/result-helpers.ts

This file was deleted.

16 changes: 8 additions & 8 deletions src/core/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import type { NormalizedReleaseScriptsOptions } from "../options";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { selectPackagePrompt } from "#core/prompts";
import { exitWithError } from "#shared/errors";
import { isCI, logger, run } from "#shared/utils";
import { getIsCI, logger, run } from "#shared/utils";
import { err, ok } from "#types";
import farver from "farver";

Expand Down Expand Up @@ -81,10 +80,11 @@ export async function discoverWorkspacePackages(
const missing = explicitPackages.filter((p) => !foundNames.has(p));

if (missing.length > 0) {
exitWithError(
`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`,
"Check your package names or run 'pnpm ls' to see available packages",
);
return err(toWorkspaceError(
"discoverWorkspacePackages",
`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}. `
+ `Check your package names or run 'pnpm ls' to see available packages`,
));
}
}

Expand All @@ -94,15 +94,15 @@ export async function discoverWorkspacePackages(
// 3. No explicit packages were specified (user didn't pre-select specific packages)
const isPackagePromptEnabled = options.prompts?.packages !== false;
logger.verbose("Package prompt gating", {
isCI,
isCI: getIsCI(),
isPackagePromptEnabled,
hasExplicitPackages: Boolean(explicitPackages),
include: workspaceOptions.include ?? [],
exclude: workspaceOptions.exclude ?? [],
excludePrivate: workspaceOptions.excludePrivate ?? false,
});

if (!isCI && isPackagePromptEnabled && !explicitPackages) {
if (!getIsCI() && isPackagePromptEnabled && !explicitPackages) {
const selectedNames = await selectPackagePrompt(workspacePackages);
workspacePackages = workspacePackages.filter((pkg) =>
selectedNames.includes(pkg.name),
Expand Down
38 changes: 25 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ReleaseResult } from "#types";
import type { WorkspacePackage } from "./core/workspace";
import type { ReleaseScriptsOptionsInput } from "./options";
import process from "node:process";
import { printReleaseError, ReleaseError } from "#shared/errors";
import { logger } from "#shared/utils";
import { prepareWorkflow as release } from "#workflows/prepare";
import { publishWorkflow as publish } from "#workflows/publish";
Expand All @@ -18,6 +20,16 @@ export interface ReleaseScripts {
};
}

function withErrorBoundary<T>(fn: () => Promise<T>): Promise<T> {
return fn().catch((e) => {
if (e instanceof ReleaseError) {
printReleaseError(e);
process.exit(1);
}
throw e;
});
}

export async function createReleaseScripts(options: ReleaseScriptsOptionsInput): Promise<ReleaseScripts> {
// Normalize options once for packages.list and packages.get
const normalizedOptions = normalizeReleaseScriptsOptions(options);
Expand All @@ -41,32 +53,32 @@ export async function createReleaseScripts(options: ReleaseScriptsOptionsInput):

return {
async verify(): Promise<void> {
return verify(normalizedOptions);
return withErrorBoundary(() => verify(normalizedOptions));
},

async prepare(): Promise<ReleaseResult | null> {
return release(normalizedOptions);
return withErrorBoundary(() => release(normalizedOptions));
},

async publish(): Promise<void> {
return publish(normalizedOptions);
return withErrorBoundary(() => publish(normalizedOptions));
},

packages: {
async list(): Promise<WorkspacePackage[]> {
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
if (!result.ok) {
throw new Error(result.error.message);
}
return result.value;
return withErrorBoundary(async () => {
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
if (!result.ok) throw new Error(result.error.message);
return result.value;
});
},

async get(packageName: string): Promise<WorkspacePackage | undefined> {
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
if (!result.ok) {
throw new Error(result.error.message);
}
return result.value.find((p) => p.name === packageName);
return withErrorBoundary(async () => {
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
if (!result.ok) throw new Error(result.error.message);
return result.value.find((p) => p.name === packageName);
});
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/operations/changelog-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface FormatCommitLineOptions {
authors: AuthorInfo[];
}

export function formatCommitLine({ commit, owner, repo, authors }: FormatCommitLineOptions): string {
function formatCommitLine({ commit, owner, repo, authors }: FormatCommitLineOptions): string {
const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
let line = `${commit.description}`;
const references = commit.references ?? [];
Expand Down
39 changes: 24 additions & 15 deletions src/shared/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import process from "node:process";
import { getIsVerbose } from "#shared/utils";
import farver from "farver";
import mri from "mri";

const args = mri(process.argv.slice(2));
const isVerbose = !!args.verbose;

type UnknownRecord = Record<string, unknown>;

Expand Down Expand Up @@ -71,7 +67,7 @@ function extractStderrLike(record: UnknownRecord): string | undefined {
return undefined;
}

export interface FormattedUnknownError {
interface FormattedUnknownError {
message: string;
stderr?: string;
code?: string;
Expand Down Expand Up @@ -148,12 +144,23 @@ export function formatUnknownError(error: unknown): FormattedUnknownError {
};
}

export function exitWithError(message: string, hint?: string, cause?: unknown): never {
console.error(` ${farver.red("✖")} ${farver.bold(message)}`);
export class ReleaseError extends Error {
readonly hint?: string;

constructor(message: string, hint?: string, cause?: unknown) {
super(message);
this.name = "ReleaseError";
this.hint = hint;
this.cause = cause;
}
}

export function printReleaseError(error: ReleaseError): void {
console.error(` ${farver.red("✖")} ${farver.bold(error.message)}`);

if (cause !== undefined) {
const formatted = formatUnknownError(cause);
if (formatted.message && formatted.message !== message) {
if (error.cause !== undefined) {
const formatted = formatUnknownError(error.cause);
if (formatted.message && formatted.message !== error.message) {
console.error(farver.gray(` Cause: ${formatted.message}`));
}

Expand All @@ -170,15 +177,17 @@ export function exitWithError(message: string, hint?: string, cause?: unknown):
console.error(farver.gray(` ${formatted.stderr}`));
}

if (isVerbose && formatted.stack) {
if (getIsVerbose() && formatted.stack) {
console.error(farver.gray(" Stack:"));
console.error(farver.gray(` ${formatted.stack}`));
}
}

if (hint) {
console.error(farver.gray(` ${hint}`));
if (error.hint) {
console.error(farver.gray(` ${error.hint}`));
}
}

process.exit(1);
export function exitWithError(message: string, hint?: string, cause?: unknown): never {
throw new ReleaseError(message, hint, cause);
}
1 change: 0 additions & 1 deletion src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { WorkspacePackage } from "#core/workspace";

export type BumpKind = "none" | "patch" | "minor" | "major";
export type GlobalCommitMode = false | "dependencies" | "all";

export interface CommitTypeRule {
/**
Expand Down
Loading
Loading