calibre
Version:
Performance monitoring with Synthetic testing, Chrome UX Report, and Real User Metrics
1,075 lines (809 loc) • 31.7 kB
Markdown
# Calibre CLI v7.0.0 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Restructure the CLI into Synthetic/CrUX/RUM namespaces, add 8 new field data commands, maintain backwards compatibility via hidden deprecated aliases.
**Architecture:** Move existing command files into new directories (`synthetic/`, `deploy/`), create new API modules and CLI commands for CrUX and RUM, wire hidden deprecated aliases in the slimmed-down `site.js`, and update all docs/examples. All existing GraphQL API patterns are followed identically.
**Tech Stack:** JavaScript (ES Modules), yargs 18, graphql-request 7, chalk, columnify, ora, Jest
**Design doc:** `docs/plans/2026-05-05-v7-restructure-design.md`
**Changelog spec:** `CHANGELOG-v7-DRAFT.md`
---
### Task 1: Create deprecation utility
**Files:**
- Create: `src/utils/deprecation.js`
- Test: `__tests__/utils/deprecation.test.js`
**Step 1: Write the test**
```javascript
import { deprecatedHandler } from '../../src/utils/deprecation.js'
describe('deprecatedHandler', () => {
let stderrOutput
const originalWrite = process.stderr.write
beforeEach(() => {
stderrOutput = ''
process.stderr.write = (chunk) => { stderrOutput += chunk }
delete process.env.CALIBRE_SUPPRESS_DEPRECATIONS
})
afterEach(() => {
process.stderr.write = originalWrite
delete process.env.CALIBRE_SUPPRESS_DEPRECATIONS
})
test('prints deprecation warning to stderr', async () => {
const inner = jest.fn()
const handler = deprecatedHandler('site pages', 'synthetic pages', inner)
await handler({ site: 'test' })
expect(stderrOutput).toContain('[calibre:deprecated]')
expect(stderrOutput).toContain('site pages')
expect(stderrOutput).toContain('synthetic pages')
expect(inner).toHaveBeenCalledWith({ site: 'test' })
})
test('suppresses warning when CALIBRE_SUPPRESS_DEPRECATIONS is set', async () => {
process.env.CALIBRE_SUPPRESS_DEPRECATIONS = '1'
const inner = jest.fn()
const handler = deprecatedHandler('site pages', 'synthetic pages', inner)
await handler({ site: 'test' })
expect(stderrOutput).toBe('')
expect(inner).toHaveBeenCalled()
})
})
```
**Step 2: Run test to verify it fails**
Run: `npm test -- __tests__/utils/deprecation.test.js`
Expected: FAIL — module not found
**Step 3: Write implementation**
```javascript
import chalk from 'chalk'
const deprecatedHandler = (oldCommand, newCommand, handler) => {
return async (args) => {
if (!process.env.CALIBRE_SUPPRESS_DEPRECATIONS) {
process.stderr.write(
chalk.yellow(
`[calibre:deprecated] "${oldCommand}" has moved to "${newCommand}"\n`
)
)
}
return handler(args)
}
}
export { deprecatedHandler }
```
**Step 4: Run test to verify it passes**
Run: `npm test -- __tests__/utils/deprecation.test.js`
Expected: PASS
**Step 5: Lint**
Run: `npm run lint`
Expected: No errors
**Step 6: Commit**
```bash
git add src/utils/deprecation.js __tests__/utils/deprecation.test.js
git commit -m "feat: add deprecation handler utility for v7 command restructure"
```
---
### Task 2: Create shared option modules
**Files:**
- Create: `src/utils/crux-options.js`
- Create: `src/utils/rum-options.js`
**Step 1: Create CrUX options**
```javascript
const cruxOptions = {
formFactor: {
describe: 'Filter by device type.',
choices: ['desktop', 'phone', 'tablet'],
type: 'string'
},
timePeriod: {
describe: 'History time window.',
choices: [
'three-months',
'six-months',
'nine-months',
'twelve-months',
'eighteen-months',
'twenty-four-months'
],
default: 'six-months',
type: 'string'
}
}
export { cruxOptions }
```
**Step 2: Create RUM options**
```javascript
import { options } from './cli.js'
const rumFilterOptions = {
duration: {
describe: 'Number of days to aggregate.',
default: 7,
type: 'number'
},
dateBin: {
describe: 'Time granularity.',
choices: ['day', 'month'],
default: 'day',
type: 'string'
},
country: {
describe: 'Filter by country code(s) (space-separated, e.g. AU US).',
type: 'array'
},
device: {
describe: 'Filter by device type.',
choices: ['desktop', 'mobile', 'tablet'],
type: 'string'
},
connection: {
describe: 'Filter by connection type.',
type: 'array'
},
path: {
describe: 'Filter by URL path(s) (space-separated).',
type: 'array'
},
pageGrouping: {
describe: 'Filter by page grouping UUID(s) (space-separated).',
type: 'array'
}
}
export { rumFilterOptions }
```
**Step 3: Lint**
Run: `npm run lint`
Expected: No errors
**Step 4: Commit**
```bash
git add src/utils/crux-options.js src/utils/rum-options.js
git commit -m "feat: add shared CrUX and RUM option definitions"
```
---
### Task 3: Create grading view utility
**Files:**
- Create: `src/views/grading.js`
- Test: `__tests__/views/grading.test.js`
**Step 1: Write the test**
```javascript
import { formatGrading } from '../../src/views/grading.js'
describe('formatGrading', () => {
test('returns Good for good grading', () => {
const result = formatGrading('good')
expect(result).toContain('Good')
})
test('returns NI for needs-improvement grading', () => {
const result = formatGrading('needs-improvement')
expect(result).toContain('NI')
})
test('returns Poor for poor grading', () => {
const result = formatGrading('poor')
expect(result).toContain('Poor')
})
test('returns — for null', () => {
const result = formatGrading(null)
expect(result).toBe('—')
})
})
```
**Step 2: Run test to verify it fails**
Run: `npm test -- __tests__/views/grading.test.js`
Expected: FAIL
**Step 3: Write implementation**
```javascript
import chalk from 'chalk'
const formatGrading = (grading) => {
if (!grading) return '—'
switch (grading) {
case 'good':
return chalk.green('Good')
case 'needs-improvement':
return chalk.yellow('NI')
case 'poor':
return chalk.red('Poor')
default:
return grading
}
}
export { formatGrading }
```
**Step 4: Run test to verify it passes**
Run: `npm test -- __tests__/views/grading.test.js`
Expected: PASS
**Step 5: Commit**
```bash
git add src/views/grading.js __tests__/views/grading.test.js
git commit -m "feat: add grading formatter with accessible text+colour output"
```
---
### Task 4: Move synthetic commands and create router
**Files:**
- Move: 17 files from `src/cli/site/` to `src/cli/synthetic/` (see list below)
- Modify: `src/cli/synthetic/download-artifacts.js` (rename command string)
- Modify: `src/cli/synthetic/create-pull-request-review.js` (update hint message)
- Create: `src/cli/synthetic.js`
**Step 1: Create synthetic directory and move files**
```bash
mkdir -p src/cli/synthetic
git mv src/cli/site/pages.js src/cli/synthetic/pages.js
git mv src/cli/site/create-page.js src/cli/synthetic/create-page.js
git mv src/cli/site/update-page.js src/cli/synthetic/update-page.js
git mv src/cli/site/delete-page.js src/cli/synthetic/delete-page.js
git mv src/cli/site/snapshots.js src/cli/synthetic/snapshots.js
git mv src/cli/site/create-snapshot.js src/cli/synthetic/create-snapshot.js
git mv src/cli/site/delete-snapshot.js src/cli/synthetic/delete-snapshot.js
git mv src/cli/site/download-snapshot-artifacts.js src/cli/synthetic/download-artifacts.js
git mv src/cli/site/get-snapshot-metrics.js src/cli/synthetic/get-snapshot-metrics.js
git mv src/cli/site/metrics.js src/cli/synthetic/metrics.js
git mv src/cli/site/test-profiles.js src/cli/synthetic/test-profiles.js
git mv src/cli/site/create-test-profile.js src/cli/synthetic/create-test-profile.js
git mv src/cli/site/update-test-profile.js src/cli/synthetic/update-test-profile.js
git mv src/cli/site/delete-test-profile.js src/cli/synthetic/delete-test-profile.js
git mv src/cli/site/pull-request-reviews.js src/cli/synthetic/pull-request-reviews.js
git mv src/cli/site/create-pull-request-review.js src/cli/synthetic/create-pull-request-review.js
git mv src/cli/site/pull-request-review.js src/cli/synthetic/pull-request-review.js
```
**Step 2: Update command string in download-artifacts.js**
In `src/cli/synthetic/download-artifacts.js`, change:
```javascript
const command = 'download-snapshot-artifacts [options]'
```
to:
```javascript
const command = 'download-artifacts [options]'
```
**Step 3: Update hint in create-pull-request-review.js**
In `src/cli/synthetic/create-pull-request-review.js`, change:
```javascript
`View progress by running \`calibre site pull-request-review ${args.branch} --site=${args.site}\``
```
to:
```javascript
`View progress by running \`calibre synthetic pull-request-review ${args.branch} --site=${args.site}\``
```
**Step 4: Create the synthetic group router**
Write `src/cli/synthetic.js`:
```javascript
import * as Pages from './synthetic/pages.js'
import * as CreatePage from './synthetic/create-page.js'
import * as UpdatePage from './synthetic/update-page.js'
import * as DeletePage from './synthetic/delete-page.js'
import * as Snapshots from './synthetic/snapshots.js'
import * as CreateSnapshot from './synthetic/create-snapshot.js'
import * as DeleteSnapshot from './synthetic/delete-snapshot.js'
import * as DownloadArtifacts from './synthetic/download-artifacts.js'
import * as GetSnapshotMetrics from './synthetic/get-snapshot-metrics.js'
import * as Metrics from './synthetic/metrics.js'
import * as TestProfiles from './synthetic/test-profiles.js'
import * as CreateTestProfile from './synthetic/create-test-profile.js'
import * as UpdateTestProfile from './synthetic/update-test-profile.js'
import * as DeleteTestProfile from './synthetic/delete-test-profile.js'
import * as PullRequestReviews from './synthetic/pull-request-reviews.js'
import * as CreatePullRequestReview from './synthetic/create-pull-request-review.js'
import * as PullRequestReview from './synthetic/pull-request-review.js'
const commands = [
Pages,
CreatePage,
UpdatePage,
DeletePage,
Snapshots,
CreateSnapshot,
DeleteSnapshot,
DownloadArtifacts,
GetSnapshotMetrics,
Metrics,
TestProfiles,
CreateTestProfile,
UpdateTestProfile,
DeleteTestProfile,
PullRequestReviews,
CreatePullRequestReview,
PullRequestReview
]
const command = 'synthetic <command>'
const desc =
'Synthetic monitoring — manage scheduled Lighthouse tests, Pages, Test Profiles, Snapshots, and Pull Request Reviews.'
const builder = yargs => {
return yargs.commands(commands)
}
const handler = () => {}
export { command, desc, builder, handler, commands }
```
**Step 5: Lint**
Run: `npm run lint`
Expected: No errors
**Step 6: Commit**
```bash
git add -A
git commit -m "refactor: move synthetic commands from site/ to synthetic/"
```
---
### Task 5: Move deploy commands and create router
**Files:**
- Move: 3 files from `src/cli/site/` to `src/cli/deploy/`
- Modify: command strings and pagination hint
- Create: `src/cli/deploy.js`
**Step 1: Create deploy directory and move files**
```bash
mkdir -p src/cli/deploy
git mv src/cli/site/deploys.js src/cli/deploy/list.js
git mv src/cli/site/create-deploy.js src/cli/deploy/create.js
git mv src/cli/site/delete-deploy.js src/cli/deploy/delete.js
```
**Step 2: Update command strings**
In `src/cli/deploy/list.js`, change:
```javascript
const command = 'deploys [options]'
const describe = 'List all deployments for a Site.'
```
to:
```javascript
const command = 'list [options]'
const describe = 'List all deployments for a Site.'
```
Also update the pagination hint from:
```javascript
`To see deploys after ${
lastDeploy.revision || lastDeploy.id
}, run: calibre site deploys --site=calibre --cursor=${
index.pageInfo.endCursor
}`
```
to:
```javascript
`To see deploys after ${
lastDeploy.revision || lastDeploy.id
}, run: calibre deploy list --site=${args.site} --cursor=${
index.pageInfo.endCursor
}`
```
In `src/cli/deploy/create.js`, change:
```javascript
const command = 'create-deploy [options]'
const describe = 'Create a deployment.'
```
to:
```javascript
const command = 'create [options]'
const describe = 'Create a deployment.'
```
In `src/cli/deploy/delete.js`, change:
```javascript
const command = 'delete-deploy [options]'
const describe = 'Delete a deploy from a selected Site.'
```
to:
```javascript
const command = 'delete [options]'
const describe = 'Delete a deploy from a selected Site.'
```
**Step 3: Create the deploy group router**
Write `src/cli/deploy.js`:
```javascript
import * as DeployList from './deploy/list.js'
import * as DeployCreate from './deploy/create.js'
import * as DeployDelete from './deploy/delete.js'
const commands = [DeployList, DeployCreate, DeployDelete]
const command = 'deploy <command>'
const desc =
'Manage deployment markers — annotate your performance charts across Synthetic, CrUX, and RUM data.'
const builder = yargs => {
return yargs.commands(commands)
}
const handler = () => {}
export { command, desc, builder, handler, commands }
```
**Step 4: Lint**
Run: `npm run lint`
Expected: No errors
**Step 5: Commit**
```bash
git add -A
git commit -m "refactor: move deploy commands from site/ to deploy/"
```
---
### Task 6: Wire deprecations in site.js and update cli-commands.js
**Files:**
- Modify: `src/cli/site.js`
- Modify: `src/cli-commands.js`
**Step 1: Rewrite site.js**
Replace all contents of `src/cli/site.js` with the slimmed version containing hidden deprecated wrappers. The file imports from the new locations (`./synthetic/`, `./deploy/`), exports only `create`, `list`, `delete` as visible commands, and registers all 20 deprecated commands with `describe: false` and `deprecatedHandler`-wrapped handlers.
See the design doc Phase 4 for the full structure. Every deprecated command must:
- Use the **old** command string (e.g., `'pages [options]'`, `'deploys [options]'`, `'create-deploy [options]'`)
- Set `describe: false`
- Use `builder` from the original module
- Wrap `handler` with `deprecatedHandler(oldPath, newPath, originalHandler)`
Full list of 20 deprecated entries:
| Old command string | Old path | New path |
|---|---|---|
| `pages [options]` | `site pages` | `synthetic pages` |
| `create-page <name> [options]` | `site create-page` | `synthetic create-page` |
| `update-page [options]` | `site update-page` | `synthetic update-page` |
| `delete-page [options]` | `site delete-page` | `synthetic delete-page` |
| `snapshots [options]` | `site snapshots` | `synthetic snapshots` |
| `create-snapshot [options]` | `site create-snapshot` | `synthetic create-snapshot` |
| `delete-snapshot [options]` | `site delete-snapshot` | `synthetic delete-snapshot` |
| `download-snapshot-artifacts [options]` | `site download-snapshot-artifacts` | `synthetic download-artifacts` |
| `get-snapshot-metrics [options]` | `site get-snapshot-metrics` | `synthetic get-snapshot-metrics` |
| `metrics [options]` | `site metrics` | `synthetic metrics` |
| `test-profiles [options]` | `site test-profiles` | `synthetic test-profiles` |
| `create-test-profile <name> [options]` | `site create-test-profile` | `synthetic create-test-profile` |
| `update-test-profile [options]` | `site update-test-profile` | `synthetic update-test-profile` |
| `delete-test-profile [options]` | `site delete-test-profile` | `synthetic delete-test-profile` |
| `pull-request-reviews [options]` | `site pull-request-reviews` | `synthetic pull-request-reviews` |
| `create-pull-request-review [options]` | `site create-pull-request-review` | `synthetic create-pull-request-review` |
| `pull-request-review <branch>` | `site pull-request-review` | `synthetic pull-request-review` |
| `deploys [options]` | `site deploys` | `deploy list` |
| `create-deploy [options]` | `site create-deploy` | `deploy create` |
| `delete-deploy [options]` | `site delete-deploy` | `deploy delete` |
**Step 2: Update cli-commands.js**
Add imports for `Synthetic`, `Deploy` (and stub imports for `Crux`, `Rum` — these will be created in Tasks 9-10 but we need the import to exist). For now, only add `Synthetic` and `Deploy` — `Crux` and `Rum` will be added in their respective tasks.
```javascript
import * as ConnectionList from './cli/connection-list.js'
import * as DeviceList from './cli/device-list.js'
import * as LocationList from './cli/location-list.js'
import * as MetricList from './cli/metric-list.js'
import * as Request from './cli/request.js'
import * as Site from './cli/site.js'
import * as Synthetic from './cli/synthetic.js'
import * as Deploy from './cli/deploy.js'
import * as Team from './cli/team.js'
import * as Test from './cli/test.js'
import * as Token from './cli/token.js'
const commands = [
Site,
Synthetic,
Deploy,
Test,
Team,
ConnectionList,
DeviceList,
LocationList,
MetricList,
Token,
Request
]
export default commands
```
**Step 3: Lint and test**
Run: `npm run lint && npm test`
Expected: lint passes, existing tests may need snapshot updates (see Task 7)
**Step 4: Commit**
```bash
git add src/cli/site.js src/cli-commands.js
git commit -m "feat: wire deprecated aliases in site.js, add synthetic and deploy to cli-commands"
```
---
### Task 7: Update existing tests for moved commands
**Files:**
- Move: `__tests__/cli/site/snapshots.test.js` → `__tests__/cli/synthetic/snapshots.test.js`
- Move: `__tests__/cli/site/pages.test.js` → `__tests__/cli/synthetic/pages.test.js`
- Move: `__tests__/cli/site/get-snapshot-metrics.test.js` → `__tests__/cli/synthetic/get-snapshot-metrics.test.js`
- Create: `__tests__/cli/deprecation.test.js`
**Step 1: Move test files and update command paths**
```bash
mkdir -p __tests__/cli/synthetic
git mv __tests__/cli/site/snapshots.test.js __tests__/cli/synthetic/snapshots.test.js
git mv __tests__/cli/site/pages.test.js __tests__/cli/synthetic/pages.test.js
git mv __tests__/cli/site/get-snapshot-metrics.test.js __tests__/cli/synthetic/get-snapshot-metrics.test.js
```
In each moved test file, update the CLI args from `'site <subcommand>'` to `'synthetic <subcommand>'`. For example in `snapshots.test.js`:
- Change `args: 'site snapshots --site=test'` to `args: 'synthetic snapshots --site=test'`
In `pages.test.js`:
- Change `args: 'site pages --site=test'` to `args: 'synthetic pages --site=test'`
In `get-snapshot-metrics.test.js`:
- Change `args: 'site get-snapshot-metrics --site=test --snapshot=1000'` to `args: 'synthetic get-snapshot-metrics --site=test --snapshot=1000'`
**Step 2: Delete old snapshots and regenerate**
```bash
rm -rf __tests__/cli/synthetic/__snapshots__
npm test -- --updateSnapshot __tests__/cli/synthetic/
```
**Step 3: Write deprecation integration test**
Create `__tests__/cli/deprecation.test.js`:
```javascript
import {
runCLI,
setupIntegrationServer,
teardownIntegrationServer
} from '../utils'
import listPages from '../fixtures/listPages.json'
describe('deprecated commands', () => {
beforeAll(async () => await setupIntegrationServer(listPages))
afterAll(async () => await teardownIntegrationServer())
test('site pages shows deprecation warning on stderr', async () => {
const stderr = await runCLI({
args: 'site pages --site=test',
testForError: true
})
expect(stderr).toContain('[calibre:deprecated]')
expect(stderr).toContain('synthetic pages')
})
test('site pages still returns valid output on stdout', async () => {
const stdout = await runCLI({
args: 'site pages --site=test'
})
expect(stdout).toBeTruthy()
})
})
```
**Step 4: Run all tests**
Run: `npm test`
Expected: PASS
**Step 5: Commit**
```bash
git add -A
git commit -m "test: update tests for synthetic/deploy restructure, add deprecation tests"
```
---
### Task 8: Enhance site list with monitoring status
**Files:**
- Modify: `src/api/site.js` (LIST_QUERY)
- Modify: `src/cli/site/list.js` (table output)
**Step 1: Update LIST_QUERY in `src/api/site.js`**
Add `monitoringStatus` to the query:
```graphql
query {
organisation {
sites {
name
slug
createdAt
team {
name
slug
}
monitoringStatus {
synthetic
crux
rum
}
}
}
}
```
**Step 2: Update `src/cli/site/list.js` table**
Add a `monitoring` column that shows active statuses. In the `rows` map:
```javascript
const rows = index.map(row => {
const statuses = []
if (row.monitoringStatus?.synthetic) statuses.push(chalk.green('synthetic'))
if (row.monitoringStatus?.crux) statuses.push(chalk.green('crux'))
if (row.monitoringStatus?.rum) statuses.push(chalk.green('rum'))
return {
slug: chalk.grey(row.slug),
name: row.name,
monitoring: statuses.join(' ') || chalk.grey('—'),
created: `${dateFormat(new Date(row.createdAt), 'h:mma d-MMM-yyyy')}`
}
})
```
JSON output passes through the raw API response unchanged (it already includes `monitoringStatus`).
**Step 3: Lint and test**
Run: `npm run lint && npm test`
Expected: PASS
**Step 4: Commit**
```bash
git add src/api/site.js src/cli/site/list.js
git commit -m "feat: show monitoring status (synthetic/crux/rum) in site list"
```
---
### Task 9: Implement CrUX API module and commands
**Files:**
- Create: `src/api/crux.js`
- Create: `src/cli/crux.js`
- Create: `src/cli/crux/summary.js`
- Create: `src/cli/crux/history.js`
- Create: `src/cli/crux/urls.js`
- Create: `src/cli/crux/url.js`
- Create: `__tests__/fixtures/cruxSummary.json`
- Create: `__tests__/cli/crux/summary.test.js`
- Modify: `src/cli-commands.js` (add Crux)
This is a large task. See the design doc Phases 8 for full GraphQL queries (derived from the queries provided in the user's initial message). Each command follows the exact same pattern as existing commands: spinner → API call → format output (JSON/CSV/table).
**Step 1: Create API module `src/api/crux.js`**
Four functions: `summary`, `history`, `urls`, `url`. Each builds a GraphQL query string and calls `request()`. Query shapes are derived from `GetCruxData`, `ListCruxUrls`, and `GetCruxUrlData` as provided by the user.
Map `--form-factor` CLI values to GraphQL enum: `desktop` → `DESKTOP`, `phone` → `PHONE`, `tablet` → `TABLET`.
Map `--time-period` CLI values to GraphQL enum: `three-months` → `THREE_MONTHS`, etc.
**Step 2: Create CLI commands**
Each command file in `src/cli/crux/` follows the standard pattern:
- Import from `../../api/crux.js`
- Import `options` from `../../utils/cli.js` and `cruxOptions` from `../../utils/crux-options.js`
- Export `command`, `describe`, `builder`, `handler`
- Handler: spinner → API call → JSON/CSV/table output
Key details per command:
- `summary.js`: describe = `'Display Chrome UX Report (CrUX) origin-level performance data and Core Web Vitals assessment.'`
- `history.js`: describe = `'Display Chrome UX Report (CrUX) historical trends for a site.'`; add `--limit` option (default 25)
- `urls.js`: describe = `'List Chrome UX Report (CrUX) monitored URLs with their metrics and Core Web Vitals assessment.'`
- `url.js`: command = `'url <uuid> [options]'`; describe = `'Display Chrome UX Report (CrUX) data for a specific monitored URL.'`
When API returns null/empty data, print: `No CrUX data available for this site. CrUX requires sufficient Chrome user traffic.`
**Step 3: Create group router `src/cli/crux.js`**
```javascript
import * as Summary from './crux/summary.js'
import * as History from './crux/history.js'
import * as Urls from './crux/urls.js'
import * as Url from './crux/url.js'
const commands = [Summary, History, Urls, Url]
const command = 'crux <command>'
const desc =
'Chrome UX Report (CrUX) — real-world performance data from Chrome users.'
const builder = yargs => {
return yargs.commands(commands)
}
const handler = () => {}
export { command, desc, builder, handler, commands }
```
**Step 4: Add Crux to cli-commands.js**
Add `import * as Crux from './cli/crux.js'` and add `Crux` to the commands array (after `Deploy`).
**Step 5: Create test fixtures and tests**
Create `__tests__/fixtures/cruxSummary.json` with a mock response matching the `GetCruxData` query shape. Write `__tests__/cli/crux/summary.test.js` following the existing test pattern (mock server, `runCLI`, snapshot).
**Step 6: Lint and test**
Run: `npm run lint && npm test`
Expected: PASS
**Step 7: Commit**
```bash
git add -A
git commit -m "feat: add CrUX commands (summary, history, urls, url)"
```
---
### Task 10: Implement RUM API module and commands
**Files:**
- Create: `src/api/rum.js`
- Create: `src/cli/rum.js`
- Create: `src/cli/rum/summary.js`
- Create: `src/cli/rum/history.js`
- Create: `src/cli/rum/pages.js`
- Create: `src/cli/rum/config.js`
- Create: `__tests__/fixtures/rumSummary.json`
- Create: `__tests__/cli/rum/summary.test.js`
- Modify: `src/cli-commands.js` (add Rum)
Follows the same pattern as Task 9.
**Step 1: Create API module `src/api/rum.js`**
Four functions: `summary`, `history`, `pages`, `config`. Query shapes from `GetSiteRumDashboard`, `GetSiteRumDashboardHistory`, `GetSiteRumPages`, and site rumConfig field.
Build the `RumFilterInput` from CLI flags:
- `--duration` → `filter.duration` (number)
- `--date-bin` → `filter.dateBin` (`day` → `DAY`, `month` → `MONTH`)
- `--country` → `filter.countryCode` (array)
- `--device` → `filter.isDesktopDevice`/`isMobileDevice`/`isTabletDevice` (boolean flags)
- `--path` → `filter.path` (array)
- `--page-grouping` → passed as `pageGroupingUuids` variable
Metrics default: `['lcp', 'cls', 'inp', 'ttfb', 'fcp', 'rtt']`
**Step 2: Create CLI commands**
Each command in `src/cli/rum/`:
- `summary.js`: Display live visitors, countries, aggregate metrics, UX ratings. Table format: `Metric | p75 | Rating | Good% | NI% | Poor%`
- `history.js`: Display per-date metrics. Add `--limit` (default 25). Table format: `date | lcp | cls | inp | ttfb | fcp | sessions`
- `pages.js`: Display page-level breakdown. Options: `--sort-by` (default `sessionCount`), `--limit` (default 25), `--offset` (default 0). Pagination hint on truncated output.
- `config.js`: Display RUM configuration. Simple key-value output.
When API returns null/empty: print `No RUM data available. Check that RUM is enabled for this site with: calibre rum config --site=<slug>`
**Step 3: Create group router `src/cli/rum.js`**
```javascript
import * as Summary from './rum/summary.js'
import * as History from './rum/history.js'
import * as Pages from './rum/pages.js'
import * as Config from './rum/config.js'
const commands = [Summary, History, Pages, Config]
const command = 'rum <command>'
const desc =
'Real User Metrics (RUM) — field performance data from your real users.'
const builder = yargs => {
return yargs.commands(commands)
}
const handler = () => {}
export { command, desc, builder, handler, commands }
```
**Step 4: Add Rum to cli-commands.js**
Add `import * as Rum from './cli/rum.js'` and add `Rum` to the commands array (after `Crux`).
**Step 5: Create test fixtures and tests**
**Step 6: Lint and test**
Run: `npm run lint && npm test`
Expected: PASS
**Step 7: Commit**
```bash
git add -A
git commit -m "feat: add RUM commands (summary, history, pages, config)"
```
---
### Task 11: Enhance metric-list with --type flag
**Files:**
- Modify: `src/api/metric.js`
- Modify: `src/cli/metric-list.js`
**Step 1: Add CrUX and RUM query support to `src/api/metric.js`**
Add new queries for `cruxMetrics` and `rumMetrics` root fields. Add a `type` parameter to the `list` function that selects which query to run.
**Step 2: Add --type flag to metric-list.js builder**
```javascript
type: {
describe: 'Filter metrics by data source.',
choices: ['synthetic', 'crux', 'rum']
}
```
Pass `args.type` to the API `list()` call.
**Step 3: Lint and test**
Run: `npm run lint && npm test`
Expected: PASS
**Step 4: Commit**
```bash
git add src/api/metric.js src/cli/metric-list.js
git commit -m "feat: add --type flag to metric-list for filtering by data source"
```
---
### Task 12: Update Node.js API exports
**Files:**
- Modify: `index.js`
**Step 1: Add new exports**
```javascript
export * as Crux from './src/api/crux.js'
export * as Rum from './src/api/rum.js'
```
**Step 2: Build**
Run: `npm run build`
Expected: esbuild bundles `dist/index.cjs` without errors
**Step 3: Commit**
```bash
git add index.js
git commit -m "feat: export Crux and Rum from Node.js API"
```
---
### Task 13: Update documentation
**Files:**
- Modify: `README.md`
- Modify: `package.json` (version, description, keywords)
- Modify: `CHANGELOG.md`
- Create: `examples/bash/crux-summary.sh`
- Create: `examples/bash/rum-pages.sh`
- Create: `examples/bash/deploy-create.sh`
- Modify: `examples/bash/README.md`
- Create: `examples/nodejs/crux/summary.js`
- Create: `examples/nodejs/crux/history.js`
- Create: `examples/nodejs/crux/urls.js`
- Create: `examples/nodejs/rum/summary.js`
- Create: `examples/nodejs/rum/pages.js`
- Create: `examples/nodejs/rum/config.js`
- Modify: `examples/nodejs/README.md`
- Delete: `CHANGELOG-v7-DRAFT.md`
**Step 1: Update package.json**
- `"version": "7.0.0"`
- `"description": "Performance monitoring with Synthetic testing, Chrome UX Report, and Real User Metrics"`
- Add keywords: `"rum"`, `"crux"`, `"web-vitals"`, `"real-user-monitoring"`
**Step 2: Update README.md**
- Update features to mention three pillars
- Update usage examples with new namespaces
- Update package exports to show `Crux` and `Rum`
**Step 3: Update CHANGELOG.md**
Prepend content from `CHANGELOG-v7-DRAFT.md` to `CHANGELOG.md`. Delete `CHANGELOG-v7-DRAFT.md`.
**Step 4: Create bash examples**
Each example script follows the pattern in `examples/bash/create-test.sh`: set `-euo pipefail`, require `CALIBRE_API_TOKEN`, call CLI with `--json`, pipe through `jq`.
**Step 5: Create Node.js examples**
Each example follows the pattern in existing `examples/nodejs/` files: import from `'calibre'`, call API function, log result.
**Step 6: Update example READMEs**
Add new examples to the listings in both `examples/bash/README.md` and `examples/nodejs/README.md`.
**Step 7: Regenerate CLI_COMMANDS.md**
Run: `npm run generate-cli-md`
Verify:
- No deprecated `site <command>` entries
- All new commands present
- `site` section shows only `create`, `list`, `delete`
**Step 8: Commit**
```bash
git add -A
git commit -m "docs: update README, CHANGELOG, examples, and CLI_COMMANDS.md for v7.0.0"
```
---
### Task 14: Final verification
**Step 1: Run full lint**
Run: `npm run lint`
Expected: Zero warnings, zero errors
**Step 2: Run full test suite**
Run: `npm test`
Expected: All tests pass
**Step 3: Run build**
Run: `npm run build`
Expected: `dist/index.cjs` built successfully
**Step 4: Regenerate and diff CLI_COMMANDS.md**
Run: `npm run generate-cli-md`
Run: `git diff CLI_COMMANDS.md`
Verify no deprecated commands appear, all new commands are present.
**Step 5: Smoke test help output**
Run: `node src/cli.js --help`
Verify: `synthetic`, `deploy`, `crux`, `rum` appear. No deprecated entries.
Run: `node src/cli.js site --help`
Verify: Only `create`, `list`, `delete` shown.
Run: `node src/cli.js synthetic --help`
Verify: All 17 subcommands listed.
**Step 6: Smoke test deprecation**
Run: `node src/cli.js site pages --site=test 2>&1 | head -1`
Verify: `[calibre:deprecated] "site pages" has moved to "synthetic pages"`
**Step 7: Commit any final adjustments**
```bash
git add -A
git commit -m "chore: final verification and cleanup for v7.0.0"
```