@nuxtseo/cli 0.1.2 → 0.1.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
@@ -123,12 +123,14 @@ to stdout and writes the readable recovery message to stderr.
123
123
 
124
124
  Local outcomes have no server protocol body. With `--json`, commands such as
125
125
  bare `nuxtseo`, `config`, `logout`, and `sites use` emit a tagged CLI-owned JSON
126
- value with `schemaVersion: 1`. Local failures emit `CliError` with a stable code
127
- and exit code. These tags are separate from protocol envelopes.
126
+ value with `schemaVersion: 1`. `CliHelp` uses `schemaVersion: 2`. Local failures
127
+ emit `CliError` with a stable code and exit code. These tags are separate from
128
+ protocol envelopes.
128
129
 
129
- `--help --json` returns the selected command, global options, arguments, and
130
- subcommands as `CliHelp`. Option names are strict; an unknown option exits `2`
131
- before authentication or network work.
130
+ `--help --json` returns the selected command, inherited `globalOptions`,
131
+ positional `arguments`, command-specific `localOptions`, and `subcommands` as
132
+ `CliHelp`. Option names are strict. An unknown option exits `2` before
133
+ authentication or network work.
132
134
 
133
135
  ## Automation controls
134
136
 
@@ -172,6 +174,7 @@ nuxtseo page scan <url>
172
174
  nuxtseo performance
173
175
  nuxtseo search status
174
176
  nuxtseo search analytics <pages|keywords>
177
+ nuxtseo search indexing <summary|urls>
175
178
  nuxtseo research overview
176
179
  nuxtseo research keywords <topic>
177
180
  nuxtseo research serp <keyword>
@@ -189,6 +192,7 @@ performance, research, content, and account setup. The menu prints the direct
189
192
  command it runs.
190
193
 
191
194
  `search status` reads stored connection state. It never waits for Google.
195
+ `search indexing summary` reads retained URL Inspection coverage.
192
196
  `research keywords`, `research serp`, and `research rankings` can use the Team
193
197
  research allowance. Keyword responses report cache use in `evidence`. SERP and
194
198
  ranking responses report `cached: true`. Cached responses use no unit.
@@ -204,6 +208,7 @@ responses.
204
208
  | `backlinks recoverable` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` |
205
209
  | `mentions list` | `--limit 1..200` | `--limit 100` |
206
210
  | `search analytics` | `--limit 1..100`, `--page >=1` | `--limit 25 --page 1` |
211
+ | `search indexing` | `--limit 1..500`, `--offset >=0` | `--limit 50 --offset 0` |
207
212
  | `content briefs list` | `--limit 1..100`, `--offset >=0` | `--limit 25 --offset 0` |
208
213
 
209
214
  Keep the server order for actions. For another page, pass the next offset or
package/dist/cli.js CHANGED
@@ -252,6 +252,7 @@ async function interactiveBare(runtime, globals) {
252
252
  options: [
253
253
  { value: 'performance', label: 'Performance overview' },
254
254
  { value: 'connection', label: 'Search Console connection' },
255
+ { value: 'indexing', label: 'Search indexing coverage' },
255
256
  { value: 'pages', label: 'Search Console Pages' },
256
257
  { value: 'keywords', label: 'Search Console queries' },
257
258
  ],
@@ -265,7 +266,9 @@ async function interactiveBare(runtime, globals) {
265
266
  ? ['performance']
266
267
  : task === 'connection'
267
268
  ? ['search', 'status']
268
- : ['search', 'analytics', task];
269
+ : task === 'indexing'
270
+ ? ['search', 'indexing', 'summary']
271
+ : ['search', 'analytics', task];
269
272
  return runInteractiveCommand(command, runtime, globals);
270
273
  }
271
274
  if (group === 'research') {
@@ -16,12 +16,13 @@ export interface CliHelpArgument {
16
16
  }
17
17
  export interface CliHelpResponse {
18
18
  _tag: 'CliHelp';
19
- schemaVersion: 1;
19
+ schemaVersion: 2;
20
20
  cliVersion: string;
21
21
  command: string;
22
22
  description?: string;
23
23
  globalOptions: CliHelpArgument[];
24
24
  arguments: CliHelpArgument[];
25
+ localOptions: CliHelpArgument[];
25
26
  subcommands: Array<{
26
27
  name: string;
27
28
  description?: string;
@@ -54,14 +54,16 @@ function argumentsFor(command) {
54
54
  export function describeCommand(root, rawArgs, cliVersion) {
55
55
  const resolved = resolveCommand(root, rawArgs.filter(argument => argument !== '--help' && argument !== '-h'));
56
56
  const meta = commandMeta(resolved.command);
57
+ const localArguments = resolved.command === root ? [] : argumentsFor(resolved.command);
57
58
  return {
58
59
  _tag: 'CliHelp',
59
- schemaVersion: 1,
60
+ schemaVersion: 2,
60
61
  cliVersion,
61
62
  command: resolved.path.join(' '),
62
63
  ...(meta.description === undefined ? {} : { description: meta.description }),
63
64
  globalOptions: argumentsFor(root).filter(argument => argument.type !== 'positional'),
64
- arguments: resolved.command === root ? [] : argumentsFor(resolved.command),
65
+ arguments: localArguments.filter(argument => argument.type === 'positional'),
66
+ localOptions: localArguments.filter(argument => argument.type !== 'positional'),
65
67
  subcommands: Object.entries(commandChildren(resolved.command)).flatMap(([name, value]) => {
66
68
  const child = childCommand(value);
67
69
  if (!child)
package/dist/commands.js CHANGED
@@ -5,7 +5,7 @@ import { openBrowser } from './browser.js';
5
5
  import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
6
6
  import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
7
7
  import { parseAbsolutePageUrl, parseChoice, parseInteger } from './parse.js';
8
- import { renderAction, renderActionResolution, renderActions, renderContentBrief, renderContentBriefCreated, renderContentBriefs, renderContentDecay, renderDuplicateClusters, renderKeywordResearch, renderLinkOpportunities, renderMentions, renderPageInspection, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderResearchOverview, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSites, renderUsage, } from './render.js';
8
+ import { renderAction, renderActionResolution, renderActions, renderContentBrief, renderContentBriefCreated, renderContentBriefs, renderContentDecay, renderDuplicateClusters, renderIndexingDiagnostics, renderKeywordResearch, renderLinkOpportunities, renderMentions, renderPageInspection, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderResearchOverview, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSites, renderTimeline, renderUsage, } from './render.js';
9
9
  import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
10
10
  import { resolveSite } from './site.js';
11
11
  import { clearCredential, getCredentialStatus, readConfig, resolveApiUrl, saveCredential, updateConfig, } from './state/index.js';
@@ -447,6 +447,36 @@ async function searchAnalytics(runtime, globals, args) {
447
447
  }, { signal: runtime.requestSignal }));
448
448
  return present(runtime, globals, response, value => renderSearchAnalytics(value.data));
449
449
  }
450
+ async function searchIndexing(runtime, globals, args) {
451
+ const view = parseChoice(args.view, { name: 'view', choices: ['summary', 'urls'] });
452
+ if (view._tag === 'Err')
453
+ return view;
454
+ const status = args.status === undefined
455
+ ? ok(undefined)
456
+ : parseChoice(args.status, { name: '--status', choices: ['indexed', 'not_indexed', 'pending'] });
457
+ if (status._tag === 'Err')
458
+ return status;
459
+ const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 500, defaultValue: 50 });
460
+ if (limit._tag === 'Err')
461
+ return limit;
462
+ const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
463
+ if (offset._tag === 'Err')
464
+ return offset;
465
+ const resolved = await apiAndSite(runtime, globals);
466
+ if (resolved._tag === 'Err')
467
+ return resolved;
468
+ const response = await withSpinner(runtime, 'Loading Search Console indexing', () => resolved.value.api.client.search.readIndexingDiagnostics({
469
+ params: { siteId: resolved.value.siteId },
470
+ query: {
471
+ view: view.value,
472
+ issue: args.issue,
473
+ status: status.value,
474
+ limit: limit.value,
475
+ offset: offset.value,
476
+ },
477
+ }, { signal: runtime.requestSignal }));
478
+ return present(runtime, globals, response, value => renderIndexingDiagnostics(value.data));
479
+ }
450
480
  async function researchOverview(runtime, globals) {
451
481
  const resolved = await apiAndSite(runtime, globals);
452
482
  if (resolved._tag === 'Err')
@@ -576,6 +606,27 @@ async function auditLinkOpportunities(runtime, globals) {
576
606
  }, { signal: runtime.requestSignal }));
577
607
  return present(runtime, globals, response, value => renderLinkOpportunities(value.data));
578
608
  }
609
+ async function timelineEntries(runtime, globals, args) {
610
+ const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
611
+ if (limit._tag === 'Err')
612
+ return limit;
613
+ const resolved = await apiAndSite(runtime, globals);
614
+ if (resolved._tag === 'Err')
615
+ return resolved;
616
+ const response = await withSpinner(runtime, 'Loading Timeline', () => resolved.value.api.client.timeline.entries({
617
+ params: { siteId: resolved.value.siteId },
618
+ query: {
619
+ kinds: args.kind,
620
+ feature: args.feature,
621
+ severity: args.severity,
622
+ since: args.since,
623
+ limit: limit.value,
624
+ cursor: args.cursor,
625
+ includeClosed: args.includeClosed,
626
+ },
627
+ }, { signal: runtime.requestSignal }));
628
+ return present(runtime, globals, response, value => renderTimeline(value.data));
629
+ }
579
630
  async function contentBriefList(runtime, globals, args) {
580
631
  const status = args.status === undefined
581
632
  ? ok(undefined)
@@ -928,6 +979,49 @@ export function createRootCommand(runtime, globals, execution) {
928
979
  sortDir: args['sort-dir'],
929
980
  }), args._, 1)(),
930
981
  }),
982
+ indexing: defineCommand({
983
+ meta: { name: 'indexing', description: 'Read retained Search Console indexing coverage' },
984
+ args: {
985
+ view: positional('summary-or-urls', 'summary or urls'),
986
+ issue: { type: 'string', description: 'Filter URLs by issue type' },
987
+ status: { type: 'enum', options: ['indexed', 'not_indexed', 'pending'], description: 'Filter URLs by indexing status' },
988
+ limit: { type: 'string', description: 'Maximum URLs, 1 to 500' },
989
+ offset: { type: 'string', description: 'Pagination offset' },
990
+ },
991
+ run: ({ args }) => capture(execution, () => searchIndexing(runtime, globals, {
992
+ view: args.view,
993
+ issue: args.issue,
994
+ status: args.status,
995
+ limit: args.limit,
996
+ offset: args.offset,
997
+ }), args._, 1)(),
998
+ }),
999
+ },
1000
+ });
1001
+ const timeline = defineCommand({
1002
+ meta: { name: 'timeline', description: 'Read the Timeline — what changed on the Site' },
1003
+ subCommands: {
1004
+ list: defineCommand({
1005
+ meta: { name: 'list', description: 'List Timeline Entries' },
1006
+ args: {
1007
+ 'kind': { type: 'string', description: 'Comma-separated Timeline Entry kinds, e.g. traffic.dropped,indexing.changed' },
1008
+ 'feature': { type: 'string', description: 'Filter to the kinds registered under this feature' },
1009
+ 'severity': { type: 'string', description: 'Comma-separated severities: info, notable, high' },
1010
+ 'since': { type: 'string', description: 'Only entries at or after this ISO timestamp' },
1011
+ 'limit': { type: 'string', description: 'Maximum Timeline Entries, 1 to 100' },
1012
+ 'cursor': { type: 'string', description: 'Opaque pagination cursor from a previous page' },
1013
+ 'include-closed': { type: 'boolean', description: 'Include closed episodes' },
1014
+ },
1015
+ run: ({ args }) => capture(execution, () => timelineEntries(runtime, globals, {
1016
+ kind: args.kind,
1017
+ feature: args.feature,
1018
+ severity: args.severity,
1019
+ since: args.since,
1020
+ limit: args.limit,
1021
+ cursor: args.cursor,
1022
+ includeClosed: args['include-closed'],
1023
+ }), args._, 0)(),
1024
+ }),
931
1025
  },
932
1026
  });
933
1027
  return defineCommand({
@@ -994,6 +1088,7 @@ export function createRootCommand(runtime, globals, execution) {
994
1088
  }),
995
1089
  research,
996
1090
  search,
1091
+ timeline,
997
1092
  },
998
1093
  });
999
1094
  }
package/dist/render.d.ts CHANGED
@@ -3,10 +3,11 @@ import type { ActionList, ActionResolve, ActionShow } from '@nuxtseo/protocol/v1
3
3
  import type { AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities } from '@nuxtseo/protocol/v1/audit';
4
4
  import type { Mentions, RecoverableBacklinks } from '@nuxtseo/protocol/v1/backlinks';
5
5
  import type { ContentBrief, ContentBriefCreateData, ContentBriefListData } from '@nuxtseo/protocol/v1/content';
6
- import type { SearchAnalyticsData, SearchStatusData } from '@nuxtseo/protocol/v1/gsc';
6
+ import type { IndexingDiagnosticsData, SearchAnalyticsData, SearchStatusData } from '@nuxtseo/protocol/v1/gsc';
7
7
  import type { PageInspect, PageScan } from '@nuxtseo/protocol/v1/pages';
8
8
  import type { SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
9
9
  import type { KeywordResearchData, RankingsResearchData, SerpResearchData, StoredResearchOverview } from '@nuxtseo/protocol/v1/research';
10
+ import type { TimelineEntries } from '@nuxtseo/protocol/v1/timeline';
10
11
  export declare function renderSites(response: SitesListResponse, selectedSiteId?: string): string;
11
12
  export declare function renderUsage(response: AccountUsageResponse): string;
12
13
  export declare function renderActions(data: ActionList): string;
@@ -17,6 +18,7 @@ export declare function renderPageScan(data: PageScan): string;
17
18
  export declare function renderPerformance(data: SitePerformanceOverview): string;
18
19
  export declare function renderSearchStatus(data: SearchStatusData): string;
19
20
  export declare function renderSearchAnalytics(data: SearchAnalyticsData): string;
21
+ export declare function renderIndexingDiagnostics(data: IndexingDiagnosticsData): string;
20
22
  export declare function renderResearchOverview(data: StoredResearchOverview): string;
21
23
  export declare function renderKeywordResearch(data: KeywordResearchData): string;
22
24
  export declare function renderSerpResearch(data: SerpResearchData): string;
@@ -27,5 +29,6 @@ export declare function renderContentBriefCreated(data: ContentBriefCreateData):
27
29
  export declare function renderContentDecay(data: AuditContentDecay): string;
28
30
  export declare function renderDuplicateClusters(data: AuditDuplicateClusters): string;
29
31
  export declare function renderLinkOpportunities(data: AuditLinkOpportunities): string;
32
+ export declare function renderTimeline(data: TimelineEntries): string;
30
33
  export declare function renderRecoverableBacklinks(data: RecoverableBacklinks): string;
31
34
  export declare function renderMentions(data: Mentions): string;
package/dist/render.js CHANGED
@@ -151,6 +151,34 @@ export function renderSearchAnalytics(data) {
151
151
  ['Rows', 'rows' in data ? data.rows.length : data.type === 'timeseries' ? data.daily.length : 0],
152
152
  ]);
153
153
  }
154
+ export function renderIndexingDiagnostics(data) {
155
+ if (data.view === 'summary') {
156
+ const indexedPercent = data.totalUrls > 0
157
+ ? `${(data.indexed / data.totalUrls * 100).toFixed(1)}%`
158
+ : '0.0%';
159
+ const issues = data.issues
160
+ .filter(issue => issue.count > 0)
161
+ .sort((left, right) => right.count - left.count)
162
+ .map(issue => `${issue.count} ${issue.label} ${issue.severity}`);
163
+ return [
164
+ fields([
165
+ ['Known URLs', data.totalUrls],
166
+ ['Indexed', data.indexed],
167
+ ['Not indexed', Math.max(0, data.totalUrls - data.indexed)],
168
+ ['Indexed rate', indexedPercent],
169
+ ['As of', data.asOf],
170
+ ]),
171
+ ...(issues.length > 0 ? ['Issues', ...issues] : []),
172
+ ].join('\n');
173
+ }
174
+ if (data.urls.length === 0)
175
+ return 'No Search Console indexing URLs match these filters.';
176
+ return [
177
+ `Search Console indexing URLs (${data.urls.length} of ${data.total})`,
178
+ ...data.urls.map(row => `${row.verdict ?? 'unknown'} ${row.coverageState ?? 'unknown'}\n ${row.url}`),
179
+ data.hasMore ? `More URLs available after offset ${data.offset + data.limit}.` : '',
180
+ ].filter(Boolean).join('\n');
181
+ }
154
182
  export function renderResearchOverview(data) {
155
183
  const subject = data.subject
156
184
  ? fields([
@@ -242,6 +270,18 @@ export function renderLinkOpportunities(data) {
242
270
  ...data.rows.map(row => `${row.phrase}\n From: ${row.sourceUrl}\n To: ${row.targetUrl}`),
243
271
  ].join('\n');
244
272
  }
273
+ export function renderTimeline(data) {
274
+ if (data.entries.length === 0)
275
+ return 'No Timeline Entries match these filters.';
276
+ return [
277
+ `Timeline (${data.entries.length})`,
278
+ ...data.entries.map((entry) => {
279
+ const label = typeof entry.rendered?.label === 'string' ? entry.rendered.label : entry.kind;
280
+ return `${entry.occurredAt} ${entry.severity ?? 'unrated'} ${entry.kind}\n ${label}`;
281
+ }),
282
+ data.nextCursor ? `More Timeline Entries available — pass --cursor ${JSON.stringify(data.nextCursor)}.` : '',
283
+ ].filter(Boolean).join('\n');
284
+ }
245
285
  export function renderRecoverableBacklinks(data) {
246
286
  if (data.items.length === 0)
247
287
  return 'No recoverable Backlinks. Every stored inbound link resolves.';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "Command line interface for the NuxtSEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -34,7 +34,7 @@
34
34
  "@clack/prompts": "^1.7.0",
35
35
  "citty": "^0.2.2",
36
36
  "pathe": "^2.0.3",
37
- "@nuxtseo/sdk": "^0.1.2"
37
+ "@nuxtseo/sdk": "^0.1.4"
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "@napi-rs/keyring": "^1.3.0"
@@ -44,7 +44,7 @@
44
44
  "@types/node": "^26.2.0",
45
45
  "publint": "^0.3.24",
46
46
  "typescript": "npm:typescript-native-bridge@6.0.3-bridge.10.tsgo.7.0.2",
47
- "@nuxtseo/protocol": "0.1.2"
47
+ "@nuxtseo/protocol": "0.1.4"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
@@ -26,6 +26,7 @@ report `cached`.
26
26
 
27
27
  `search status` reads stored connection state. It never waits for Google.
28
28
  `search analytics` reads Search Console rows through the public API.
29
+ `search indexing` reads retained URL Inspection coverage through the public API.
29
30
 
30
31
  ## Get the binary
31
32
 
@@ -115,6 +116,7 @@ handling failures, read [CLI protocol](references/protocol.md).
115
116
  | `performance` | Site performance overview | Medians for perf, a11y, SEO, LCP, TBT, CLS |
116
117
  | `search status` | Stored Search Console connection | Provider free |
117
118
  | `search analytics <pages\|keywords>` | Search Console Page or query rows | `--period`, `--limit`, `--page`, `--search` |
119
+ | `search indexing <summary\|urls>` | Retained URL Inspection coverage | `summary` returns indexed counts; `urls` supports `--issue`, `--status`, `--limit`, `--offset` |
118
120
  | `research overview` | Stored Site and competitor research | Metrics, history, gaps, and quick wins |
119
121
  | `research keywords <topic>` | Live keyword ideas | Volume, difficulty, intent, and cost data |
120
122
  | `research serp <keyword>` | Live SERP snapshot | Results, features, and fetch time |
@@ -130,11 +132,11 @@ handling failures, read [CLI protocol](references/protocol.md).
130
132
  `page inspect` and `page scan` take an absolute URL, for example
131
133
  `https://example.com/about`.
132
134
 
133
- `--help --json` returns a `CliHelp` value for one command level: its
134
- `arguments`, the inherited `globalOptions`, and its `subcommands` as names and
135
- descriptions only. To learn a subcommand's own flags, ask it directly, for
136
- example `nuxtseo actions list --help --json`. Use this instead of guessing a
137
- flag, and prefer the table above for anything it already answers.
135
+ `--help --json` returns a `CliHelp` value for one command level. It separates
136
+ positional `arguments`, command-specific `localOptions`, inherited
137
+ `globalOptions`, and `subcommands`. To learn a subcommand's own flags, ask it
138
+ directly, for example `nuxtseo actions list --help --json`. Use this instead of
139
+ guessing a flag, and prefer the table above for anything it already answers.
138
140
 
139
141
  ## The triage loop
140
142
 
@@ -18,7 +18,8 @@ Protocol failure, still on stdout, with recovery details on stderr:
18
18
 
19
19
  Read `data`. The CLI does not unwrap, rename, rank, or enrich fields.
20
20
 
21
- Local outcomes have no server body. They carry `schemaVersion: 1` and one tag:
21
+ Local outcomes have no server body. They carry one tag and their own schema
22
+ version. `CliHelp` uses version 2. The other values use version 1.
22
23
 
23
24
  | `_tag` | Written by |
24
25
  | --- | --- |