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
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ jobs:
uses: ./.github/actions/setup

- name: Build workspace packages
run: bun ws "packages/*" build
run: bun ws 'packages/*:build'

# Build each docs site variant so both are build-verified. Every variant
# writes to the same `.next/` dir (the per-site `distDir` in
# next.config.mjs is dev-only), so these run sequentially.
- name: Build docs (diffs, generates .source)
run: bun ws docs build
run: bun ws docs:build

- name: Build docs (trees)
run: NEXT_PUBLIC_SITE=trees bun ws docs build
run: NEXT_PUBLIC_SITE=trees bun ws docs:build

# diffshub is now its own standalone app rather than a docs site variant.
- name: Build diffshub
run: bun ws diffshub build
run: bun ws diffshub:build

- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
Expand Down Expand Up @@ -108,7 +108,7 @@ jobs:
name: build-output

- name: Run unit tests (all packages)
run: bun ws "packages/*" test --sequential
run: bun ws 'packages/*:test' --sequential

- name: Mount Playwright browsers
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
Expand All @@ -119,7 +119,7 @@ jobs:
run: bunx playwright@1.51.1 install --with-deps chromium

- name: Run tests - trees e2e
run: bun ws trees test:e2e
run: bun ws trees:test:e2e

typecheck:
name: TypeScript
Expand All @@ -138,7 +138,7 @@ jobs:
name: build-output

- name: Type check all workspaces
run: bun run ws "*" tsc
run: bun run ws :tsc

actions-pinned:
name: Actions pinned to SHA
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export AGENT=1
package intentionally needs its own range.
- Run commands from the monorepo root when they operate across the repo. Use
package directories for package-local scripts, or use
`bun ws <project> <task>` as the root shortcut when that fits the task.
`bun ws <project>:<task>` as the root shortcut when that fits the task.
- Preserve trailing newlines at the end of files.

## Skills
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build": "bun run build:deps && vite build",
"build:deps": "cd ../.. && bun ws \"packages/*\" build",
"build:deps": "cd ../.. && bun ws 'packages/*:build'",
"build-types": "bun run build:deps && tsgo --build",
"dev": "bun run build:deps && concurrently \"bun run dev:deps:diffs\" \"bun run dev:vite\" --names \"diffs,vite\" --prefix-colors \"blue,green\"",
"dev:deps:diffs": "(cd ../../packages/diffs && bun run dev)",
Expand Down
2 changes: 1 addition & 1 deletion apps/diffshub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"build": "bun run build:deps && bun run build:next",
"build:next": "next build",
"build:deps": "cd ../.. && bun ws \"packages/*\" build",
"build:deps": "cd ../.. && bun ws 'packages/*:build'",
"dev": "export PORT=$((${PIERRE_PORT_OFFSET:-0} + 3692)) && bash ../../scripts/run-dev.sh \"$PORT\" -- bun run _dev",
"prod": "export PORT=$((${PIERRE_PORT_OFFSET:-0} + 3692)) && bash ../../scripts/run-dev.sh \"$PORT\" -- bun run _prod",
"_dev": "bun run build:deps && bun concurrently \"bun run dev:deps:diffs\" \"bun run dev:deps:tree\" \"bun run dev:deps:truncate\" \"bun run dev:next\" --names \"diffs,trees,truncate,diffshub\" --prefix-colors \"blue,green,yellow,purple\"",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"build": "bun run build:deps && bun run generate-llms-txt && bun run build:next",
"build:next": "next build",
"build:deps": "cd ../.. && bun ws \"packages/*\" build",
"build:deps": "cd ../.. && bun ws 'packages/*:build'",
"diffs:dev": "export NEXT_PUBLIC_SITE=diffs PORT=$((${PIERRE_PORT_OFFSET:-0} + 3690)) && bash ../../scripts/run-dev.sh \"$PORT\" -- bun run _dev",
"trees:dev": "export NEXT_PUBLIC_SITE=trees PORT=$((${PIERRE_PORT_OFFSET:-0} + 3691)) && bash ../../scripts/run-dev.sh \"$PORT\" -- bun run _dev",
"_dev": "bun run build:deps && bun concurrently \"bun run dev:deps:diffs\" \"bun run dev:deps:tree\" \"bun run dev:deps:truncate\" \"bun run dev:next\" --names \"diffs,trees,truncate,docs\" --prefix-colors \"blue,green,yellow,purple\"",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
"scripts": {
"ws": "bun --silent scripts/ws.ts",
"wt": "bun --silent scripts/wt.ts",
"tsc": "bun run ws \"*\" tsc",
"tsc": "bun ws :tsc",
"clean": "bunx del-cli 'apps/*/.{next,source}' 'packages/*/dist' 'apps/*/tsconfig.tsbuildinfo' 'packages/*/tsconfig.tsbuildinfo'",
"clean:all": "bun run clean && bunx del-cli '**/node_modules'",
"icons:sprite": "node scripts/build-sprite.js && bun run format",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function parseArgs(argv: string[]): BenchmarkConfig {

function printHelpAndExit(): never {
console.log(
'Usage: bun ws diffs benchmark:parse-merge-conflict -- [options]'
'Usage: bun ws diffs:benchmark:parse-merge-conflict -- [options]'
);
console.log('');
console.log('Options:');
Expand Down
2 changes: 1 addition & 1 deletion packages/diffs/src/utils/parseMergeConflictDiffFromFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// bun test parseMergeConflictDiffFromFile
//
// Performance benchmark (checksum must match 33121550):
// bun ws diffs benchmark:parse-merge-conflict
// bun ws diffs:benchmark:parse-merge-conflict
//
// If you encounter a bug:
// 1. Add a new test case in test/parseMergeConflictDiffFromFile.test.ts with
Expand Down
16 changes: 8 additions & 8 deletions packages/path-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ Private engine types stay in `internal-types.ts` or implementation files.
Run these commands from the repository root:

```bash
bun ws path-store test
bun ws path-store tsc
bun ws path-store benchmark -- --preset mutation
bun ws path-store benchmark -- --preset cleanup
bun ws path-store benchmark -- --preset static
bun ws path-store benchmark:visible-tree-projection
bun ws path-store profile:demo
bun ws path-store profile:visible-tree-projection
bun ws path-store:test
bun ws path-store:tsc
bun ws path-store:benchmark -- --preset mutation
bun ws path-store:benchmark -- --preset cleanup
bun ws path-store:benchmark -- --preset static
bun ws path-store:benchmark:visible-tree-projection
bun ws path-store:profile:demo
bun ws path-store:profile:visible-tree-projection
```
2 changes: 1 addition & 1 deletion packages/path-store/scripts/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ function parseArgs(argv: readonly string[]): BenchmarkCliOptions {
}

if (argument === '--help') {
console.log('Usage: bun ws path-store benchmark -- [options]');
console.log('Usage: bun ws path-store:benchmark -- [options]');
console.log('');
console.log('Options:');
console.log(
Expand Down
2 changes: 1 addition & 1 deletion packages/path-store/scripts/profileDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ const AGGREGATE_METRIC_DEFINITIONS: Array<{
];

function printHelpAndExit(): never {
console.log('Usage: bun ws path-store profile:demo -- [options]');
console.log('Usage: bun ws path-store:profile:demo -- [options]');
console.log('');
console.log(
'Assumes Chrome is already running with --remote-debugging-port enabled.'
Expand Down
2 changes: 1 addition & 1 deletion packages/trees/scripts/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function parseArgs(argv: readonly string[]): BenchmarkCliOptions {
}

if (argument === '--help') {
console.log('Usage: bun ws trees benchmark -- [options]');
console.log('Usage: bun ws trees:benchmark -- [options]');
console.log('');
console.log('Options:');
console.log(
Expand Down
2 changes: 1 addition & 1 deletion packages/trees/scripts/profileFileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ const AGGREGATE_METRIC_DEFINITIONS: Array<{
const INTEGER_FORMATTER = new Intl.NumberFormat('en-US');

function printHelpAndExit(): never {
console.log('Usage: bun ws trees profile:file-tree -- [options]');
console.log('Usage: bun ws trees:profile:file-tree -- [options]');
console.log('');
console.log(
'Assumes Chrome is already running with --remote-debugging-port enabled.'
Expand Down
4 changes: 2 additions & 2 deletions packages/trees/test/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ If a unit test can prove behavior, write a unit test instead.
From repo root:

```bash
bun ws trees test
bun ws trees test:e2e
bun ws trees:test
bun ws trees:test:e2e
```

`test:e2e` automatically:
Expand Down
2 changes: 1 addition & 1 deletion packages/trees/test/file-tree-profile-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ test('profile:file-tree CLI help advertises the expected workload/render workflo

expect(result.exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain('bun ws trees profile:file-tree');
expect(stdout).toContain('bun ws trees:profile:file-tree');
expect(stdout).toContain('linux-5x');
expect(stdout).toContain('file-tree-profile.html');
expect(stdout).toContain('starts `bun run chrome` automatically');
Expand Down
34 changes: 21 additions & 13 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ most common workflows.

## `bun ws` — the workspace script runner

`bun ws <package> <script> [args…]` runs an npm script inside a specific
`bun ws <package>:<script> [args…]` runs an npm script inside a specific
workspace. It exists for three reasons:

1. You can run it from anywhere in the monorepo — you don't have to `cd` into
Expand All @@ -35,16 +35,24 @@ workspace. It exists for three reasons:
### Syntax

```bash
bun ws <package> <script> [args...]
bun ws <package> <script> --verbose # don't elide lines in output
bun ws 'packages/*' <script> # fan out across a glob
bun ws '*' <script> # every workspace (apps + packages)
bun ws <package>:<script> [args...]
bun ws <package>:<script> --verbose # don't elide lines in output
bun ws 'packages/*:<script>' # fan out across a glob
bun ws ':<script>' # every workspace that defines it
```

`ws` forwards every argument after the script name to the underlying `bun run`
The target is a single `<package>:<script>` token. The package part comes before
the first colon; everything after it is the script name (so script names that
contain colons, like `trees:dev` → `docs:trees:dev`, stay intact). An empty
package part (`:test`) fans out to every workspace, equivalent to `'*:test'`.

`ws` forwards every argument after the target to the underlying `bun run`
invocation. You do **not** need `--` to separate them unless a downstream tool
requires it. The only flag `ws` itself eats is `-v` / `--verbose`.

When fanning out across a glob or the empty package part, `ws` runs the script
only in the workspaces that actually define it and silently skips the rest.

### Package name resolution

In priority order:
Expand All @@ -57,11 +65,11 @@ In priority order:
### Examples

```bash
bun ws diffs build # build packages/diffs
bun ws docs trees:dev # run the docs trees dev server
bun ws trees test # bun test in packages/trees
bun ws 'packages/*' build # build every package
bun ws '*' tsc # typecheck everything
bun ws diffs:build # build packages/diffs
bun ws docs:trees:dev # run the docs trees dev server
bun ws trees:test # bun test in packages/trees
bun ws 'packages/*:build' # build every package
bun ws :tsc # typecheck everything
```

### How `ws` interacts with worktrees
Expand Down Expand Up @@ -271,7 +279,7 @@ no central registry file.
PIERRE_WORKTREE_SLUG=drag-drop-fix
PIERRE_PORT_OFFSET=30
```
2. When you run `bun ws docs trees:dev`, `ws` walks up from your cwd, finds
2. When you run `bun ws docs:trees:dev`, `ws` walks up from your cwd, finds
`.env.worktree`, and injects its keys into the child env.
3. The package.json script itself uses shell arithmetic to derive the final
port:
Expand Down Expand Up @@ -326,7 +334,7 @@ permissions dialog hadn't finished resolving yet.
```bash
bun run wt new drag-drop-fix
cd ~/pierre/pierre-worktrees/drag-drop-fix
bun ws docs trees:dev
bun ws docs:trees:dev
# → serves on http://localhost:<3691 + offset>, tab title prefixed with
# an emoji + "[drag-drop-fix]"
```
Expand Down
82 changes: 65 additions & 17 deletions scripts/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,26 @@ if (runModeFlag) {
args.splice(args.indexOf(runModeFlag), 1);
}

const [pkgArg, ...scriptArgs] = args;
// The first positional argument is the `<workspace>:<script>` target; every
// argument after it is forwarded verbatim to the underlying script.
const [target, ...scriptArgs] = args;

if (!pkgArg || scriptArgs.length === 0) {
console.log('Usage: bun ws <package> <script> [args...] [--verbose]');
function printUsage() {
console.log('Usage: bun ws <workspace>:<script> [args...] [--verbose]');
console.log('');
console.log('Filters:');
console.log('Target:');
console.log(' <workspace>:<script> Run <script> in one workspace');
console.log(
' :<script> Run <script> in every workspace that defines it'
);
console.log('');
console.log('Workspace part:');
console.log(' <name> Matches @pierre/<name> in package.json');
console.log(' packages/<name> Matches directory on filesystem');
console.log(' apps/<name> Matches directory on filesystem');
console.log(' packages/* Every package (glob)');
console.log(' apps/* Every app (glob)');
console.log(' * (or empty) Every workspace');
console.log('');
console.log('Options:');
console.log(' -v, --verbose Show full output (no line elision)');
Expand All @@ -333,16 +344,52 @@ if (!pkgArg || scriptArgs.length === 0) {
);
console.log('');
console.log('Examples:');
console.log(' bun ws diffs build');
console.log(' bun ws diffs test');
console.log(' bun ws diffs test --verbose # full output');
console.log(' bun ws packages/diffs build # path-based');
console.log(" bun ws 'packages/*' test # all packages");
console.log(" bun ws 'apps/*' dev # all apps");
console.log(" bun ws '*' build # all workspaces");
console.log(' bun ws diffs:build');
console.log(' bun ws diffs:test');
console.log(' bun ws diffs:test --verbose # full output');
console.log(' bun ws packages/diffs:build # path-based');
console.log(" bun ws 'packages/*:test' # all packages");
console.log(" bun ws 'apps/*:dev' # all apps");
console.log(
' bun ws :test # every workspace with test'
);
console.log(
" bun ws '*:build' # every workspace (explicit)"
);
}

if (!target) {
printUsage();
process.exit(0);
}

// Targets are `<workspace>:<script>`. Splitting on the first colon keeps script
// names that themselves contain colons intact (e.g. `docs:trees:dev`,
// `:test:e2e`). An empty workspace part (`:test`) fans out to every workspace.
const colonIndex = target.indexOf(':');
if (colonIndex === -1) {
console.error(
`Invalid target "${target}". The ws syntax is now <workspace>:<script>.`
);
const suggestedScript = scriptArgs.find((arg) => !arg.startsWith('-'));
if (suggestedScript) {
console.error(`Did you mean: bun ws ${target}:${suggestedScript}`);
} else {
console.error(`For example: bun ws ${target}:build`);
}
process.exit(1);
}

const pkgArg = target.slice(0, colonIndex);
const scriptName = target.slice(colonIndex + 1);

if (!scriptName) {
console.error(
`Missing script name in "${target}". Expected <workspace>:<script>.`
);
process.exit(1);
}

// Resolve package directory for direct execution
function resolvePackageDir(pkg: string): string | null {
// Check if it's already a path
Expand All @@ -358,11 +405,13 @@ function resolvePackageDir(pkg: string): string | null {
return null;
}

// Glob patterns (e.g. '*', 'packages/*') must use bun run -F
if (pkgArg.includes('*')) {
// An empty workspace part (`:test`) or a glob (`*`, `packages/*`) fans out via
// `bun run -F`, which already runs the script only in the workspaces that
// define it and silently skips the rest.
if (pkgArg === '' || pkgArg.includes('*')) {
const isPath = pkgArg.startsWith('packages/') || pkgArg.startsWith('apps/');
let filter: string;
if (pkgArg === '*') {
if (pkgArg === '' || pkgArg === '*') {
filter = '*';
} else if (isPath) {
filter = `./${pkgArg}`;
Expand All @@ -376,7 +425,7 @@ if (pkgArg.includes('*')) {

const proc = spawn(
'bun',
['run', '-F', filter, ...outputFlags, ...scriptArgs],
['run', '-F', filter, ...outputFlags, scriptName, ...scriptArgs],
{
stdio: 'inherit',
cwd,
Expand All @@ -394,15 +443,14 @@ if (pkgArg.includes('*')) {

const pkgJsonPath = resolve(pkgDir, 'package.json');
const pkgJson = JSON.parse(await Bun.file(pkgJsonPath).text());
const scriptName = scriptArgs[0];
const scriptCmd = pkgJson.scripts?.[scriptName];

if (!scriptCmd) {
console.error(`Script "${scriptName}" not found in ${pkgArg}/package.json`);
process.exit(1);
}

const restArgs = scriptArgs.slice(1); // args after script name (e.g., -- --update-snapshots)
const restArgs = scriptArgs; // forwarded args (e.g., -- --update-snapshots)

// If the script contains shell operators, run via shell
const needsShell = /&&|\|\||[|;]/.test(scriptCmd);
Expand Down
Loading