@nuxtseo/cli 0.5.8 → 0.5.9

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/dist/cli.js CHANGED
@@ -376,7 +376,7 @@ async function interactiveBare(runtime, globals) {
376
376
  message: 'Account and setup',
377
377
  options: [
378
378
  { value: 'site', label: 'Switch Site', hint: config.value.siteId ?? 'none selected' },
379
- { value: 'usage', label: 'Account usage' },
379
+ { value: 'usage', label: 'Team usage' },
380
380
  { value: 'whoami', label: 'Credential and scopes' },
381
381
  { value: 'config', label: 'Configuration' },
382
382
  { value: 'logout', label: 'Log out' },
package/dist/commands.js CHANGED
@@ -1374,11 +1374,10 @@ async function actionDismiss(runtime, globals, args) {
1374
1374
  return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
1375
1375
  if (!args.artifactVersion)
1376
1376
  return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
1377
- if (args.note !== undefined && (!args.note.trim() || args.note.trim().length > 2000))
1378
- return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters.', undefined, 'invalid_cli_input');
1379
- const confirmed = await confirmMutation(runtime, globals, args.note
1380
- ? `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`
1381
- : `Dismiss action ${args.actionId}? The page stays removed.`);
1377
+ const note = args.note?.trim();
1378
+ if (!note || note.length > 2000)
1379
+ return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters that say why this action is intentional or incorrect.', undefined, 'invalid_cli_input');
1380
+ const confirmed = await confirmMutation(runtime, globals, `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`);
1382
1381
  if (confirmed._tag === 'Err')
1383
1382
  return confirmed;
1384
1383
  if (!confirmed.value) {
@@ -1390,7 +1389,7 @@ async function actionDismiss(runtime, globals, args) {
1390
1389
  return resolved;
1391
1390
  const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
1392
1391
  params: { siteId: resolved.value.siteId, actionId: args.actionId },
1393
- body: { artifactVersion: args.artifactVersion, ...(args.note !== undefined ? { note: args.note.trim() } : {}) },
1392
+ body: { artifactVersion: args.artifactVersion, note },
1394
1393
  }, { signal: runtime.requestSignal }));
1395
1394
  return present(runtime, globals, response, value => renderActionDismissal(value.data));
1396
1395
  }
@@ -1714,11 +1713,11 @@ export function createRootCommand(runtime, globals, execution) {
1714
1713
  run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
1715
1714
  }),
1716
1715
  dismiss: defineCommand({
1717
- meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note, or confirm an unlinked page removal' },
1716
+ meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note that says why' },
1718
1717
  args: {
1719
1718
  'actionId': positional('action-id', 'Action ID'),
1720
1719
  'artifact-version': { type: 'string', description: 'The artifactVersion returned with this action' },
1721
- 'note': { type: 'string', description: 'Why this action is intentional or incorrect, 1 to 2000 characters' },
1720
+ 'note': { type: 'string', description: 'Required. Why this action is intentional or incorrect, 1 to 2000 characters' },
1722
1721
  },
1723
1722
  run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
1724
1723
  actionId: args.actionId,
@@ -2369,7 +2368,7 @@ export function createRootCommand(runtime, globals, execution) {
2369
2368
  },
2370
2369
  }),
2371
2370
  usage: defineCommand({
2372
- meta: { name: 'usage', description: 'Read account usage' },
2371
+ meta: { name: 'usage', description: 'Read Team usage' },
2373
2372
  args: {
2374
2373
  group: {
2375
2374
  type: 'enum',
package/dist/render.js CHANGED
@@ -106,20 +106,56 @@ export function renderPageScan(data) {
106
106
  ...data.scans.map(scan => `${scan.strategy}: ${scan.scanId}`),
107
107
  ].join('\n');
108
108
  }
109
+ /** A lab duration in milliseconds. Null is unmeasured, never zero. */
110
+ function labMs(value) {
111
+ return value === null ? 'not measured' : `${value}ms`;
112
+ }
113
+ function labScanLine(label, scan) {
114
+ if (!scan?.completedAt)
115
+ return `${label} no complete Scan`;
116
+ return `${label} perf ${scan.perf ?? 'none'} LCP ${labMs(scan.lcp)} TBT ${labMs(scan.tbt)} CLS ${scan.cls ?? 'not measured'} scanned ${scan.completedAt}`;
117
+ }
109
118
  export function renderPerformance(data) {
110
- if (!data.health)
119
+ const blocks = [];
120
+ if (data.health) {
121
+ blocks.push([
122
+ 'Mobile medians across monitored Pages',
123
+ fields([
124
+ ['Scanned pages', data.health.scannedPageCount],
125
+ ['Last scan', data.health.lastScanAt],
126
+ ['Performance median', data.health.perf.median],
127
+ ['Performance worst', data.health.perf.worst],
128
+ ['Accessibility median', data.health.a11y.median],
129
+ ['SEO median', data.health.seo.median],
130
+ ['LCP median', data.health.lcp.median],
131
+ ['TBT median', data.health.tbt.median],
132
+ ['CLS median', data.health.cls.median],
133
+ ]),
134
+ ].join('\n'));
135
+ }
136
+ if (data.homepage || data.homepageDesktop) {
137
+ blocks.push([
138
+ 'Homepage',
139
+ labScanLine(' mobile ', data.homepage),
140
+ labScanLine(' desktop', data.homepageDesktop),
141
+ ].join('\n'));
142
+ }
143
+ if (data.routes.length || data.routesDesktop.length) {
144
+ const desktopByPage = new Map(data.routesDesktop.map(route => [route.pageId, route]));
145
+ const mobileByPage = new Map(data.routes.map(route => [route.pageId, route]));
146
+ const pages = [...data.routes, ...data.routesDesktop.filter(route => !mobileByPage.has(route.pageId))];
147
+ blocks.push([
148
+ `Monitored Pages (${pages.length})`,
149
+ ...pages.flatMap(page => [
150
+ ` ${page.path}`,
151
+ labScanLine(' mobile ', mobileByPage.get(page.pageId)),
152
+ labScanLine(' desktop', desktopByPage.get(page.pageId)),
153
+ ]),
154
+ ].join('\n'));
155
+ }
156
+ if (!blocks.length)
111
157
  return `No stored performance overview. Monitored routes: ${data.routes.length}.`;
112
- return fields([
113
- ['Scanned pages', data.health.scannedPageCount],
114
- ['Last scan', data.health.lastScanAt],
115
- ['Performance median', data.health.perf.median],
116
- ['Performance worst', data.health.perf.worst],
117
- ['Accessibility median', data.health.a11y.median],
118
- ['SEO median', data.health.seo.median],
119
- ['LCP median', data.health.lcp.median],
120
- ['TBT median', data.health.tbt.median],
121
- ['CLS median', data.health.cls.median],
122
- ]);
158
+ return blocks.join('\n\n');
123
159
  }
124
160
  export function renderSearchStatus(data) {
125
161
  if (data._tag === 'Connected')
@@ -669,8 +705,9 @@ export function renderFieldVitalFindings(data) {
669
705
  if (data.findings.length === 0)
670
706
  return data.message ?? 'No failing field vitals attributed to an element.';
671
707
  const shown = `${data.offset + 1}-${data.offset + data.findings.length} of ${data.total}`;
708
+ const window = data.window ? `, ${data.window.from} to ${data.window.to}` : '';
672
709
  return [
673
- `Field vital findings (${shown}, source ${data.source ?? 'unknown'})`,
710
+ `Field vital findings (${shown}, source ${data.source ?? 'unknown'}${window})`,
674
711
  ...data.findings.map(finding => [
675
712
  `${finding.metric.toUpperCase()} ${finding.severity} ${vitalValue(finding.metric, finding.p75)} ${finding.route}`,
676
713
  ` selector ${finding.selector}`,
@@ -898,6 +935,12 @@ export function renderPageIssues(data) {
898
935
  ].filter(Boolean).join('\n')),
899
936
  ].join('\n') + truncated;
900
937
  }
938
+ /** A failed Scan names its kind and message. Other states carry no failure row. */
939
+ function scanFailureRows(scan) {
940
+ if (scan.status !== 'failed')
941
+ return [];
942
+ return [['failure', `${scan.failureKind ?? 'unknown kind'}: ${scan.error ?? 'no message recorded'}`]];
943
+ }
901
944
  export function renderScans(data) {
902
945
  if (data.scans.length === 0)
903
946
  return 'No Lighthouse Scans retained for this Site.';
@@ -907,6 +950,9 @@ export function renderScans(data) {
907
950
  `${scan.completedAt ?? 'pending'} ${scan.strategy} ${scan.status} ${scan.url}`,
908
951
  ` ${fields([
909
952
  ['id', scan.id],
953
+ ['trigger', scan.trigger],
954
+ ['lighthouse', scan.lighthouseVersion],
955
+ ...scanFailureRows(scan),
910
956
  ['performance', scan.scores.perf],
911
957
  ['accessibility', scan.scores.a11y],
912
958
  ['best practices', scan.scores.bestPractices],
@@ -914,11 +960,20 @@ export function renderScans(data) {
914
960
  ['lcp', scan.lcp],
915
961
  ['tbt', scan.tbt],
916
962
  ['cls', scan.cls],
917
- ['error', scan.error],
918
963
  ]).split('\n').join('\n ')}`,
919
964
  ].join('\n')),
920
965
  ].join('\n');
921
966
  }
967
+ function renderScanComparison(comparison) {
968
+ if (!comparison || comparison._tag === 'none')
969
+ return 'Previous Scan none compared';
970
+ if (comparison._tag === 'incompatible')
971
+ return `Previous Scan ${comparison.previousScanId ?? 'unknown'} not comparable: ${comparison.reasons.join(', ')}`;
972
+ return [
973
+ `Since previous Scan ${comparison.previousScanId ?? 'unknown'} ${comparison.severity}`,
974
+ ...comparison.changes.map(change => ` ${change.metric} ${change.previous} to ${change.current} ${change.delta > 0 ? '+' : ''}${change.delta} ${change.severity}`),
975
+ ].join('\n');
976
+ }
922
977
  export function renderMonitoredPages(data) {
923
978
  if (data.pages.length === 0)
924
979
  return 'No monitored Pages. Start one with `nuxtseo page scan <url>`.';
@@ -934,6 +989,9 @@ export function renderScanDetail(data) {
934
989
  ['URL', data.url],
935
990
  ['Strategy', data.strategy],
936
991
  ['Status', data.status],
992
+ ...scanFailureRows(data).map(([, failure]) => ['Failure', failure]),
993
+ ['Trigger', data.trigger],
994
+ ['Lighthouse', data.lighthouseVersion],
937
995
  ['Completed', data.completedAt],
938
996
  ['Performance', data.scores.perf],
939
997
  ['Accessibility', data.scores.a11y],
@@ -947,6 +1005,8 @@ export function renderScanDetail(data) {
947
1005
  ['TTFB', data.ttfb],
948
1006
  ]),
949
1007
  ];
1008
+ if (data.status === 'complete')
1009
+ blocks.push(renderScanComparison(data.comparison));
950
1010
  if (data.lcpElement)
951
1011
  blocks.push(`LCP element ${data.lcpElement.selector ?? data.lcpElement.nodeLabel ?? 'unknown'}`);
952
1012
  if (data.observedTrace?._tag === 'available') {
@@ -968,13 +1028,15 @@ export function renderScanDetail(data) {
968
1028
  if (waste.length) {
969
1029
  blocks.push([
970
1030
  `Wasteful assets (${waste.length})`,
971
- ...waste.map(issue => ` ${issue.wastedMs ?? 0}ms ${issue.wastedBytes ?? 0}b ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
1031
+ ...waste.map(issue => ` ${issue.wastedMs === null ? 'time not measured' : `${issue.wastedMs}ms`} ${issue.wastedBytes === null ? 'bytes not measured' : `${issue.wastedBytes}b`} ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
972
1032
  ].join('\n'));
973
1033
  }
974
1034
  if (data.thirdParties.length) {
1035
+ // A null blocking time is unmeasured. Printing it as 0ms read as "this
1036
+ // script never blocks".
975
1037
  blocks.push([
976
1038
  `Third parties (${data.thirdParties.length})`,
977
- ...data.thirdParties.map(script => ` ${script.blockingTime ?? 0}ms blocking ${script.entity ?? 'unknown'}`),
1039
+ ...data.thirdParties.map(script => ` ${script.blockingTime === null ? 'blocking not measured' : `${script.blockingTime}ms blocking`} ${script.entity ?? 'unknown'}`),
978
1040
  ].join('\n'));
979
1041
  }
980
1042
  for (const [label, checks] of [['Accessibility failures', data.accessibility], ['Best-practices failures', data.bestPractices]]) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.5.8",
4
+ "version": "0.5.9",
5
5
  "description": "Command line interface for the Nuxt SEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.8.0",
36
- "@nuxtseo/protocol": "^0.5.8",
37
- "@nuxtseo/sdk": "^0.5.8",
36
+ "@nuxtseo/protocol": "^0.5.9",
37
+ "@nuxtseo/sdk": "^0.5.9",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -211,21 +211,21 @@ The server verifies the claim; the CLI never marks anything fixed. Resolve
211
211
  sends the current `artifactVersion` for you. If the evidence moved, it exits
212
212
  `5` with `stale_evidence`: re-run step 3 and decide again.
213
213
 
214
- If the fix is a deliberate removal, dismiss instead. Read `artifactVersion`
215
- from `actions show`, then:
214
+ If the owner confirms an action is intentional or incorrect, dismiss it. Read
215
+ `artifactVersion` from `actions show`, then record the reason:
216
216
 
217
217
  ```sh
218
- nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
218
+ nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --note "<reason>" --yes --json
219
219
  ```
220
220
 
221
- Without a note, the server admits a dismiss only for a broken-page action with
222
- complete 404 or 410 evidence and no internal referrers.
221
+ The note is required. Review the exact scope first. A note dismisses the whole
222
+ action, so never use it to hide a mix of intentional and broken URLs. Dismissal
223
+ keeps the observations and does not verify a fix. The dashboard can restore the
224
+ action later.
223
225
 
224
- If the owner confirms an action is intentional or incorrect, record that
225
- decision with `--note "<reason>"`. Review the exact scope first. A note dismisses
226
- the whole action, so never use it to hide a mix of intentional and broken URLs.
227
- Dismissal keeps the observations and does not verify a fix. The dashboard can
228
- restore the action later.
226
+ A 404 or 410 is a gone URL. An action names one only when a link, a sitemap
227
+ entry, or search demand still points at it, or when the crawl cannot confirm
228
+ the removal.
229
229
 
230
230
  **7. Mark the day.**
231
231
 
@@ -264,6 +264,10 @@ and do not route around it with `pull` or another read.
264
264
  `coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
265
265
  true` marks the list as a floor.
266
266
 
267
+ **A failed Scan has no scores.** `status: failed` with `failureKind` says why.
268
+ Its null scores and vitals are not zeros. A null `blockingTime` or `wastedMs`
269
+ is unmeasured, never zero.
270
+
267
271
  For `search indexing urls`, read `totalSemantics` before quoting `total`.
268
272
  `lower-bound` means the provider used lookahead instead of counting every URL.
269
273
  The total can rise between pages without a filter change. Follow `hasMore` or
@@ -124,7 +124,7 @@ lines.
124
124
  | `actions list` | Server-ranked issues and opportunities. `--limit 1..25` (default 10), `--offset`. Each row carries `evidence.freshness`; `verdict: "aged"` means the observation is over a day old, so live-check before fixing |
125
125
  | `actions show <action-id>` | One issue or opportunity, plus its evidence. `--limit 1..100` (default 50), `--group-id`, `--cursor` |
126
126
  | `actions resolve <action-id>` | Mutation. Claims the issue or opportunity and starts server verification. Reads the action first and sends `artifactVersion` for you |
127
- | `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`. `--note` records why the owner considers the action intentional or incorrect. Without a note, complete unlinked 404 or 410 evidence is required. Dismissal preserves observations and never verifies a fix |
127
+ | `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`, and `--note`, which records why the owner considers the action intentional or incorrect. Dismissal preserves observations and never verifies a fix |
128
128
 
129
129
  ## Pages, Scans, and lab performance
130
130
 
@@ -133,11 +133,24 @@ lines.
133
133
  | `page inspect <url>` | Stored observations, Lighthouse rows and keywords for one Page. `--limit 1..200`, `--offset`, `--include-resolved`. Carries `observations.coverage`: `never-scanned` means no source recorded this Page, `scanned-clear` means recorded and all clear |
134
134
  | `page issues` | Raw Page Issue rows. Pass `--action-id`, or pass both `--source` and `--issue`. Passing both selectors exits `2`. `--url`, `--path-prefix`, `--include-resolved`, `--limit 1..200`, `--offset`. When the selector matches nothing, `availableKeys` lists the pairs this Site does have. Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
135
135
  | `page scan <url>` | Mutation. Adds or re-enables the URL as a monitored Page, then starts mobile and desktop Scans |
136
- | `performance` | Stored Lighthouse lab overview for the Site: medians for perf, a11y, SEO, LCP, TBT, CLS. Lab Scans, never real-user field data |
137
- | `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, and the error on a failed Scan. `--limit 1..100` (default 25) |
138
- | `scans show <scan-id>` | One Scan, and the evidence behind its scores: wasteful assets, repeat-visit cache policy, third-party scripts, the LCP element, and every failing accessibility and best-practices check with its CSS selectors and DOM snippets. Read `coverage` before quoting a total |
136
+ | `performance` | Stored Lighthouse lab overview for the Site: mobile medians for perf, a11y, SEO, LCP, TBT, CLS, then the homepage and each monitored Page on mobile and desktop. Lab Scans, never real-user field data |
137
+ | `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, `trigger`, and `lighthouseVersion`. A failed Scan carries `failureKind` and `error`, and its scores are null. `--limit 1..100` (default 25) |
138
+ | `scans show <scan-id>` | One Scan, and the evidence behind its scores: wasteful assets, repeat-visit cache policy, third-party scripts, the LCP element, and every failing accessibility and best-practices check with its CSS selectors and DOM snippets. Also `trigger`, `lighthouseVersion`, and `comparison` with the previous Scan of the same Page. Read `coverage` before quoting a total |
139
139
  | `scans pages` | The Pages this Site scans on a schedule. Add one with `page scan <url>` |
140
140
 
141
+ `scans list` and `scans show` return the same Scan shape as the `lighthouse_audits` MCP tool.
142
+ `failureKind` says why a Scan failed, for example `timeout`, `rejected`, or `quota`.
143
+ It is null on a Scan that did not fail, and on a Scan that failed before kinds were recorded.
144
+
145
+ `comparison` has one of three tags.
146
+ `compared` lists each metric both Scans measured, with `previous`, `current`, `delta`, and `severity`.
147
+ `incompatible` names why the two Scans differ, for example `lighthouse-version`. Do not compare their numbers.
148
+ `none` means no previous Scan was compared. The homepage slot always reads `none`.
149
+ A lab `regression` compares one Lighthouse result per Scan. Lab scores vary by a few points between Scans.
150
+
151
+ A null `blockingTime` or `wastedMs` is unmeasured. It is never zero.
152
+ If a Scan could not measure blocking time for a third party, text output prints "blocking not measured".
153
+
141
154
  `scans show` can include `observedTrace` from the retained Lighthouse projection.
142
155
  If its tag is `available`, its phases describe `observedLcpMs` from the browser trace.
143
156
  `throttlingMethod` names how Lighthouse computed the reported metrics.
@@ -185,7 +198,7 @@ The monitored Page then follows its own recurring schedule.
185
198
  | --- | --- |
186
199
  | `vitals summary` | Field Core Web Vitals from CrUX: the latest p75, plus its change over the window. `--url` for one Page, `--form-factor phone\|desktop\|all`. `available: false` means the target is under the CrUX traffic floor |
187
200
  | `vitals trend` | A 25-week field p75 series. Same inputs as `vitals summary` |
188
- | `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector and a ready-to-apply `fixPrompt`. Needs a connected Web Analytics provider |
201
+ | `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector, `poorFraction`, `labHint`, and a ready-to-apply `fixPrompt`. An LCP finding adds `resource`, `fetchPriority`, `initiatorType`, and `phases`. An INP finding adds `interaction`. `window` gives the dates the provider aggregated. Needs a connected Web Analytics provider |
189
202
 
190
203
  ## Search Console and sitemaps
191
204