Skip to Content
ScriptingMonorepos

Monorepos

Miso has first-class support for monorepos. Workspace membership is auto-detected — independent of the repo setting — from a pnpm-workspace.yaml (checked first) or a workspaces field in your root package.json. When members are found, you can run workspace-scoped scripts using the verb-first miso <script> @scope syntax, and Miso will automatically scope commands based on your current directory.

Setup

1. Add a workspaces field to your root package.json:

>package.json
{ "name": "my-monorepo", "packageManager": "pnpm", "workspaces": [ "apps/*", "packages/*" ] }

The workspaces field uses standard glob patterns and is compatible with npm, yarn, pnpm, and bun — no extra tooling required.

Alternatively, a pnpm-workspace.yaml with a packages: list works the same way and takes priority when both are present:

>pnpm-workspace.yaml
packages: - "apps/*" - "packages/*"

2. Leave repo unset (or set it to "miso"). Workspace discovery happens automatically — no monorepo-specific repo mode is required. Native fan-out and @scope runs execute through Miso’s TUI — see Running Workspace Scripts. Set repo to "turbo" or "nx" only if you want to delegate orchestration to one of those tools instead.

Workspace Scripts

Each workspace can have its own scripts/ folder. Miso discovers and executes scripts from the correct workspace, and sets the working directory to that workspace’s root so all relative paths resolve correctly.

Folder Structure

my-monorepo/ ├── miso.json ├── package.json ├── scripts/ ← root-level scripts │ └── build.sh ├── apps/ │ ├── api/ │ │ └── scripts/ ← workspace-scoped scripts │ │ ├── build.sh │ │ └── deploy.sh │ └── web/ │ └── scripts/ │ ├── build.sh │ └── dev.sh └── packages/ └── ui/ └── scripts/ └── build.sh

Running Workspace Scripts

From the Root — miso <script> @scope

Target one or more workspaces’ scripts from the project root by appending an @scope after the script name:

miso build @api # runs apps/api/scripts/build.sh miso dev @web # runs apps/web/scripts/dev.sh miso build @ui # runs packages/ui/scripts/build.sh

The @ sigil is required — it’s what tells Miso a token is a scope rather than a passthrough argument.

Pass more than one scope to run the same script across several workspaces:

miso dev @web @api # runs dev in both apps/web and apps/api

Anything after -- is passed straight through to the underlying script and is never treated as a scope:

miso test @web -- --watch

An explicit @scope is always honored or Miso fails — if it doesn’t resolve to a workspace, or the target isn’t something Miso can scope, the command exits with an error rather than quietly running unscoped.

With repo unset or "miso", @scope execution runs through Miso’s TUI: set tui to "tabbed" or "merged" and run from an interactive terminal, or the command fails rather than running unscoped. Delegated repos (repo: "turbo" or "nx") scope through the delegate’s own filter (--filter / --projects) and need no TUI.

The old prefixed form, miso @workspace/script, has been removed in favor of miso <script> @scope.

Scoping needs a package manager. In simple mode ("packageManager": false), workspace discovery is disabled and @scope is passed through to the script as a literal argument instead of being resolved.

From Inside a Workspace

Miso resolves scripts against the nearest miso.json, walking up from your current directory. So when a workspace carries its own miso.json, running Miso from inside it targets that member — no @scope needed:

cd apps/api # apps/api has its own miso.json miso build # runs apps/api/scripts/build.sh miso deploy # runs apps/api/scripts/deploy.sh

A member without its own miso.json resolves against the repo root instead — target it from the root with miso build @api.

Root Is the Orchestrator

When workspace members exist, the root is not a fan-out member. Root package.json scripts are entry points ("dev": "miso dev" is safe — it never gets shadowed by member fan-out), and root-owned work is addressed explicitly with #:

FormResolves against
namethe scope declaring it — a root list resolves at root, a member list resolves at that member
#namethe repo root, explicitly, regardless of declaring scope
@member/namethe named workspace member, regardless of declaring scope

A root task’s own list spawns once per run, no matter how many members fan out. That’s how you fan every member’s dev out while bringing up a single root Compose stack alongside it:

>miso.json
{ "repo": { "tasks": { "dev": { "concurrent": ["#db/up"] } } } }

Each member’s own miso.json declares its own companions, resolved inside that member — a root task’s concurrent list never broadcasts into members.

Per-Workspace miso.json

A workspace member can carry its own miso.json. When miso runs a task scoped to that member, the member’s config overlays the root config for these fields: scripts, shell, flags, tui, and tasks. Any field the member omits falls back to the root value.

Two things are not taken from a member’s config:

  • repo — orchestration mode is a root-level decision, so a member’s repo field is ignored. Members are leaves, not orchestrators.
  • env — env entries scope by file location, not by overlay. A member’s env entries apply only to that member — see env scope.

How Workspace Matching Works

A @scope token resolves by identity in priority order — the first tier with a match wins, so a workspace’s own name is never shadowed by an unrelated one that merely shares a folder or path segment:

  1. Full package.json name — the name field (e.g. @myorg/api, or a bare api)
  2. Scoped short-name — the segment after the last / in a scoped name (@myorg/apiapi)
  3. Relative path from root — the full path from the repo root (e.g. packages/api)
  4. Directory basename — the workspace folder’s name, used only when nothing above matches

Because name identity wins, @web resolves to the workspace named web even if another workspace lives in a folder called web.

So for a workspace at packages/api with "name": "@myorg/api" in its package.json, all of these resolve to it:

miso build @myorg/api # full package.json name miso build @api # scoped short-name miso build @packages/api # relative path

If two workspaces match within the same tier (e.g. two both named api), Miso exits with an error listing their paths so you can qualify by path:

scope "@api" is ambiguous — matches apps/api, packages/api; qualify by path (e.g. @apps/api)

If no workspace matches, Miso exits with an error listing the available scopes:

scope "@typo" matched no workspace (available: @myorg/api, @myorg/web, @myorg/ui)

Migrating to 0.7.0

  • Root scripts no longer preempt member fan-out. If a root script was deliberately shadowing a member fan-out, declare repo.tasks.<name> or address it with #name instead.
  • concurrent entries that resolve nowhere now fail the launch instead of being silently skipped.
  • A name claimed by both turbo.json and a scripts-folder file now errors instead of the script silently winning. Root package.json entry points ("build": "turbo run build") are unaffected.
  • Nx repos: Miso only parses turbo.json for delegated pipeline tasks, so in nx mode every non-task name now runs under Miso’s own orchestration and chrome rather than falling through to nx directly. Set "tui": "off" to get plain output back.
  • Plain scripts in turbo/nx repos now run under Miso’s single-pane TUI; "tui": "off" restores plain output there too.
Last updated on