diff --git a/AGENTS.md b/AGENTS.md
index b49348f..1fc37c7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,8 +27,9 @@ oversight. Persistence and a background sampler come later — see
- swiftformat (`.swiftformat`) for lint. CI runs on a self-hosted macOS ARM64
runner.
- Logging is `os.Logger` via the package-wide `log` in `MonitorCore/Log.swift`
- (subsystem `wtf.evan.monitor`). Only `monitorctl` prints, because printing is
- its output.
+ (subsystem `wtf.evan.monitor`). The app never prints. The two CLIs do, because
+ output is their point: `monitorctl` prints readings, and `monitord` prints its
+ startup line, its usage and its version.
## Environment & Dependencies
@@ -38,6 +39,8 @@ oversight. Persistence and a background sampler come later — see
to one revision rather than to whatever the range resolves to today.
- `swiftformat` must be on `PATH` for the lint gate. CI installs it with
`brew install swiftformat` when it is missing.
+- `Package.resolved` is committed and is **not** ignored. Update it only by
+ changing a dependency deliberately, never to silence a resolve.
- `MonitorSourcesTests` read the real machine, so they need a real Mac. They
assert plausible ranges, not values, and pass whatever the machine is doing.
@@ -53,6 +56,8 @@ swift run monitorctl read # read every metric once
swift run monitorctl watch --source disk --interval 0.5
swift run monitorctl watch --json --count 5 # machine-readable, bounded
swift run monitord --retention 7d --dir /tmp/logs # rotating CSV logger
+swift run monitorctl --help # generated from the declarations, never hand-written
+swift run monitord --version # version and the commit it was built from
swiftformat Sources Tests Plugins --lint --cache ignore # CI lint gate
Scripts/make-app.sh [dest] # wrap the release binary in monitor.app
Scripts/make-icon.swift out.icns # draw the app icon (make-app.sh calls this)
@@ -85,8 +90,10 @@ Sources/
MonitorLog/ the rotating CSV logger: CSVLogSink. Written by monitord;
never linked into the app.
monitor/ the app target (@main SwiftUI App) and its AppDelegate
- monitorctl/ headless CLI harness
- monitord/ headless daemon that logs every metric to rotating CSV
+ monitorctl/ headless CLI harness: Monitorctl.swift, an ArgumentParser
+ root with list/read/watch subcommands
+ monitord/ headless daemon that logs every metric to rotating CSV:
+ Monitord.swift, an ArgumentParser command
Plugins/
StampCommit/ prebuild plugin: writes the commit into a Swift constant
before every build, so the title bar cannot go stale
@@ -94,10 +101,12 @@ Scripts/ make-app.sh, which builds monitor.app, make-icon.swift,
which draws its icon, and notarize.sh, which notarizes and
staples a Developer ID build
Tests/ MonitorCoreTests, MonitorSourcesTests, MonitorStoreTests,
- MonitorLogTests, MonitorUITests
+ MonitorLogTests, MonitorUITests, CommandLineTests (the two
+ CLIs' argument parsing — see "Making Changes")
docs/ README.md is the index
.github/workflows/
- ci.yml build, test, release build, CLI smoke test, lint
+ ci.yml build, test, release build, CLI smoke tests (including
+ --help/--version and that monitord --help writes no CSV), lint
release.yml bumps the version on a merge to main, tags it, releases it
package.yml reusable: builds monitor.app, zips it, attaches it to a tag
```
@@ -509,6 +518,22 @@ are no component-level AGENTS.md files.
both locks stay on: `[skip ci]` in the bump commit, and the `if:` guard on
the `tag` job that ignores a commit carrying it. The tag targets the bump
commit rather than `GITHUB_SHA`, or the zip reports the version before it.
+- `Package.resolved` — a lockfile, committed on purpose. It pins the one
+ dependency to a revision, so a release is built from the code that was
+ tested rather than from whatever the version range resolved to that day.
+- `Sources/MonitorCore/CommitStamp.generated.swift` — written by the
+ `StampCommit` plugin before every build. Never edit it, and never check a
+ version of it in.
+
+### Deprecated
+
+- **Hand-rolled argument parsing in the CLI targets.** `firstIndex(of:)` scans
+ over `CommandLine.arguments`, and usage text held in a `let usage = """…"""`
+ literal. Both are gone: they could not reject an unknown flag and the literal
+ had no route to a terminal, so `monitord --help` started the daemon. Declare
+ flags on a `ParsableCommand` instead.
+- **`main.swift` in an executable target.** Both CLIs used top-level code; they
+ are now `@main` types, which is what lets a test target import them.
## Troubleshooting
@@ -551,6 +576,13 @@ are no component-level AGENTS.md files.
request body as the commit message — so a merge like that would also skip
the release. Write the marker as "skip-ci" in prose and keep the literal
form inside `release.yml`.
+- **A CLI change makes the test suite hang or burn CPU**: something is calling
+ `SourceRegistry.makeAll()` per access. It builds real readers, and `SMCSource`
+ opens an IOKit connection. `SourceRegistry.allIDs` is a stored property for
+ this reason — it was a computed one, and putting it in a `--help` string that
+ ArgumentParser rebuilds on every parse took `CommandLineTests` from under a
+ second to over four minutes. Interpolate `SourceSelection.known`, not a fresh
+ registry build.
- **CPU shows no cluster series**: correct on a machine with one performance
level, or when the `hw.perflevel*` core counts do not sum to the core count.
`CPUSource.readClusters` returns empty rather than guess a wrong split.
diff --git a/Package.swift b/Package.swift
index 047dd44..8e20ee7 100644
--- a/Package.swift
+++ b/Package.swift
@@ -9,6 +9,8 @@ import PackageDescription
// MonitorUI SwiftUI dashboard
// monitor the app
// monitorctl headless CLI — develop and verify sources without the GUI
+// MonitorLog the rotating CSV writer behind monitord
+// monitord headless daemon — logs every metric to rotating CSV
// MonitorStore on-disk history — designed and tested, deliberately NOT
// linked into v1; see below
//
diff --git a/README.md b/README.md
index ee9ca91..a21c719 100644
--- a/README.md
+++ b/README.md
@@ -15,34 +15,45 @@ two things wrong: the charts are postage stamps, and the history begins the
moment you open the app — so whatever weird blip you went looking for is exactly
the thing it cannot show you.
+One Swift package builds three programs: a SwiftUI app, a CSV logging daemon,
+and a headless CLI for reading the same metrics from a terminal.
+
## Screenshot
-
+
+## What the repository provides
-## Status
+Three executables, five libraries and a build plugin, in one SwiftPM package.
-The app is realtime. History lives in a ten-minute ring buffer in memory and
-dies with the process. Gauges for rates, charts for levels.
+| Program | What it is | Shipped in the release zip |
+|---------|------------|----------------------------|
+| `monitor` | The SwiftUI app. Realtime panel of gauges and charts, ten minutes of in-memory history. | yes, as `monitor.app` |
+| `monitord` | Headless daemon. Samples every metric on one clock and writes rotating CSV. | yes, as a bare binary |
+| `monitorctl` | Headless CLI. Lists, reads and watches the same metrics in a terminal. | no — a development tool |
-`monitord` is the disk logger. It is a headless daemon that samples every
-metric on the same clock and writes rotating, human-readable CSV. Run it as a
-launchd `LaunchAgent` to log for days. It ships in the release zip beside
-`monitor.app`.
+| Library | What it holds |
+|---------|---------------|
+| `MonitorCore` | Metric model, ring buffer, downsampling, gauge auto-ranging, formatting, the sampling clock, CSV export, chart axis ticks. No macOS APIs, so it is testable anywhere. |
+| `MonitorSources` | The readers — CPU, memory, disk, network, GPU, SMC sensors — and the registry that lists them. |
+| `MonitorUI` | The dashboard: theme, gauges, chart cards, preferences, drag-to-reorder, `AppModel`. |
+| `MonitorLog` | `CSVLogSink`, the rotating CSV writer. Used by `monitord`; never linked into the app. |
+| `MonitorStore` | SQLite history and retention. Written and tested, deliberately **not** linked into any executable — see [docs/storage.md](docs/storage.md). |
-The SQLite store and an app-side history picker remain on the roadmap — see
-`docs/roadmap.md`.
+`Plugins/StampCommit` is a prebuild plugin that writes the current commit into a
+Swift constant, so the app's title bar cannot claim a stale build.
## Download
Grab the latest `monitor-*.zip` from
[Releases](https://github.com/evanwtf/monitor/releases/latest), unzip it, and
-drag `monitor.app` to Applications. The zip also contains `monitord`, a headless
-daemon that logs every metric to rotating CSV files.
+drag `monitor.app` to Applications. The zip also contains `monitord`, so a
+downloader runs `./monitord` with no toolchain installed.
-The app is signed ad-hoc rather than with a Developer ID, and it is not
-notarized, so macOS quarantines it on first launch and says it is damaged.
-Right-click the app and choose Open, then Open again in the dialog. Or clear
-the flag yourself:
+Releases are ad-hoc signed and not notarized unless the repository's
+`SIGN_IDENTITY` and `NOTARY_PROFILE` variables are set, in which case the
+release notes say so. When they are not, macOS quarantines the app on first
+launch and calls it damaged. Right-click the app and choose Open, then Open
+again in the dialog, or clear the flag yourself:
```sh
xattr -d com.apple.quarantine /Applications/monitor.app
@@ -50,45 +61,67 @@ xattr -d com.apple.quarantine /Applications/monitor.app
Building it yourself avoids all of that.
-## Running it
+## Usage
+
+### The app
```sh
swift run monitor
```
-No Xcode project needed. That is the development loop. The app claims a
-foreground identity at launch, so it appears in Cmd-Tab and quits with Cmd-Q
-like anything else, even though it is a bare SwiftPM executable.
+No Xcode project needed. The app claims a foreground identity at launch, so it
+appears in Cmd-Tab and quits with Cmd-Q even as a bare SwiftPM executable.
+Cmd-, opens Preferences: **Layout** chooses a gauge and/or chart per metric,
+**Charts** controls how cards are drawn, **Sampling** sets the rates. Drag tiles
+to rearrange, double-click to zoom, right-click for Copy Image and Copy Data.
-To install it somewhere you can launch it from, build a real bundle:
+To install a real bundle:
```sh
Scripts/make-app.sh ~/Applications
```
-That produces `monitor.app` — Info.plist, icon, bundle id and an ad-hoc
-signature. It is not signed with a Developer ID or notarized, so it is for this
-Mac.
-
-There is also a headless CLI, which is how the sampling code gets developed and
-verified without a GUI in the way:
+### `monitorctl` — read metrics in a terminal
```sh
-swift run monitorctl list # what can be measured
-swift run monitorctl read # one reading of everything
+swift run monitorctl list # every source and metric it declares
+swift run monitorctl read # one reading of everything
swift run monitorctl watch --source disk --interval 0.5
-swift run monitorctl watch --json | jq # machine-readable
-swift run monitord --retention 7d --dir /tmp/logs # rotating CSV logger
+swift run monitorctl watch --json --count 5 | jq # machine-readable, bounded
+```
+
+| Option | Applies to | Meaning |
+|--------|-----------|---------|
+| `--source ` | all | Limit to one source; repeatable. `cpu`, `memory`, `disk`, `network`, `gpu`, `sensors`. |
+| `--interval ` | `read`, `watch` | Sampling interval. Default `1.0`. |
+| `--json` | `read`, `watch` | One JSON object per sample instead of a table. |
+| `--count ` | `watch` | Stop after n samples. |
+
+Disk, network and paging are counters, so a rate needs two readings: `read`
+takes two ticks itself, and `watch` prints nothing for them on its first line.
+That is correct, not a failure.
+
+### `monitord` — log every metric to CSV
+
+```sh
+swift run monitord --retention 7d --dir /tmp/logs
+./monitord # from the release zip
```
-`monitord` is the logger: it samples every metric on the same clock and writes
-rotating, human-readable CSV — one file per run, hostname in the filename and
-as a column, timestamps in ISO8601 and epoch millis, temperatures in both °C
-and °F. Run it as a launchd `LaunchAgent` to log for days.
+| Option | Meaning |
+|--------|---------|
+| `--dir ` | Directory for the CSV files. Default `~/Library/Logs/monitor`. |
+| `--retention ` | `1h`, `6h`, `24h`, `48h`, `3d`, `5d`, `7d`, `14d`, `30d`, `forever`. Default `24h`. |
+| `--interval ` | Sampling interval. Default `1.0`. |
-With no options it logs at 1s with 1d retention to `~/Library/Logs/monitor`.
-The release zip ships a standalone `monitord` binary alongside `monitor.app`, so
-a downloader runs `./monitord` — no `swift run` needed.
+Files are named `sensors.._