---
title: Library
description: scan and serve from code, what they return, their errors, and the types @foundry.sh/spyglass exports.
---

The CLI's scans and server, called from code. A program scans a workspace,
reads what came back, and serves it, with the same results the CLI prints.

```ts
import { scan, serve } from "@foundry.sh/spyglass";

const everything = await scan("./my-workspace"); // workspace and every complete package's internals
const workspace = await scan.workspace("./my-workspace"); // workspace level only
const db = await scan.package("./my-workspace", "@acme/db"); // one package's internals
const past = await scan("./my-workspace", { history: true });

const server = await serve("./my-workspace", { port: 0 });
server.url;
await server.stop();

// Several workspaces, read together as a collection
const collection = await scan.collection(["./platform", "./product"]);
collection.manifest.crossings; // dependencies from one workspace on another
const both = await serve.collection(["./platform", "./product"], { port: 0 });
await both.stop();
```

## Runtime

- Spyglass runs on Bun 1.3.14 or newer. `bun add @foundry.sh/spyglass` installs the
  library and the `spyglass` binary together.
- Under Node, the package loads and `scan` works, but it starts its
  analysis workers with `bun` from the `PATH`, so Bun must still be
  installed. Under Bun, the running Bun starts them.
- `serve` needs Bun itself. Under Node it rejects with
  `Spyglass serve needs Bun (>=1.3.14).`
- The declarations use `URL`, which a TypeScript project gets from the DOM
  library, `@types/node`, or Bun's types.

## Exports

The package has one entry, `@foundry.sh/spyglass`. Nothing else in it can be
imported.

| Export | Kind | Does |
| --- | --- | --- |
| `scan(root, options?)` | function | Scans the workspace and every complete package's internals, saves the dataset, and returns a `Scan` |
| `scan.workspace(root, options?)` | function | Scans the workspace level only, saves the dataset without internals, and returns a `WorkspaceScan` |
| `scan.package(root, name)` | function | Analyzes one package's internals fresh and returns them as `PackageInternals`; saves nothing |
| `scan.collection(roots, options?)` | function | Scans each workspace of a [collection](#several-workspaces) as `scan` does, in turn, and returns a `CollectionScan` |
| `serve(root, options?)` | function | Serves the saved dataset and the viewer on `127.0.0.1` and returns an `SpyglassServer` |
| `serve.collection(roots, options?)` | function | Serves a collection's saved datasets, its collection manifest, and the viewer, and returns a `CollectionServer` |
| `DatasetNotFoundError` | class | No dataset is saved where one was expected |
| `DatasetReplacedError` | class | A later scan saved over the dataset an accessor reads |
| `DATASET_SCHEMA_VERSION` | constant | The dataset's schema version, `5` |
| `HISTORY_SCHEMA_VERSION` | constant | History's schema version, `1` |
| `COLLECTION_SCHEMA_VERSION` | constant | The collection manifest's schema version, `1` |

`root` is the workspace directory, resolved through symbolic links; `roots`
are a collection's.

## What a scan returns

`scan` and `scan.workspace` return what they saved: the dataset read back
from disk, not kept in memory.

| Field | Type | Holds |
| --- | --- | --- |
| `output` | `string` | The directory the dataset is saved in |
| `manifest` | `DatasetManifest` | The dataset's [manifest](/spyglass/reference/output) |
| `history` | `{ manifest: HistoryManifest }` or absent | History's manifest, when the scan asked for `history` |
| `workspaceReport()` | `Promise<WorkspaceReport>` | The workspace report, read from the saved dataset |
| `packageReport(name)` | `Promise<PackageReport>` | One package's report, read from the saved dataset |
| `internals(name)` | `Promise<PackageInternals>` | `Scan` only: one package's internals, read from the saved dataset |

- The plain fields are JSON. The accessors are functions, so
  `JSON.stringify` leaves them out.
- Each accessor reads the manifest, one file, and the manifest again. Once
  another scan of the workspace has saved over the dataset, the accessor
  rejects with `DatasetReplacedError`; scan again to read the new one.
- A scan reuses the cache, so scanning an unchanged workspace again is
  fast. There is no separate function that reads a saved scan.
- `scan.package(root, name)` analyzes fresh what
  `scan(root).internals(name)` reads back, and returns the six stages:
  `topology`, `locality`, `responsibilities`, `primitives`, `rewiring`, and
  `review`.

## Options

`ScanOptions`, for `scan` and `scan.workspace`:

| Option | Default | Effect |
| --- | --- | --- |
| `state` | `~/.foundry/spyglass` | The state root, as `--state`. Each workspace's dataset and cache go in a directory under it |
| `history` | `false` | Also analyze Git checkpoints into the dataset's `history/`; see [history](/spyglass/guides/history) |
| `onProgress` | none | Called with each `ScanProgressEvent` |

`ServeOptions`, for `serve`:

| Option | Default | Effect |
| --- | --- | --- |
| `port` | `4173` | The port, an integer from `0` to `65535`; `0` picks a free one |
| `state` | `~/.foundry/spyglass` | The state root to read the saved dataset from |

`scan.package` takes no options: it reads the workspace's configuration and
saves nothing.

## Progress

A `ScanProgressEvent` is one of these, told apart by `kind`:

| `kind` | When | Carries |
| --- | --- | --- |
| `phase` | A phase starts: `discover`, `fingerprint`, `analyze`, `derive`, `ingest`, `intelligence`, `project`, `write`, `lenses` | `phase`, and `detail` when there is one |
| `plan` | Once, before any analysis | `invalidations`: per package and stage, whether it is reused, recomputed, or removed, and why |
| `unit` | A package's own analysis finishes or is reused | `unit`, `status` (`complete`, `failed`, `reused`), `durationMs`, `done` of `total`, and `error` when it failed |
| `internals` | A package's internals are written | `package`, `index` (1-based) of `total` |
| `history` | History's own events, with `history: true` | `event`, whose `kind` is `phase`, `checkpoint`, or `unit` |

## The server

`serve` returns an `SpyglassServer`:

| Field | Holds |
| --- | --- |
| `url` | The viewer's address, a `URL` such as `http://127.0.0.1:4173/`; the dataset is under `data/` |
| `root` | The workspace root, resolved through symbolic links |
| `stop()` | Stops listening and closes open connections; resolves once the port is free. A second call does nothing |

`serve` takes no lock. A scan that runs while it serves replaces the
dataset atomically, and later requests see the new one.

## Several workspaces

A collection is one or more workspaces read together, such as a product
split across repositories that depend on each other. The CLI forms one only
from two or more distinct paths; the library also takes one. Each workspace keeps
its own state, dataset, cache, and lock; nothing is saved for the
collection itself. What ties them is the collection manifest, derived from
the workspaces' saved manifests whenever it is asked for: each workspace
with its id, label, and qualified package ids (`<workspace id>:<package>`),
and the crossings, the declared dependencies of a package in one workspace
on a package in another. See [output](/spyglass/reference/output#a-collection).

The paths are resolved through symbolic links, and a workspace given twice
counts once. They are put in one order, by resolved root, whatever the
order given. A workspace inside another is refused, and so are no paths at
all; one workspace is a collection of one.

`scan.collection(roots, options?)` scans each workspace as `scan` does, one
after the other in that order, each under its own lock, and returns a
`CollectionScan`:

| Field | Type | Holds |
| --- | --- | --- |
| `manifest` | `CollectionManifest` | The collection manifest, derived from the manifests the scans just saved |
| `workspaces` | `Scan[]` | Each workspace's scan, in the collection's order, with its accessors |

Its options are `scan`'s (`state`, `history`), except that `onProgress`
receives a `CollectionProgressEvent`, `{ kind: "workspace", id, root,
event }`: the workspace's id and resolved root, and the `ScanProgressEvent`
its own scan emitted. When a workspace fails, the ones scanned before it
keep the datasets they saved, and the call rejects with `Spyglass could not
scan <root>: <message>`, the workspace's own error as its `cause` and
their scans, each a `Scan` with its accessors, as `scanned`.

`serve.collection(roots, options?)` takes `serve`'s options and returns a
`CollectionServer`, with `url` and `stop()` as an `SpyglassServer` has and
`roots`, the resolved roots in the collection's order, in place of `root`.
It serves `data/collection-manifest.json`, derived at each request, and
each workspace's dataset under `data/<workspace id>/`; any other path under
`data/`, an unknown id included, is not found. It checks every workspace's
saved dataset first, as `serve` checks one, and takes no lock: a scan of
one workspace while it serves is seen by later requests, the collection
manifest included. While a workspace's dataset cannot be read (deleted,
caught mid-swap by a scan, or saved with another schema version), the
collection manifest answers `503` with `Retry-After: 1` and a message
naming the workspace. The viewer draws a collection as one archipelago
per workspace, side by side, with the crossings between them drawn on the
map and listed in Place.

## Errors

Every function rejects with an `Error` whose message says what to do; the
CLI prints the same message after `Spyglass: `.

| When | Error |
| --- | --- |
| No dataset is saved for the workspace: `serve`, or an accessor once the dataset is gone | `DatasetNotFoundError`. From `serve`: `No dataset found for <root> in <output>. Run spyglass scan to save one.` |
| A later scan saved over the dataset an accessor reads | `DatasetReplacedError`: `A later scan replaced this dataset. Scan again to read the new one.` |
| The dataset was saved with another schema version | `Spyglass reads dataset schema version 5; this dataset has version <n>. Run spyglass scan to save it again.` |
| An accessor names a package the dataset does not hold | `The dataset has no package named <name>. Its packages: <id>, <id>.` (or `It has no packages.`) |
| `packageReport` or `internals` names a package that failed analysis | `Spyglass could not analyze <id>, so it has no package report.`, `… so it has no internals.` |
| `internals` on a dataset saved without them, such as by `scan.workspace` | `The dataset was saved without internals for <id>. Run spyglass scan to include them.` |
| A file the manifest names is missing, or saved internals are incomplete | `The dataset is missing <path>. Run spyglass scan again.`, `The dataset's internals for <name> are incomplete. Run spyglass scan again.` |
| Another scan, export, or apply of the workspace is running | `Workspace <root> is being scanned, exported, or applied by process <pid>. Wait for it to finish, then retry.` |
| A package fails analysis; nothing is saved and the previous dataset stays | `Packages failed analysis: <id> (<error>)` |
| A history checkpoint fails analysis; the dataset is saved, and the scan still rejects | `Some history checkpoints failed analysis.` |
| `scan.package` names no package of the workspace | `Workspace <root> has no package named <name>.` |
| The path is missing or not a directory | `Workspace path does not exist: <path>`, `Workspace is not a directory: <root>` |
| The configuration is invalid | The message names the file and the setting; see [configuration](/spyglass/reference/configuration) |
| `serve` without Bun, or with a bad port | `Spyglass serve needs Bun (>=1.3.14).`, `Port must be an integer between 0 and 65535.` |
| A collection has no paths, or a workspace inside another | `A collection needs at least one workspace.`, `Workspace <outer> holds workspace <inner>; the workspaces of a collection cannot overlap.` |
| A workspace of `scan.collection` fails | `Spyglass could not scan <root>: <message>`, with the workspace's own error as `cause` and the scans before it as `scanned`; those workspaces stay saved |
| `serve` cannot find the viewer's files | `The viewer's files are missing from <dir>. Reinstall the package, or build it first when running from source.` |

Only the two dataset errors have their own classes in 0.1. Test for them
with `instanceof`; tell the others apart by message.

## Types

| Type | Describes |
| --- | --- |
| `Scan`, `WorkspaceScan` | What `scan` and `scan.workspace` return |
| `ScanOptions`, `ScanProgressEvent` | `scan`'s options and its progress events |
| `PackageInternals` | One package's internals, every stage through the review |
| `PackageReport` | One package's report |
| `WorkspaceReport` | The workspace report |
| `SpyglassServer`, `ServeOptions` | What `serve` returns and takes |
| `CollectionScan`, `CollectionScanOptions`, `CollectionProgressEvent` | What `scan.collection` returns and takes, and its progress events |
| `CollectionServer` | What `serve.collection` returns |
| `CollectionManifest` | The collection manifest, `data/collection-manifest.json` when served or exported |
| `DatasetManifest` | The dataset's manifest, `output/manifest.json` |
| `HistoryManifest` | History's manifest, `output/history/manifest.json` |
| `SpyglassConfig` | What one configuration file default-exports |

The types nested inside these are not exported. Reach one by indexed
access, such as `DatasetManifest["packages"][number]` for a package's
entry in the manifest. Editors show nested types under Spyglass's internal
names, many starting with `Semantics`, and show `PackageReport` as
`SurfaceReport`; the exported names are the ones to write.

`DATASET_SCHEMA_VERSION` is the dataset's promise: a dataset whose manifest
has this version holds the files [output](/spyglass/reference/output)
describes, in that shape. A program that reads `output/` itself, rather
than through a scan's accessors, checks the manifest's `schemaVersion`
against it. History
has its own version, `HISTORY_SCHEMA_VERSION`, in its own manifest.

## Configuration files

A configuration file in `.foundry/spyglass/` can check itself against
`SpyglassConfig`:

```ts
// .foundry/spyglass/index.ts
import type { SpyglassConfig } from "@foundry.sh/spyglass";

export default {
  exclude: ["packages/fixtures"],
} satisfies SpyglassConfig;
```

The import is type-only, so it is removed before the file runs; the
workspace needs `@foundry.sh/spyglass` installed only for its editor to check
the types. A domain is `NonNullable<SpyglassConfig["domains"]>[number]`, and a
rule `NonNullable<SpyglassConfig["rules"]>[number]`. See
[configuration](/spyglass/reference/configuration).

## One scan at a time

`scan` and `scan.workspace` hold the workspace's lock in its state
directory from start to finish, history included. A second scan or export
of the same workspace fails at once rather than waiting; scans of
different workspaces, or of one workspace under different state roots, run
side by side. `scan.collection` holds each workspace's lock while it scans
that workspace. `scan.package`, `serve`, and `serve.collection` take no
lock.

## Not exported

The analyses themselves, the dataset reader, the report printer (`spyglass
report` is CLI only), `spyglass export`, and the codemods are not part of the
library. `spyglass scan` and `spyglass serve` are `scan` and `serve` plus
formatting; see the [CLI](/spyglass/reference/cli).
