Skip to main content

AI does help me write code, but when I'm writing for you here, I only write my own words, not even using autofill. I would rather lose some time than lose your trust. - Scott

pacwich: The Official Launch

· 10 min read
Scott Morse
Creator of Pacwich | Principal Engineer and Founder of Smorsic Labs

I'm the developer of bun-workspaces, a package for monorepo tooling that works with Bun's workspaces.

In deciding to support npm and pnpm as package managers as well, bun-workspaces's name became obviously inappropriate if I were to continue development.

With that, I'm introducing pacwich! This is the official continuation of bun-workspaces with multi-package-manager support.

pacwich is built from bun-workspaces's core and is mostly backwards compatible. If you're an existing user, you can use the official migration guide and explore the docs, which have enhanced LLM integrations as well.

Read on to learn more about my motivations and strategy for the pivot.

# Global install with your preferred package manager
# The pacwich command will use a local install if available
bun add -g pacwich
pnpm add -g pacwich
npm install -g pacwich

# Local install in your project
bun add -d pacwich
pnpm add -D pacwich
npm install -D pacwich

The CLI is essentially backwards compatible, save for the fact that the system shell (sh/cmd.exe) is now the default for inline scripts over the Bun shell, since Bun is no longer guaranteed to be available due to Node and npm/pnpm support.

# Before (bun-workspaces):
alias bw="bunx bun-workspaces"
bw ls
bw run lint my-workspace "path:my-packages/**/*" --parallel=2
# Bun shell default for inline scripts:
bw run --inline "echo <workspaceName>" --shell=bun

# After (pacwich):
# You could alias "pw", but demonstrating the vanilla global install
pacwich ls
pacwich run lint my-workspace "path:my-packages/**/*" --parallel=2

# "system" now the default shell for inline scripts
# Pass --shell=bun to use Bun shell instead if Bun is available:
pacwich run --inline "echo <workspaceName>" --shell=bun

Some History

Since early in my career, I have found monorepos to be very satisfying to work with.

npm's workspaces feature was the first tooling I used for monorepos, but it felt very bare-bones.

For many years, I had an itch I wanted to scratch by making my own monorepo tooling that worked as directly with native tooling like as possible. I wanted it to be both lightweight and my own.

At some point after Bun's release, I started using it more and more until it became my package manager of choice for TypeScript projects.

I created bun-workspaces on a whim to get around a now irrelevant limitation of Bun's --filter feature. It gave me an excuse to start building the monorepo toolset I had wanted for years, and I noticed that I was getting more positive reactions from other devs than I expected as a no-name, but I didn't take the project very seriously until about a year later.

Why Change?

A Revelation of Abstraction

After months of building upon bun-workspaces, it went from only the CLI that gave you metadata about workspaces and ran scripts in series or parallel to a CLI and TS library with more powerful configuration and a rapidly solidifying set of concepts.

As bun-workspaces became more advanced, I noticed that its mental model was increasingly less specific to Bun as it started to grow as its own set of abstractions, such as "workspace patterns," its model for the affected graph, and more.

Workspace patterns used to select subsets of workspaces in various features and config:

"my-name-or-alias" # matches workspace name or alias
"my-name-pattern-*" # matches workspace names only by wildcard
"alias:my-alias-pattern-*" # matches workspace aliases by wildcard
"path:packages/**/*" # matches workspace paths by glob
"tag:my-tag" # matches workspaces with tag in config
"not:my-name-or-alias" # excludes workspace name or alias
"not:tag:my-tag-pattern-*" # excludes workspace tag matching wildcard
"re:^my-regex-pattern$" # matches workspace name or alias by regex
"not:tag:re:^my-tag-regex-pattern$" # excludes workspace tag by regex
"@root" # matches the root workspace

An Anti-Lockin Philosophy

Part of my philosophy for bun-workspaces was to make it relatively unopinionated and anti-lockin. However, its own namesake and description made it locked into one package manager.

Reflecting upon this, I dug deeper into whether bun-workspaces could retain all its present functionality while working with npm or pnpm workspaces instead.

No Time Like The Present

After confirming that this was a reasonable goal, I couldn't ignore the fact that if I wanted this, putting it off and continuing development of bun-workspaces would mean accruing technical debt and unknowns for this pivot.

On top of this, more feature additions to bun-workspaces would mean more potential migration pain for users, so I didn't want to waste any time. My bun-workspaces releases halted, where almost a month went by without a release instead of my usual weekly cadence.

I am still a fan of Bun's design as a package manager and think it handles workspaces and dependency catalogs very elegantly, and I still plan to use it personally.

However, I realized that decoupling my monorepo tooling from my package manager and runtime would provide greater freedom in development, and I could not ignore this realization.

Development Strategy

AI Assistance

I will get the elephant out of the room early here.

In a page on pacwich's security practices, I wrote at length about my approach to using AI tools for development of open source like this.

The quick summary is that I greatly value knowing my codebase and performing real manual review of code I'm releasing, and I want to make it clear that I have no desire to release risky vibe code in a package that deals with monorepo shell script orchestration and DevOps.

For this pivot, tools like Claude Code and the Windsurf IDE did assist my effort.

However, I refused to perform this launch until I had re-reviewed the entire source code, documentation, and tests myself. I can't stand to feel like I don't know a codebase of this importance inside and out.

I also want to clarify that I write the documentation found at https://pacwich.dev myself. I only use agents like Claude to review what I wrote to flag potential mistakes rather than write it for me. I personally would feel embarrassed if I couldn't describe my own spec, and this is a way to keep myself feeling like I am the one who truly leads the project, not one of my tools.

If I was trying to show off how fast I could pump this pivot out, I could have easily gotten it done in a week or less. I'm extremely glad that I didn't do this and instead spent a month ensuring it was ready enough.

Luckily, while this was a lot of work, the changes to source code aren't as dramatic as they might seem, meaning that my knowledge of bun-workspaces's source code, which I developed largely manually, carried over.

Things That Stayed

pacwich's own source monorepo retains almost exactly the same structure as bun-workspaces. Similarly, pacwich's package source code also retains the same general shape, with a large majority of modules carrying over.

In addition to this, almost all exisitng bun-workspaces tests remained. The overwhelming majority of test changes other than minor Bun specifics ended up being additions of coverage.

Given that pacwich is mostly backwards compatible with bun-workspaces, the similarities should only be natural, so despite its newness, it is far from brand new to me as its maintainer.

Actual Stages of Development

I'll describe my actual strategy that I took in performing this pivot, and you'll see this was by no means anything close to a one-shot.

Stage 1: Make source code compatible with NodeJS

One tradeoff of this pivot was that I could no longer rely on Bun builtins without introducing a layer of complexity to swap out builtins based on the runtime.

I decided the most straightforward path would be to simply make existing source code equally compatible in both Bun and Node without this split.

My goal here was to replace all Bun builtins with Node builtins that Bun also supported, until existing tests passed with almost no modificiations and the package seemed to function normally locally.

Bun.spawn -> child_process.spawn

Stage 2: Migrate the test runner

This is related to the last stage, since at this point, I was using Bun's test runner and therefore could not confirm tests passed when Node was actually used at the runtime.

I migrated tests to vitest and added scripts to run tests through Bun or Node. Actual test code generally did not meaningfully change besides Node support changes in utilities.

# before
bun test

# after
npx vitest # use Node.js runtime
bunx --bun vitest # force Bun runtime

Stage 3: Design an adapter layer to hide Bun details

Before jumping to npm and pnpm support, it made sense to first extract all behavior specific to Bun integration into a new layer that would abstract away the package manager.

This way I could prepare the multi-package support while confirming that the existing tests passed for what was still essentially bun-workspaces at this point.

parseBunLock() -> createAdapter("bun").parseLockfile()

Stage 4: Create a testing layer for the adapter layer

To further prepare for npm and pnpm support, it made sense to go ahead and add tests that would iterate over each supported pm to confrim conformancy of behavior, ensuring that behavior was the same across the CLI and API for the same set of test projects.

Stage 5: Add support for npm workspaces

Now that an adapter existed with conformancy tests, adding pm support just meant making a new adapter implementation.

This was the natural first choice due to npm's simplicity and the fact that its workspace configuration is very similar to Bun's, using package.json["workspaces"].

However, this addition inspired the verify feature used to help ensure workspace-to-workspace dependencies are explicitly defined in workspace package.json files, something npm does not require, unlike Bun and pnpm.

Stage 6: Add support for pnpm workspaces

This was the obvious next step, which mainly involved adding in the YAML parsing and logic needed to read pnpm-workspace.yaml and pnpm-lock.yaml.

// Hooray!
export const PACKAGE_MANAGER_NAMES = ["bun", "pnpm", "npm"] as const;
export type PackageManagerName = (typeof PACKAGE_MANAGER_NAMES)[number];

Stage 7: Desired deprecations and small breaking changes from bun-workspaces

Since this is the launch of a brand new package, I took the opportunity for some minor breaking cleanup items described in the migration guide.

Stage 8: Feature additions

Mainly due to npm's lack of requirement for explicit workspace dependencies, the verify feature was designed to help detect workspaces that imported/exported from workspaces not declared as dependencies in package.json.

AI integrations had some improvements, while most MCP tools were removed, since they were too redundant and limited compared to CLI, placing emphasis on using MCP resources to read the package overview and CLI docs.

A CLI feature to add skill files was added as a quick win for another means of providing docs to an agent.

Stage 9: Documentation and Final Manual Reviews

This was actually the longest of all these stages. As I described earlier, I rewrote the old bun-workspaces documentation website into pacwich.dev, myself, which was worth it.

In addition to multiple agent passes to help with review, I also conducted a full unassisted manual review of the codebase myself to regain confidence in my source code knowledge, since a lot of details had changed.

Even though I already reviewed as I went through each stage, this was one last deep dive where I caught code smells, adjusted code to match style and naming consistency, found concerns to refactor for DRYness and the like, polished tests, removed long comments where better naming would suffice, and more.

I also had a lot of other preparations to do, like writing this very blog post.

/** @todo plan changes for every single thing bun-workspaces ever touched */

Conclusion

If you have read this as someone who was already a fan of bun-workspaces, I hope that you feel as optimistic as I do that this is a net win for the project, as almost nothing has been lost from bun-workspaces while several new advantages not possible before have been gained thanks to runtime and package manager portability.

While pacwich is at 0.x at launch, I plan to guard stability as much as I can., treating it more like a 1.x or higher version without a very compelling reason for any breakage.