@progen-labs/spectra 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,9 @@ and per-component render/change-detection time; source files too, where the fram
14
14
  functions. A [Progen Labs](https://github.com/progen-labs) project — open source, non-profit, no
15
15
  telemetry, no account required.
16
16
 
17
+ **Documentation:** the full guide, with annotated screenshots of every screen, is at
18
+ [progen-labs.github.io/website](https://progen-labs.github.io/website/#spectra/docs/introduction).
19
+
17
20
  ## Install
18
21
 
19
22
  ```bash
@@ -52,15 +55,24 @@ Then open the visual dashboard — ranked culprits, React re-renders, long tasks
52
55
  **PDF export** and **commit-to-commit compare**:
53
56
 
54
57
  ```bash
55
- npx spectra dashboard
58
+ npx spectra dashboard --open
56
59
  ```
57
60
 
61
+ ![The Spectra dashboard: the top culprit of a recording, burn() in src/heavy.js, with its time and a suggested fix](docs/dashboard.webp)
62
+
63
+ A [tour of the dashboard](https://progen-labs.github.io/website/#spectra/docs/dashboard), with every part
64
+ labelled, is in the docs.
65
+
58
66
  ## Commands
59
67
 
60
68
  ```
61
- spectra performance [options] Golden-path evaluation (default)
62
- spectra dashboard [--port N] Open the dashboard UI
63
- spectra --help Full option list
69
+ spectra performance [options] Golden-path evaluation (default)
70
+ spectra performance --quick One page-load profile (about 15 s)
71
+ spectra compare <before> <after> Did it get faster? Noise-aware before/after
72
+ spectra reports List saved reports
73
+ spectra mcp [--project <dir>] MCP server for AI tools (stdio)
74
+ spectra dashboard [--port N] [--open] Open the dashboard UI
75
+ spectra --help Full option list
64
76
  spectra --version
65
77
  ```
66
78
 
@@ -74,7 +86,147 @@ spectra --version
74
86
  | `--boot-timeout <s>` | How long to wait for the dev server (default 60) |
75
87
  | `--headed` | Show the browser window |
76
88
  | `--sign-in <url>` | Sign-in URL template, e.g. `http://host/login/?redirect={url}` |
89
+ | `--quick` | One page-load profile instead of the golden-path suite: the fast loop |
90
+ | `--runs <n>` | Record n times and report the median (with `--quick` or `--mode`) |
77
91
  | `--mode live\|load` | Classic single-page profile instead of the golden-path suite |
92
+ | `--json` | Print a machine-readable result on stdout (tables go to stderr) |
93
+ | `--out <file>` | Also write that result to a file (an input for `compare`) |
94
+ | `--config <file>` | Budgets file (default: `spectra.config.json` in your project) |
95
+
96
+ ## Measure, change, verify
97
+
98
+ Performance numbers are noisy, so Spectra never trusts a single recording when you ask it whether a change
99
+ helped. Record a few runs before and after, then compare:
100
+
101
+ ```bash
102
+ npx spectra performance --quick --runs 5 --out before.json
103
+ # …make your change…
104
+ npx spectra performance --quick --runs 5 --out after.json
105
+ npx spectra compare before.json after.json
106
+ ```
107
+
108
+ ```
109
+ Spectra compare — IMPROVED (confidence: medium)
110
+ ✓ JavaScript CPU time: 1.52 s → 92.1 ms (−94%)
111
+ ✓ Row (src/Row.jsx) — render time: 1.44 s → 7.4 ms (−99%)
112
+
113
+ Findings: 3 resolved · 2 new · 10 still present
114
+ resolved [critical] Row is slow to render: 1444ms total, 0.2ms per render (7200×)
115
+ ```
116
+
117
+ Each side is reduced to its median, and a difference only counts when it is larger than the run-to-run
118
+ spread. A side can be a `--out` result file, a report id from `spectra reports`, a report file, or several
119
+ ids joined by commas.
120
+
121
+ ## Budgets and CI
122
+
123
+ Put limits in `spectra.config.json` at your project root:
124
+
125
+ ```json
126
+ {
127
+ "$schema": "./node_modules/@progen-labs/spectra/schema/config.schema.json",
128
+ "budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 }
129
+ }
130
+ ```
131
+
132
+ Every `spectra performance` run checks them: the median for `--quick`/`--mode`, every journey for the
133
+ golden-path suite. Metrics a recording did not measure are skipped, never failed. Available budgets:
134
+ `tbtMs`, `lcpMs`, `fcpMs`, `cls`, `inpMs`, `ttfbMs`, `longTaskCount`, `maxLongTaskMs`, `scriptingMs`,
135
+ `forcedLayoutCount`, `domNodes`, `heapUsedMb`, `transferKb`, `criticalFindings`, `findingCount`.
136
+
137
+ Exit codes make it a CI gate:
138
+
139
+ | Code | Meaning |
140
+ | --- | --- |
141
+ | `0` | Done, and within budget |
142
+ | `1` | Could not run (no Chrome, server down, bad config) |
143
+ | `2` | Ran, but a budget was exceeded, or `compare --fail-on-regression` found a regression |
144
+
145
+ ### GitHub Action
146
+
147
+ The repository is also a GitHub Action. On every pull request it profiles the change and its base commit on the
148
+ same runner, compares them beyond run-to-run noise, enforces your budgets, and writes the result to the job summary
149
+ and (optionally) a PR comment:
150
+
151
+ ```yaml
152
+ name: Performance
153
+ on: pull_request
154
+ permissions:
155
+ contents: read
156
+ pull-requests: write # only for comment: true
157
+ jobs:
158
+ spectra:
159
+ runs-on: ubuntu-latest
160
+ steps:
161
+ - uses: actions/checkout@v4
162
+ - uses: actions/setup-node@v4
163
+ with: { node-version: 22 }
164
+ - run: npm ci
165
+ - uses: progen-labs/spectra@v1
166
+ with:
167
+ compare-to-base: true
168
+ fail-on-regression: true
169
+ comment: true
170
+ ```
171
+
172
+ Inputs include `url`, `command`, `mode` (`quick` or `golden-paths`), `runs`, `config`, `fail-on-budget` and
173
+ `working-directory`; outputs are `result-file`, `verdict`, `over-budget` and `summary-file`. Full reports are
174
+ uploaded as an artifact. See the [Action guide](https://progen-labs.github.io/website/#spectra/docs/github-action).
175
+
176
+ ## Machine-readable output
177
+
178
+ `--json` prints a small, versioned result on stdout and sends every human-readable line to stderr, so it can
179
+ be piped into `jq`, an editor or an AI agent. Each result has `schemaVersion` and a `kind` (`profile`,
180
+ `evaluation`, `compare` or `reports`) and points at the full reports on disk rather than embedding them.
181
+ Findings carry the file, line, cost in ms and a suggested fix. The format is described by a JSON Schema
182
+ shipped with the package (`schema/result.schema.json`); additive changes keep `schemaVersion`, breaking
183
+ changes bump it. Saved reports carry their own `schemaVersion` too.
184
+
185
+ ## Use it from your AI assistant
186
+
187
+ Spectra is an [MCP](https://modelcontextprotocol.io) server too, so an AI coding assistant can profile a page,
188
+ fix what it finds, and prove each fix with before/after measurements.
189
+
190
+ **Claude Code**, with the plugin (adds an *improve-performance* skill and `/spectra:audit`):
191
+
192
+ ```
193
+ /plugin marketplace add progen-labs/spectra
194
+ /plugin install spectra@progen-labs
195
+ ```
196
+
197
+ **Any MCP client** (Claude Code without the plugin, Claude Desktop, Cursor, VS Code, Windsurf…): run
198
+ `npx -y @progen-labs/spectra mcp` as a stdio server (Spectra 0.3.0 or later). For example:
199
+
200
+ ```bash
201
+ claude mcp add spectra -- npx -y @progen-labs/spectra mcp
202
+ ```
203
+
204
+ ```json
205
+ { "mcpServers": { "spectra": { "command": "npx", "args": ["-y", "@progen-labs/spectra", "mcp", "--project", "/path/to/your/app"] } } }
206
+ ```
207
+
208
+ Tools: `profile_page`, `verify_change` (re-profile and compare against a baseline), `compare_profiles`,
209
+ `run_golden_paths`, `list_reports` and `get_report`. The server profiles pages on this machine only (start it with
210
+ `--allow-remote` to lift that), runs one browser at a time, and stops Chrome and any dev server it started when a
211
+ call is cancelled. See the [MCP guide](https://progen-labs.github.io/website/#spectra/docs/mcp-server).
212
+
213
+ ## In VS Code
214
+
215
+ The Spectra extension (`vscode/`) puts results where you work: every finding in the **Problems panel** at the file
216
+ and line responsible, **CodeLens** above the costliest components and functions
217
+ (`⚡ Spectra: Row · 7,200 renders · 1.44 s rendering`), headline numbers in the **status bar**, **Verify against
218
+ baseline** after a change, golden paths, and the dashboard inside the editor. Results that the CLI or an AI
219
+ assistant writes to `.spectra/results` appear automatically, and on VS Code 1.101+ it offers Spectra's MCP server to
220
+ agent mode. It runs your project's Spectra (0.2.0 or later) or `npx`.
221
+
222
+ Until it is listed on the VS Code Marketplace and Open VSX, build and install it from this repository:
223
+
224
+ ```bash
225
+ cd vscode && npx @vscode/vsce package --no-dependencies
226
+ code --install-extension spectra-0.1.0.vsix
227
+ ```
228
+
229
+ See the [VS Code guide](https://progen-labs.github.io/website/#spectra/docs/vscode).
78
230
 
79
231
  ## Safe by default
80
232
 
@@ -97,6 +249,19 @@ that cost the most main-thread time. See the
97
249
  [deep-dive docs](https://github.com/progen-labs/spectra/blob/main/DOCS.md) for how capture, correlation
98
250
  and report generation work.
99
251
 
252
+ ## Documentation
253
+
254
+ - **[The guide](https://progen-labs.github.io/website/#spectra/docs/introduction)**: every feature, with
255
+ annotated screenshots. Start with
256
+ [Quick start](https://progen-labs.github.io/website/#spectra/docs/quick-start),
257
+ [Compare before and after](https://progen-labs.github.io/website/#spectra/docs/compare),
258
+ [Budgets and CI](https://progen-labs.github.io/website/#spectra/docs/budgets),
259
+ [JSON output](https://progen-labs.github.io/website/#spectra/docs/json-output),
260
+ [Claude Code](https://progen-labs.github.io/website/#spectra/docs/claude-code), the
261
+ [Findings reference](https://progen-labs.github.io/website/#spectra/docs/findings) and
262
+ [Troubleshooting](https://progen-labs.github.io/website/#spectra/docs/troubleshooting).
263
+ - **[DOCS.md](DOCS.md)**: the in-repo manual, the technical deep dive, and how to contribute.
264
+
100
265
  ## License
101
266
 
102
267
  MIT © Progen Labs
package/bin/spectra.js CHANGED
@@ -3,6 +3,9 @@
3
3
  * Spectra CLI — a Progen Labs project.
4
4
  *
5
5
  * spectra performance [--url <page>] [profiler options] profile a frontend
6
+ * spectra compare <before> <after> before vs after, noise-aware
7
+ * spectra reports list saved reports
8
+ * spectra mcp [--project <dir>] MCP server for AI tools (stdio)
6
9
  * spectra dashboard open the dashboard UI
7
10
  * spectra --help show this help
8
11
  * spectra --version print the version
@@ -12,7 +15,7 @@
12
15
  * root is used for source lookup, unless the matching PERF_* env var overrides it.
13
16
  */
14
17
  import { spawn } from 'node:child_process';
15
- import { readFileSync } from 'node:fs';
18
+ import { existsSync, readFileSync } from 'node:fs';
16
19
  import path from 'node:path';
17
20
  import { fileURLToPath } from 'node:url';
18
21
  import { bootDevServer } from '../server/explore/boot.js';
@@ -33,6 +36,13 @@ Spectra ${pkg.version} — frontend performance profiler (Progen Labs)
33
36
 
34
37
  Usage:
35
38
  spectra performance [options] Profile a frontend build and report the culprits
39
+ spectra compare <before> <after> [options]
40
+ Compare two profiles; a change counts only when
41
+ it is bigger than the noise between runs
42
+ spectra reports [--json] List saved reports (ids for compare / dashboard)
43
+ spectra mcp [--project <dir>] [--allow-remote]
44
+ Run as an MCP server (stdio) for Claude Code,
45
+ Cursor, VS Code and other AI tools
36
46
  spectra dashboard Open the Spectra dashboard UI
37
47
  spectra --help, -h Show this help
38
48
  spectra --version, -v Print the version
@@ -52,20 +62,58 @@ performance options:
52
62
  --headed Show the browser window
53
63
  --sign-in <url> Sign-in URL template, e.g. "http://host/login/?redirect={url}"
54
64
 
55
- classic single-page profile (opt in with --mode or --scenario):
65
+ quick single-page profile (opt in with --quick, --mode or --scenario):
66
+ --quick One page-load profile (about 15 s): the fast loop
67
+ --runs <n> Record n times and report the median (with --quick/--mode)
56
68
  --mode live|load Live session or page-load recording
57
69
  --duration <seconds> Live recording length (default 8)
58
70
  --throttle <n> CPU throttle multiplier (default 1)
59
71
  --scenario <file.mjs> Script a live session: export default async page => {…}
60
72
  --save-trace Keep the raw Chrome trace alongside the report
61
73
 
74
+ output and checks (any performance run):
75
+ --json Print a machine-readable result on stdout (progress and
76
+ tables go to stderr)
77
+ --out <file> Also write that result to a file (input for compare)
78
+ --config <file> Budgets file (default: spectra.config.json in the project)
79
+
80
+ compare options:
81
+ <before> <after> Each: a report id, a report file, a --out result file,
82
+ or several ids joined by commas
83
+ --json, --out <file> As above
84
+ --fail-on-regression Exit with code 2 when a key metric got worse
85
+
86
+ Budgets (spectra.config.json in your project root):
87
+ { "budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 } }
88
+
89
+ Exit codes: 0 done · 1 could not run · 2 over budget / regression
90
+
62
91
  Examples:
63
92
  spectra performance # self-boot + golden-path suite
64
93
  spectra performance --paths 8 # cap the suite at 8 journeys
65
94
  spectra performance --url http://localhost:3000/ # evaluate a running server
66
- spectra performance --mode load # classic single page-load profile
95
+ spectra performance --quick --runs 5 --out before.json
96
+ spectra performance --quick --runs 5 --out after.json
97
+ spectra compare before.json after.json # did the change help?
98
+ spectra performance --json | jq .summary # pipe it anywhere
67
99
  `;
68
100
 
101
+ // What a terminate signal must clean up: the running script and a dev server
102
+ // Spectra started. Without this, stopping Spectra from outside (an editor or
103
+ // an AI client cancelling a run) would leave the dev server running.
104
+ const active = { child: null, server: null };
105
+ let stopping = false;
106
+ for (const signal of ['SIGINT', 'SIGTERM']) {
107
+ process.on(signal, () => {
108
+ if (stopping) return;
109
+ stopping = true;
110
+ active.child?.kill(signal);
111
+ Promise.resolve(active.server?.stop())
112
+ .catch(() => {})
113
+ .finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
114
+ });
115
+ }
116
+
69
117
  function runNodeAsync(scriptRelPath, args, extraEnv = {}) {
70
118
  return new Promise((resolve, reject) => {
71
119
  const child = spawn(
@@ -77,7 +125,11 @@ function runNodeAsync(scriptRelPath, args, extraEnv = {}) {
77
125
  env: { ...process.env, ...extraEnv },
78
126
  },
79
127
  );
80
- child.on('exit', code => resolve(code ?? 0));
128
+ active.child = child;
129
+ child.on('exit', code => {
130
+ if (active.child === child) active.child = null;
131
+ resolve(code ?? 0);
132
+ });
81
133
  child.on('error', reject);
82
134
  });
83
135
  }
@@ -91,6 +143,19 @@ function takeOption(args, name) {
91
143
  return { value, rest: copy };
92
144
  }
93
145
 
146
+ /**
147
+ * The scripts run with the package as their working directory, so path
148
+ * options the user typed relative to their own directory are made absolute
149
+ * here, before they cross over.
150
+ */
151
+ function absolutize(args, names) {
152
+ return args.map((a, i) =>
153
+ i > 0 && names.includes(args[i - 1].replace(/^--/, '')) && args[i - 1].startsWith('--') && !a.startsWith('--')
154
+ ? path.resolve(process.cwd(), a)
155
+ : a,
156
+ );
157
+ }
158
+
94
159
  /**
95
160
  * Default the profiled project and the reports location to the user's current
96
161
  * directory, so an installed Spectra profiles the host project and writes
@@ -114,27 +179,44 @@ async function runProfile() {
114
179
  return 0;
115
180
  }
116
181
  const env = projectEnv();
182
+ let args = absolutize(rest, ['scenario', 'out', 'config']);
183
+
184
+ // --quick is shorthand for a single page-load profile.
185
+ if (args.includes('--quick')) {
186
+ const mode = takeOption(args, 'mode').value;
187
+ if ((mode && mode !== 'load') || args.includes('--scenario')) {
188
+ console.error('spectra: --quick is a page-load profile; it cannot be combined with --mode live or --scenario.');
189
+ return 1;
190
+ }
191
+ args = args.filter(a => a !== '--quick');
192
+ if (!mode) args.push('--mode', 'load');
193
+ }
117
194
 
118
195
  // Golden-path evaluation is the default. --mode/--scenario opt into the
119
196
  // classic single-page profile instead.
120
- const classic = rest.includes('--mode') || rest.includes('--scenario');
197
+ const classic = args.includes('--mode') || args.includes('--scenario');
121
198
  const script = classic ? 'server/cli.js' : 'server/explore/evaluate-cli.js';
199
+ if (!classic && args.includes('--runs')) {
200
+ console.error(
201
+ 'spectra: --runs works with single-page profiles (--quick, --mode or --scenario); the golden-path suite records each journey once.',
202
+ );
203
+ return 1;
204
+ }
122
205
 
123
206
  // An explicit --url means the server is already running: run against it.
124
- if (rest.includes('--url')) {
125
- return runNodeAsync(script, rest, env);
207
+ if (args.includes('--url')) {
208
+ return runNodeAsync(script, args, env);
126
209
  }
127
210
 
128
211
  // No --url: self-boot the target project's own dev server, run, stop.
129
212
  const cwd = process.cwd();
130
- const { value: command, rest: afterCmd } = takeOption(rest, 'command');
213
+ const { value: command, rest: afterCmd } = takeOption(args, 'command');
131
214
  const { value: bootTimeout, rest: profileArgs } = takeOption(afterCmd, 'boot-timeout');
132
215
  const fw = detectFramework(cwd);
133
- const label =
134
- fw.framework !== 'unknown' ? `${fw.framework}/${fw.bundler}` : fw.bundler;
216
+ const label = [fw.framework, fw.bundler].filter(x => x && x !== 'unknown').join('/');
135
217
  // Status goes to stderr so it never mixes with the report on stdout.
136
218
  console.error(
137
- `spectra: no --url given — starting the ${label} dev server in ${cwd} …`,
219
+ `spectra: no --url given — starting the ${label ? `${label} ` : ''}dev server in ${cwd} …`,
138
220
  );
139
221
 
140
222
  let server;
@@ -148,6 +230,7 @@ async function runProfile() {
148
230
  console.error(`spectra: ${err.message}`);
149
231
  return 1;
150
232
  }
233
+ active.server = server;
151
234
  console.error(`spectra: dev server ready at ${server.url} — profiling …\n`);
152
235
 
153
236
  let code = 1;
@@ -158,12 +241,23 @@ async function runProfile() {
158
241
  env,
159
242
  );
160
243
  } finally {
244
+ active.server = null;
161
245
  await server.stop();
162
246
  console.error('\nspectra: stopped the dev server.');
163
247
  }
164
248
  return code;
165
249
  }
166
250
 
251
+ function runCompare() {
252
+ // Sides may be files relative to the user's directory (ids pass through).
253
+ const fixToken = t =>
254
+ t.endsWith('.json') || existsSync(path.resolve(process.cwd(), t)) ? path.resolve(process.cwd(), t) : t;
255
+ const args = absolutize(rest, ['out']).map((a, i, all) =>
256
+ a.startsWith('--') || all[i - 1] === '--out' ? a : a.split(',').map(fixToken).join(','),
257
+ );
258
+ return runNodeAsync('server/compare-cli.js', args, projectEnv());
259
+ }
260
+
167
261
  function runDashboard() {
168
262
  // Standalone server: serves the prebuilt dashboard and the backend API, so an
169
263
  // installed Spectra needs no dev toolchain. projectEnv() points it at this
@@ -177,6 +271,7 @@ function runDashboard() {
177
271
  env: { ...process.env, ...projectEnv() },
178
272
  },
179
273
  );
274
+ active.child = child;
180
275
  child.on('exit', code => process.exit(code ?? 0));
181
276
  child.on('error', err => {
182
277
  console.error(`spectra: failed to start the dashboard — ${err.message}`);
@@ -194,6 +289,26 @@ switch (command) {
194
289
  process.exit(1);
195
290
  });
196
291
  break;
292
+ case 'compare':
293
+ case 'reports': {
294
+ const run = command === 'compare' ? runCompare() : runNodeAsync('server/reports-cli.js', rest, projectEnv());
295
+ run
296
+ .then(code => process.exit(code ?? 0))
297
+ .catch(err => {
298
+ console.error(`spectra: ${err.message}`);
299
+ process.exit(1);
300
+ });
301
+ break;
302
+ }
303
+ case 'mcp':
304
+ // In-process: stdin/stdout are the protocol channel, and the working
305
+ // directory stays the user's (the project, unless --project or the
306
+ // client's roots say otherwise).
307
+ import('../server/mcp.js').catch(err => {
308
+ console.error(`spectra: ${err.message}`);
309
+ process.exit(1);
310
+ });
311
+ break;
197
312
  case 'dashboard':
198
313
  case 'ui':
199
314
  runDashboard();