@nuxtseo/cli 0.5.3 → 0.5.4

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
@@ -256,7 +256,9 @@ reads retained rows and spends nothing.
256
256
  `search status` reads stored connection state. It never waits for Google.
257
257
  `search indexing summary` reads retained URL Inspection coverage. `asOf` and `oldestCheckAt` bound the age of its verdicts.
258
258
  `search index-history` dates an indexing change against known releases.
259
- `search inspect <url>` reads Google's own verdict for one URL.
259
+ `search inspect <url>` reads the latest retained Google URL Inspection for one URL.
260
+ `search inspect <url> --fresh --yes` requests Google URL Inspection through the Site connection and uses inspection quota.
261
+ Fresh inspection reports errors and skipped URLs with a nonzero exit code. If the request needs more time, add `--timeout-ms 60000`.
260
262
  `page inspect <url>` reads the Nuxt SEO observation store for the same URL.
261
263
  `research keywords`, `research serp`, and `research rankings` can use the Team
262
264
  research limit. Keyword responses report cache use in `evidence`. SERP and
package/dist/commands.js CHANGED
@@ -9,7 +9,7 @@ import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
9
9
  import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
10
10
  import { parseAbsolutePageUrl, parseChoice, parseInteger, parsePageUrlOrPath, parseRatio } from './parse.js';
11
11
  import { PULL_PERIODS, runPull } from './pull.js';
12
- import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemaps, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUsage, } from './render.js';
12
+ import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemaps, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUrlInspectionRefresh, renderUsage, } from './render.js';
13
13
  import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
14
14
  import { resolveSite } from './site.js';
15
15
  import { installSkill, SKILL_AGENTS } from './skill.js';
@@ -717,13 +717,36 @@ async function searchIndexHistory(runtime, globals, args) {
717
717
  }, { signal: runtime.requestSignal }));
718
718
  return present(runtime, globals, response, value => renderIndexingHistory(value.data));
719
719
  }
720
- async function searchInspect(runtime, globals, rawUrl) {
720
+ async function searchInspect(runtime, globals, rawUrl, fresh = false) {
721
721
  const url = parsePageUrlOrPath(rawUrl);
722
722
  if (url._tag === 'Err')
723
723
  return url;
724
+ if (fresh) {
725
+ const confirmed = await confirmMutation(runtime, globals, `Request a fresh URL Inspection for ${url.value}? This uses inspection quota.`);
726
+ if (confirmed._tag === 'Err')
727
+ return confirmed;
728
+ if (!confirmed.value)
729
+ return ok(undefined);
730
+ }
724
731
  const resolved = await apiAndSite(runtime, globals);
725
732
  if (resolved._tag === 'Err')
726
733
  return resolved;
734
+ if (fresh) {
735
+ const response = await withSpinner(runtime, 'Requesting URL Inspection', () => resolved.value.api.client.search.refreshUrlInspection({
736
+ params: { siteId: resolved.value.siteId },
737
+ body: { url: url.value },
738
+ }, { signal: runtime.requestSignal }));
739
+ if (response._tag === 'Err')
740
+ return fromSdkFailure(response.error);
741
+ if (response.value.data.errors.length || response.value.data.skipped.length || response.value.data.results.length !== 1) {
742
+ const incomplete = fail(EXIT_CODE.conflict, 'Fresh URL Inspection did not complete. Read the errors or skipped URLs in the response.');
743
+ if (globals.json)
744
+ return { _tag: 'Err', error: { ...incomplete.error, protocolResponse: response.value } };
745
+ writeOutput(runtime, renderUrlInspectionRefresh(response.value.data));
746
+ return incomplete;
747
+ }
748
+ return present(runtime, globals, response, value => renderUrlInspectionRefresh(value.data));
749
+ }
727
750
  const response = await withSpinner(runtime, 'Loading URL Inspection', () => resolved.value.api.client.search.readUrlInspection({
728
751
  params: { siteId: resolved.value.siteId },
729
752
  query: { url: url.value },
@@ -2016,8 +2039,11 @@ export function createRootCommand(runtime, globals, execution) {
2016
2039
  }),
2017
2040
  'inspect': defineCommand({
2018
2041
  meta: { name: 'inspect', description: 'Read the latest URL Inspection result for one URL' },
2019
- args: { url: positional('url', 'Page URL or path') },
2020
- run: ({ args }) => capture(execution, () => searchInspect(runtime, globals, args.url), args._, 1)(),
2042
+ args: {
2043
+ url: positional('url', 'Page URL or path'),
2044
+ fresh: { type: 'boolean', description: 'Request Google URL Inspection using inspection quota', default: false },
2045
+ },
2046
+ run: ({ args }) => capture(execution, () => searchInspect(runtime, globals, args.url, args.fresh), args._, 1)(),
2021
2047
  }),
2022
2048
  },
2023
2049
  });
package/dist/pull.js CHANGED
@@ -319,22 +319,25 @@ export async function runPull(runtime, globals, args, resolve, catalogue = PULL_
319
319
  });
320
320
  const outcome = pullLine(candidate.name, result);
321
321
  writeProtocolResponse(runtime, outcome.line);
322
- if (!explainedAnalyticsConnection
323
- && candidate.name.startsWith('analytics ')
322
+ const analyticsUnconnected = candidate.name.startsWith('analytics ')
324
323
  && result._tag === 'Err'
325
324
  && result.error._tag === 'ApiFailure'
326
325
  && result.error.code === 'conflict'
327
- && result.error.details?.action === 'connect_web_analytics') {
326
+ && result.error.details?.action === 'connect_web_analytics';
327
+ if (!explainedAnalyticsConnection && analyticsUnconnected) {
328
328
  explainedAnalyticsConnection = true;
329
329
  writeDiagnostic(runtime, 'Web Analytics is not connected. Connect a property to read these views. For a full SEO pull, use --exclude analytics.');
330
330
  }
331
- exitCodes.push(outcome.exitCode);
331
+ // Default pulls include optional integrations. An explicit include asks
332
+ // for those reads, so their missing connection still fails that request.
333
+ const optionalUnavailable = analyticsUnconnected && !args.include;
334
+ exitCodes.push(optionalUnavailable ? EXIT_CODE.success : outcome.exitCode);
332
335
  completed++;
333
336
  // An expired or rejected credential fails every later read the same way.
334
337
  // Stopping there saves the requests and keeps the reason on one line.
335
338
  if (outcome.exitCode === EXIT_CODE.authentication || outcome.exitCode === EXIT_CODE.interrupted)
336
339
  stopped = true;
337
- writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
340
+ writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${optionalUnavailable ? 'unavailable' : outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
338
341
  if (outcome.failure?.code === 'request_timeout' && timeoutMs < 300_000) {
339
342
  const retryTimeout = Math.min(timeoutMs * 2, 300_000);
340
343
  // Retry only this read. Repeating a full paid pull can spend twice.
package/dist/render.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxt
4
4
  import type { AnalyticsQueryData, AnalyticsView } from '@nuxtseo/protocol/v1/analytics';
5
5
  import type { AuditChanges, AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities, AuditLinkStructure } from '@nuxtseo/protocol/v1/audit';
6
6
  import type { BacklinkAnchorsData, BacklinksHistory, BacklinksSummaryData, MentionsData, RecoverableBacklinksData, ReferringDomainsData } from '@nuxtseo/protocol/v1/backlinks';
7
- import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData } from '@nuxtseo/protocol/v1/gsc';
7
+ import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData, UrlInspectionRefreshData } from '@nuxtseo/protocol/v1/gsc';
8
8
  import type { PageInspectData, PageIssues, PageScan } from '@nuxtseo/protocol/v1/pages';
9
9
  import type { FieldVitalFindings, FieldVitals, MonitoredPages, PerformanceScanDetail, PerformanceScans, SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
10
10
  import type { DomainAvailability, DomainTraffic, KeywordResearchData, RankingsResearchData, SerpResearchData, StoredResearchOverview } from '@nuxtseo/protocol/v1/research';
@@ -21,6 +21,7 @@ export declare function renderPerformance(data: SitePerformanceOverview): string
21
21
  export declare function renderSearchStatus(data: SearchStatusData): string;
22
22
  export declare function renderSearchAnalytics(data: SearchAnalyticsData): string;
23
23
  export declare function renderIndexingHistory(data: IndexingHistoryData): string;
24
+ export declare function renderUrlInspectionRefresh(data: UrlInspectionRefreshData): string;
24
25
  export declare function renderUrlInspection(data: UrlInspectionData): string;
25
26
  export declare function renderAuditChanges(data: AuditChanges): string;
26
27
  export declare function renderLinkStructure(data: AuditLinkStructure): string;
package/dist/render.js CHANGED
@@ -232,6 +232,25 @@ export function renderIndexingHistory(data) {
232
232
  }),
233
233
  ].join('\n');
234
234
  }
235
+ export function renderUrlInspectionRefresh(data) {
236
+ return [
237
+ `URL Inspection request completed at ${data.checkedAt}.`,
238
+ ...data.results.map(result => fields([
239
+ ['URL', result.url],
240
+ ['Verdict', result.verdict],
241
+ ['Coverage', result.coverageState],
242
+ ['Indexing', result.indexingState],
243
+ ['robots.txt', result.robotsTxtState],
244
+ ['Page fetch', result.pageFetchState],
245
+ ['User canonical', result.userCanonical],
246
+ ['Google canonical', result.googleCanonical],
247
+ ['Last crawl', result.lastCrawlTime],
248
+ ])),
249
+ ...data.errors.map(result => `Inspection failed for ${result.url}: ${result.error}`),
250
+ ...data.skipped.map(result => `Inspection skipped for ${result.url}: ${result.reason}`),
251
+ `Inspection quota remaining: ${data.rateLimit.remaining} of ${data.rateLimit.limit}.`,
252
+ ].join('\n');
253
+ }
235
254
  export function renderUrlInspection(data) {
236
255
  if (data._tag === 'no-inspection-data')
237
256
  return `No retained URL Inspection for ${data.requestedUrl}.`;
@@ -604,7 +623,7 @@ export function renderFieldVitals(data) {
604
623
  ['As of', summary.asOf],
605
624
  ...['lcp', 'inp', 'cls'].map(metric => [
606
625
  metric.toUpperCase(),
607
- `${vitalValue(metric, summary[metric].p75)} ${summary[metric].status ?? 'unrated'} delta ${summary[metric].deltaOverPeriod ?? 'none'}`,
626
+ `${vitalValue(metric, summary[metric].p75)} ${summary[metric].status ?? 'unrated'} delta ${summary[metric].deltaOverPeriod ?? 'none'} As of ${summary[metric].asOf ?? 'unknown'}`,
608
627
  ]),
609
628
  ]),
610
629
  ].join('\n');
@@ -896,7 +915,7 @@ export function renderScanDetail(data) {
896
915
  if (data.observedTrace?._tag === 'available') {
897
916
  const trace = data.observedTrace;
898
917
  blocks.push([
899
- `Observed trace LCP ${trace.observedLcpMs}ms throttling ${trace.throttlingMethod}`,
918
+ `Observed trace LCP ${trace.observedLcpMs}ms throttling ${trace.throttlingMethod ?? 'not reported'}`,
900
919
  ...trace.phases.map(phase => ` ${phase.id} ${phase.durationMs}ms`),
901
920
  'Trace phases describe observed LCP. Reported LCP can be simulated.',
902
921
  ].join('\n'));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.5.3",
4
+ "version": "0.5.4",
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.3",
37
- "@nuxtseo/sdk": "^0.5.3",
36
+ "@nuxtseo/protocol": "^0.5.4",
37
+ "@nuxtseo/sdk": "^0.5.4",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -277,7 +277,10 @@ Do not compare this population with a summary or cohort from another capture.
277
277
  - `status` is the verdict, `performance` the lab score, `vitals` what real
278
278
  users measured. Never substitute one for another.
279
279
  - `page inspect` reads the Nuxt SEO observation store. `search inspect` reads
280
- Google's index verdict for the same URL.
280
+ the latest retained Google URL Inspection for the same URL.
281
+ `search inspect <url> --fresh --yes` requests Google URL Inspection through
282
+ the Site connection and uses inspection quota. Errors or skipped URLs mean
283
+ the fresh inspection did not complete. Never present them as fresh verdicts.
281
284
  - `search status` reads the stored connection and never waits for Google.
282
285
  `search analytics`, `search indexing`, `search index-history`, and
283
286
  `search inspect` read retained Search Console evidence.
@@ -141,6 +141,7 @@ lines.
141
141
  `scans show` can include `observedTrace` from the retained Lighthouse projection.
142
142
  If its tag is `available`, its phases describe `observedLcpMs` from the browser trace.
143
143
  `throttlingMethod` names how Lighthouse computed the reported metrics.
144
+ If it is `null`, the provider omitted the method. Do not infer a method from the timings.
144
145
  With `simulate`, reported `lcp` is modeled. Observed trace phases do not partition that modeled value.
145
146
  With `devtools` or `provided`, Lighthouse uses observed metrics.
146
147
  Neither value is real-user field data.
@@ -194,7 +195,7 @@ The monitored Page then follows its own recurring schedule.
194
195
  | `search analytics <view>` | Search Console rows, trend, detail, or analysis. Views: `pages`, `keywords`, `countries`, `devices`, `timeseries`, `page-detail`, `keyword-detail`, `analysis`. `--period 7d\|28d\|3m\|6m\|12m` (default `28d`). No other period value is accepted; `90d` exits `2`, and `3m` is the three-month window. `--limit 1..100` (default 25), `--page` (default 1), `--search`, `--sort clicks\|impressions\|ctr\|position` (default `clicks`), `--sort-dir asc\|desc` (default `desc`), `--filter top-level\|new\|lost\|improving\|declining`, `--min-clicks`, `--max-clicks`, `--min-impressions`, `--max-impressions`, `--min-position 1..100`, `--max-position 1..100`, `--max-ctr 0..1`. The `page-detail` view needs `--page-url`, `keyword-detail` needs `--keyword`, and `analysis` needs `--preset`. The brand presets also need `--brand-terms` |
195
196
  | `search indexing <summary\|urls>` | Retained URL Inspection coverage. `summary` returns indexed counts. `urls` supports `--issue`, `--status`, `--limit 1..500` (default 50), `--offset` |
196
197
  | `search index-history` | Retained indexing changes and candidate causes. `--url`, `--field`, `--days 1..730`. Dates a regression against a release |
197
- | `search inspect <url>` | Google's own verdict for one URL. Takes an absolute URL or a site-relative path |
198
+ | `search inspect <url>` | Latest retained Google URL Inspection for one URL. Takes an absolute URL or a site-relative path. `--fresh --yes` requests Google URL Inspection through the Site connection and uses inspection quota. Errors or skipped URLs produce a nonzero exit code |
198
199
  | `search cohorts` | Not-indexed rate per route family. Returns `analysed: false` when the not-indexed list is a sample (`reason: sampled-index-state`). `--limit 1..50` (default 12), `--min-pages 1..500` (default 5). Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
199
200
  | `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
200
201
  | `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
@@ -63,5 +63,6 @@ Keep these reads separate:
63
63
  | What is the indexed count for retained URLs? | `search indexing summary` | `totalUrls`, `indexed`, `oldestCheckAt`, `asOf` |
64
64
  | Which retained URLs are indexed or not indexed? | `search indexing urls` | `total`, then the complete `urls` stream |
65
65
  | Which URLs appeared in the current period? | `search analytics pages` | Rows with `impressions > 0`, then each `url` |
66
- | What does Google say about one URL now? | `search inspect <url>` | The returned URL Inspection verdict |
66
+ | What did Google say at the last inspection? | `search inspect <url>` | The retained verdict and inspection date |
67
+ | What does Google say about one URL now? | `search inspect <url>` | Add `--fresh --yes`. Report fresh results, quota, and explicit errors or skipped URLs |
67
68
  | Which URLs did the Site submit? | `sitemaps list`, then `sitemaps urls` | Sitemap totals and membership |