Library
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.
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/spyglassinstalls the library and thespyglassbinary together. - Under Node, the package loads and
scanworks, but it starts its analysis workers withbunfrom thePATH, so Bun must still be installed. Under Bun, the running Bun starts them. serveneeds Bun itself. Under Node it rejects withSpyglass 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 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 |
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.stringifyleaves 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 whatscan(root).internals(name)reads back, and returns the six stages:topology,locality,responsibilities,primitives,rewiring, andreview.
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 |
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.
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 |
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
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:
// .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.
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.