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
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.3] - 2026-06-03

### Fixed

- **Monorepo detection now works.** The directory walk previously exited after
checking only the starting directory, so running `omnes` from a sub-package
fell back to npm instead of finding the lockfile at the repository root.
Detection now walks up to the filesystem root.
- `--version` reads the version from `package.json` instead of a hardcoded
string that had drifted out of sync.
- `--help` and `--version` print to stdout instead of stderr, so they pipe and
redirect like other CLIs.
- A missing package manager now exits with code `127` (was `1`), matching the
documented API.

### Added

- Corepack `packageManager` field detection. An explicit
`"packageManager": "pnpm@9.x"` in `package.json` takes precedence over a
lockfile in the same directory.
- The informational `Using:` line now shows the fully resolved command
(e.g. `Using bun: bun run dev`).
- Unit tests covering traversal, Corepack detection, lockfile priority, and the
npm argument transform; Biome lint/format; and a GitHub Actions CI workflow.

### Changed

- Internal: pure detection and argument logic extracted into `src/lib.ts`, with
`src/omnes.ts` reduced to a thin executable entry point.
- Internal: `LOCKFILES` is an ordered tuple array rather than an object, so
detection priority is explicit instead of relying on object key order. No
behavior change.
- Internal: the supported package-manager list is a single `const` source of
truth, with the `PackageManager` type derived from it and a type guard
replacing a cast in Corepack-field parsing.

## [0.1.2] and earlier

Initial releases and documentation. See the git history for details.

[0.1.3]: https://github.com/darkroomengineering/omnes/releases/tag/v0.1.3
43 changes: 21 additions & 22 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";

// Supported package managers
export type PackageManager = "bun" | "pnpm" | "npm" | "yarn";

// Lockfile → package manager. Key order is the detection priority: the first
// match within a directory wins. Mirrors the table in the README.
export const LOCKFILES: Record<string, PackageManager> = {
"bun.lockb": "bun",
"bun.lock": "bun",
"pnpm-lock.yaml": "pnpm",
"yarn.lock": "yarn",
"package-lock.json": "npm",
};

const PACKAGE_MANAGERS: ReadonlySet<string> = new Set<PackageManager>([
"bun",
"pnpm",
"npm",
"yarn",
]);
// Supported package managers — single source of truth for the name list. The
// PackageManager type is derived from it so the two cannot drift.
export const PACKAGE_MANAGERS = ["bun", "pnpm", "npm", "yarn"] as const;
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];

function isPackageManager(value: string): value is PackageManager {
return (PACKAGE_MANAGERS as readonly string[]).includes(value);
}

// Lockfile → package manager, in detection-priority order: within a directory
// the first match wins. Mirrors the table in the README.
export const LOCKFILES: ReadonlyArray<readonly [string, PackageManager]> = [
["bun.lockb", "bun"],
["bun.lock", "bun"],
["pnpm-lock.yaml", "pnpm"],
["yarn.lock", "yarn"],
["package-lock.json", "npm"],
];

// npm built-in commands that don't require a "run" prefix
export const NPM_BUILTIN_COMMANDS = new Set([
Expand Down Expand Up @@ -128,8 +127,8 @@ export function parsePackageManagerField(
return undefined;
}
const name = value.split("@", 1)[0]?.trim();
if (name !== undefined && PACKAGE_MANAGERS.has(name)) {
return name as PackageManager;
if (name !== undefined && isPackageManager(name)) {
return name;
}
return undefined;
}
Expand Down Expand Up @@ -170,7 +169,7 @@ export function detectPackageManager(startDir: string): PackageManager {
return hint;
}

for (const [lockfile, pm] of Object.entries(LOCKFILES)) {
for (const [lockfile, pm] of LOCKFILES) {
if (existsSync(join(currentDir, lockfile))) {
return pm;
}
Expand Down
Loading