> ## Documentation Index
> Fetch the complete documentation index at: https://better-pm.fdarian.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspace Navigation

> How pm cd and pm pls enable fast navigation across workspace packages

Better PM provides two commands for discovering and navigating workspace packages: `pm pls` for listing packages and `pm cd` for jumping to package directories.

## Package Listing with pm pls

The `pm pls` command displays all workspace packages in a tree structure:

```bash theme={null}
pm pls
├── packages/
│   ├── core "@myapp/core"
│   └── utils "@myapp/utils"
└── apps/
    └── web "@myapp/web"
```

### How It Works

The `pls` command is remarkably simple:

```typescript theme={null}
// From src/commands/pls.ts:8-17
export const plsCmd = cli.Command.make('pls', {}, () =>
  Effect.gen(function* () {
    const pm = yield* PackageManagerService;
    const path = yield* Path.Path;
    const packages = yield* pm.listWorkspacePackages(pm.lockDir);
    for (const line of formatWorkspaceTree(packages, path.sep)) {
      yield* Console.log(line);
    }
  }).pipe(Effect.provide(PackageManagerLayer)),
);
```

<Steps>
  <Step title="List workspace packages">
    The package manager service discovers all workspace packages by reading the workspace configuration (`pnpm-workspace.yaml` or `package.json` workspaces field) and enumerating directories.
  </Step>

  <Step title="Format as tree">
    The `formatWorkspaceTree` function groups packages by their parent directory and formats them with box-drawing characters.
  </Step>

  <Step title="Display to console">
    Each line of the formatted tree is printed to stdout.
  </Step>
</Steps>

### Tree Formatting Logic

The tree structure is generated by the `formatWorkspaceTree` function:

```typescript theme={null}
// From src/lib/format-workspace-tree.ts:1-34
export const formatWorkspaceTree = (
  packages: Array<{ name: string; relDir: string }>,
  sep: string,
) => {
  const grouped = new Map<string, Array<{ name: string; dirName: string }>>();
  for (const pkg of packages) {
    const parts = pkg.relDir.split(sep);
    const group = parts[0];
    const dirName = parts.slice(1).join(sep);
    if (!grouped.has(group)) {
      grouped.set(group, []);
    }
    grouped.get(group)!.push({ name: pkg.name, dirName });
  }

  const lines: Array<string> = [];
  const groupEntries = Array.from(grouped.entries());
  for (let gi = 0; gi < groupEntries.length; gi++) {
    const [group, entries] = groupEntries[gi];
    const isLastGroup = gi === groupEntries.length - 1;
    const groupPrefix = isLastGroup ? '└── ' : '├── ';
    const childIndent = isLastGroup ? '    ' : '│   ';
    lines.push(`${groupPrefix}${group}/`);
    for (let ei = 0; ei < entries.length; ei++) {
      const entry = entries[ei];
      const isLastEntry = ei === entries.length - 1;
      const entryPrefix = isLastEntry ? '└── ' : '├── ';
      lines.push(
        `${childIndent}${entryPrefix}${entry.dirName} "${entry.name}"`,
      );
    }
  }
  return lines;
};
```

The function:

1. Groups packages by their top-level directory (e.g., `packages/`, `apps/`)
2. Uses Unicode box-drawing characters (`├──`, `└──`, `│`) to create the tree structure
3. Shows both the directory name and package name for each entry

<Info>
  The same `formatWorkspaceTree` function is used in multiple places:

  * `pm pls` output
  * Root install warnings
  * Package not found error messages
</Info>

## Directory Navigation with pm cd

The `pm cd` command allows you to jump directly to any workspace package:

```bash theme={null}
pm cd @myapp/web    # cd to apps/web
pm cd               # cd to monorepo root
```

### Command Implementation

```typescript theme={null}
// From src/commands/cd.ts:22-57
export const cdCmd = cli.Command.make(
  'cd',
  { packageName: packageNameArg, completions: completionsOption, path: pathOption },
  (args) =>
    Effect.gen(function* () {
      const pm = yield* PackageManagerService;
      const path = yield* Path.Path;
      const packages = yield* pm.listWorkspacePackages(pm.lockDir);

      if (args.completions) {
        yield* Effect.forEach(packages, (pkg) => Console.log(pkg.name), {
          concurrency: 'unbounded',
        });
        return;
      }

      if (Option.isNone(args.packageName)) {
        yield* Console.log(pm.lockDir);
        return;
      }

      const packageName = args.packageName.value;
      const pkg = packages.find((p) => p.name === packageName);

      if (!pkg) {
        return yield* Effect.fail(
          new PackageNotFoundError(
            packageName,
            formatWorkspaceTree(packages, path.sep),
          ),
        );
      }

      yield* Console.log(path.resolve(pm.lockDir, pkg.relDir));
    }).pipe(Effect.provide(PackageManagerLayer)),
);
```

<Steps>
  <Step title="List all packages">
    The command loads all workspace packages to search through.
  </Step>

  <Step title="Handle special cases">
    * If `--completions` flag is present, output all package names for shell completion
    * If no package name is provided, output the monorepo root directory
  </Step>

  <Step title="Find the target package">
    Search for a package matching the provided name.
  </Step>

  <Step title="Output the absolute path">
    Print the resolved absolute path to stdout. The shell wrapper intercepts this and performs the actual directory change.
  </Step>
</Steps>

<Warning>
  The `pm cd` command by itself only prints a path. It requires shell integration to actually change directories.
</Warning>

## Shell Integration Mechanism

Better PM can't change your shell's working directory directly (child processes can't modify parent state). Instead, it uses a shell wrapper function:

### Activation

Add this to your `.zshrc` or `.bashrc`:

```bash theme={null}
eval "$(pm activate zsh)"  # or bash
```

### The Shell Wrapper

The `pm activate` command outputs a shell function that wraps the `pm` binary:

```bash theme={null}
# From src/commands/activate.ts:6-22
pm() {
  if [ "$1" = "cd" ]; then
    shift;
    for arg in "$@"; do
      case "$arg" in
        -*) command pm cd "$@"; return;;
      esac;
    done;
    local dir;
    dir=$(command pm cd "$@");
    if [ $? -eq 0 ] && [ -d "$dir" ]; then
      builtin cd "$dir";
    fi;
  else
    command pm "$@";
  fi;
};
```

**How it works:**

<Steps>
  <Step title="Intercept pm cd calls">
    When you run `pm cd <package>`, the shell function intercepts it instead of calling the binary directly.
  </Step>

  <Step title="Handle flag-only invocations">
    If the arguments contain flags (like `--completions` or `--path`), pass through to the binary without changing directories.
  </Step>

  <Step title="Capture the output path">
    Execute `command pm cd "$@"` and capture the output (the package directory path).
  </Step>

  <Step title="Change directory">
    If the command succeeded and the output is a valid directory, use `builtin cd` to change the shell's working directory.
  </Step>

  <Step title="Pass through other commands">
    For any command other than `cd`, call the `pm` binary directly.
  </Step>
</Steps>

<Tip>
  The wrapper uses `command pm` to call the actual `pm` binary, avoiding infinite recursion with the wrapper function.
</Tip>

## Autocomplete Functionality

Better PM provides shell completion for package names:

### Zsh Completions

```bash theme={null}
# From src/commands/activate.ts:24-34
eval "$(command pm --completions zsh)";
if (( $+functions[_pm_zsh_completions] )); then
  functions[_pm_zsh_completions_base]=$functions[_pm_zsh_completions];
  _pm_zsh_completions() {
    if [[ $words[2] == cd ]] && (( CURRENT == 3 )); then
      compadd -- ${(f)"$(command pm cd --completions 2>/dev/null)"};
    else
      _pm_zsh_completions_base "$@";
    fi;
  };
fi;
```

### Bash Completions

```bash theme={null}
# From src/commands/activate.ts:36-44
eval "$(command pm --completions bash)";
_pm_custom_completions() {
  if [[ "${COMP_WORDS[1]}" == "cd" ]] && [[ $COMP_CWORD -eq 2 ]]; then
    COMPREPLY=($(compgen -W "$(command pm cd --completions 2>/dev/null)" -- "${COMP_WORDS[$COMP_CWORD]}"));
    return;
  fi;
  _pm_bash_completions;
};
complete -F _pm_custom_completions -o nosort -o bashdefault -o default pm;
```

**How autocomplete works:**

<Steps>
  <Step title="Detect pm cd context">
    The completion function checks if the current command is `pm cd` and the cursor is on the package name argument.
  </Step>

  <Step title="Fetch package names">
    Run `pm cd --completions` to get a list of all workspace package names.
  </Step>

  <Step title="Generate completions">
    Pass the package names to the shell's completion system (`compadd` for zsh, `compgen` for bash).
  </Step>

  <Step title="Fall back to base completions">
    For any other command, use the default CLI completions generated by Effect CLI.
  </Step>
</Steps>

### Completions Mode

When `pm cd --completions` is called, it outputs one package name per line:

```typescript theme={null}
// From src/commands/cd.ts:31-35
if (args.completions) {
  yield* Effect.forEach(packages, (pkg) => Console.log(pkg.name), {
    concurrency: 'unbounded',
  });
  return;
}
```

<Info>
  The `--completions` flag is hidden from normal help output and is only used by the shell completion functions.
</Info>

## Package Discovery

Both `pm pls` and `pm cd` use the same underlying package discovery mechanism:

1. **Detect package manager** - Find the lock file and determine if it's pnpm, bun, or npm
2. **Read workspace config** - Load workspace globs from `pnpm-workspace.yaml` or `package.json`
3. **Enumerate directories** - Expand glob patterns and find all package directories
4. **Read package names** - Parse each `package.json` to get the official package name
5. **Return structured data** - Provide an array of `{ name: string; relDir: string }` objects

This ensures consistency between listing and navigation - you can always `pm cd` to any package shown in `pm pls`.

<CardGroup cols={2}>
  <Card title="Fast Navigation" icon="rocket">
    Jump to any package from anywhere in the monorepo without manually typing paths or using fuzzy finders.
  </Card>

  <Card title="Autocomplete Support" icon="keyboard">
    Tab completion for package names makes navigation even faster and prevents typos.
  </Card>

  <Card title="Visual Discovery" icon="tree">
    The tree view in `pm pls` helps you understand your monorepo structure at a glance.
  </Card>

  <Card title="Error Guidance" icon="circle-info">
    If you try to cd to a non-existent package, Better PM shows the full package tree to help you find the right name.
  </Card>
</CardGroup>
