ApexLibrarian

Apex Stem Docs
Apex StemApexLibrariannpmCLISalesforce
An npm CLI that shelves Apex classes into the directories declared by their @directory doc-comment tag — restoring your repository layout after any retrieve, because orgs strip it.

A librarian for your Apex classes — it picks the books up off the floor and puts them back on their shelves.

Salesforce orgs have no concept of folders for Apex classes. Your repository does — Usecases/opportunity/, Handlers/account/, tests/ — but the moment a class travels through an org (a teammate creates it in Setup, you retrieve it into another repo, you install it as a package and pull the source), that layout is stripped and the file lands flat in classes/.

ApexLibrarian is an npm CLI that tunnels the layout through the org by declaring it inside the class itself, in the doc comment:

APEX
/**
 * Recalculates opportunity discounts from line items.
 * @directory Usecases/opportunity
 */
public with sharing class ReconcileDiscount {

Doc comments are part of the class body, so the @directory tag survives every round trip. After a retrieve, one command shelves everything where it belongs.

Install

BASH
npm install --save-dev apex-librarian
# or run it directly
npx apex-librarian check

Pin it as a devDependency: a tool that moves files should behave identically for every teammate and in CI.

Commands

The tag is the declaration, the filesystem is the reality. Each command resolves divergence in one direction:

CommandDirectionUse it when
apex-librarian checkcompare onlyCI / pre-commit — exits 1 on any divergence
apex-librarian arrangetag → locationafter sf project retrieve — moves files (and their .cls-meta.xml) to the tagged directory
apex-librarian stamplocation → tagadopting an existing project, or after a deliberate git mv

Options

CODE
--root <dir>     class root to operate on (repeatable; default: every "classes" directory found)
--dry-run        print what would happen without touching any file
--require-tag    check only: untagged classes in subdirectories also fail
--include-submodules
                 stamp/check: also operate on files inside git submodules (see "For framework authors")

Behavior notes

  • .cls-meta.xml always moves together with its class.
  • Git submodules are excluded automatically (paths from .gitmodules). A vendored framework's layout is upstream's business — the librarian never moves or stamps files inside one.
  • arrange never overwrites: an occupied target is reported and skipped (exit 1).
  • The tag binds to the doc block attached to the top-level type declaration. License headers at the top of the file don't count; inner classes are ignored.
  • @directory . means the class root itself. Absolute paths and .. are rejected.
  • Untagged classes are left alone by arrange and check (add --require-tag to enforce tagging); stamp adds tags to classes already living in subdirectories.
  • Run it on a committed working tree when you can — every move is a plain rename, so git status shows exactly what the librarian did, and git checkout . undoes it.

Typical flows

Adopting an existing project

Write the current layout into every class once:

BASH
npx apex-librarian stamp
git add -A && git commit -m "Stamp @directory tags"

After a retrieve

New classes created by teammates in the org arrive flat; shelve them:

BASH
sf project retrieve start -o my-org ...
npx apex-librarian arrange

After moving a class on purpose

Tell the tag about it:

BASH
git mv force-app/main/default/classes/Foo.cls force-app/main/default/classes/Services/
npx apex-librarian stamp

CI example (GitHub Actions)

YAML
- uses: actions/setup-node@v4
  with:
    node-version: 20
- run: npm ci
- run: npx apex-librarian check --require-tag

For framework authors

If you publish a framework as an unlocked package, consumers who retrieve your classes get them flat — the org strips all layout. Bake the destination into your source instead. Work from a checkout where your framework is installed the way you intend consumers to place it — as a git submodule under classes/<YourFramework>/ — and stamp the real hierarchy as-is:

BASH
npx apex-librarian stamp --include-submodules
npx apex-librarian check --include-submodules   # verify

Every class now carries its actual location, e.g. @directory MyFramework/Eloquents/tests. Commit those tags in the submodule and release. A consumer who installs your package and retrieves the source just runs npx apex-librarian arrange — and your classes shelve themselves under classes/MyFramework/..., exactly where a submodule install puts them. Same layout on every install path, declared by the classes themselves.

--include-submodules is a separate flag because submodule paths are excluded by default: an ordinary consumer must never re-stamp or move a vendored framework's files. arrange refuses the flag outright — moving a file out of a submodule's working tree would corrupt the submodule.

All four Apex Stem frameworks ship with these tags (as of ApexEloquent v3.7.0 / ApexBlueprint v2.1.0 / ApexTrace v1.5.0 / ApexTools v1.1.0) — see the installation guide.

  • GitHub: krile136/ApexLibrarian
  • npm: apex-librarian
  • Library usage: everything the CLI does is exported from apex-librarian as plain functions (analyze, planArrange, applyMoves, ...), so it can be embedded in other tooling