@nuxtseo/cli 0.5.5 → 0.5.8
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/commands.js +113 -3
- package/dist/failures.d.ts +1 -1
- package/dist/render.d.ts +5 -2
- package/dist/render.js +28 -1
- package/dist/skill.js +11 -1
- package/package.json +3 -3
- package/skills/nuxtseo-cli/SKILL.md +6 -4
- package/skills/nuxtseo-cli/references/commands.md +6 -3
- package/skills/nuxtseo-cli/references/indexing.md +16 -1
- package/skills/nuxtseo-cli/references/protocol.md +11 -0
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, renderUrlInspectionRefresh, 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, renderIndexingWatches, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemapEvidence, renderSitemaps, renderSitemapSubmission, 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';
|
|
@@ -55,6 +55,7 @@ function present(runtime, globals, result, render) {
|
|
|
55
55
|
export const ALL_PAGES_REQUEST_CAP = 50;
|
|
56
56
|
async function collect(runtime, globals, options) {
|
|
57
57
|
let position = options.start;
|
|
58
|
+
const seenKeys = new Set();
|
|
58
59
|
for (let request = 1;; request++) {
|
|
59
60
|
const response = await withSpinner(runtime, options.message, () => options.fetch(position));
|
|
60
61
|
if (response._tag === 'Err')
|
|
@@ -62,6 +63,12 @@ async function collect(runtime, globals, options) {
|
|
|
62
63
|
emit(runtime, globals, response.value, options.render);
|
|
63
64
|
if (!options.all)
|
|
64
65
|
return ok(undefined);
|
|
66
|
+
for (const key of options.rowKeys?.(response.value) ?? []) {
|
|
67
|
+
if (seenKeys.has(key)) {
|
|
68
|
+
return fail(EXIT_CODE.infrastructure, 'paging_unstable: Search Console pages repeated row keys. Export coverage is incomplete. Preserve these responses for investigation.', undefined, 'paging_unstable');
|
|
69
|
+
}
|
|
70
|
+
seenKeys.add(key);
|
|
71
|
+
}
|
|
65
72
|
const next = options.next(response.value, position);
|
|
66
73
|
if (next._tag === 'Done')
|
|
67
74
|
return ok(undefined);
|
|
@@ -647,6 +654,24 @@ async function searchAnalytics(runtime, globals, args) {
|
|
|
647
654
|
},
|
|
648
655
|
}, { signal: runtime.requestSignal }),
|
|
649
656
|
render: value => renderSearchAnalytics(value.data),
|
|
657
|
+
rowKeys: (value) => {
|
|
658
|
+
const data = value.data;
|
|
659
|
+
if (!('rows' in data) || view.value === 'analysis')
|
|
660
|
+
return [];
|
|
661
|
+
return data.rows.flatMap((row) => {
|
|
662
|
+
// Canonical URLs can repeat legitimately. Only Google's exact page
|
|
663
|
+
// dimension is unique; older responses can omit it.
|
|
664
|
+
if (view.value === 'pages')
|
|
665
|
+
return 'gscPage' in row && typeof row.gscPage === 'string' ? [row.gscPage] : [];
|
|
666
|
+
if ('keyword' in row && typeof row.keyword === 'string')
|
|
667
|
+
return [row.keyword];
|
|
668
|
+
if ('country' in row)
|
|
669
|
+
return [row.country];
|
|
670
|
+
if ('device' in row)
|
|
671
|
+
return [row.device];
|
|
672
|
+
return [];
|
|
673
|
+
});
|
|
674
|
+
},
|
|
650
675
|
next: (value, position) => {
|
|
651
676
|
const data = value.data;
|
|
652
677
|
if (!('total' in data) || !('rows' in data))
|
|
@@ -789,7 +814,16 @@ async function competitorWrite(runtime, globals, action) {
|
|
|
789
814
|
params: { siteId: resolved.value.siteId },
|
|
790
815
|
body: { domain: action.domain },
|
|
791
816
|
}, { signal: runtime.requestSignal }));
|
|
792
|
-
|
|
817
|
+
// The add only queues the analysis. Name the read that shows it, so the
|
|
818
|
+
// caller is not left with "queued" and no way to see the result.
|
|
819
|
+
const overview = `\`nuxtseo research overview --site ${resolved.value.siteId}\``;
|
|
820
|
+
const result = present(runtime, globals, response, value => [
|
|
821
|
+
`${value.data.competitor.id}\t${value.data.competitor.domain}\tAnalysis queued`,
|
|
822
|
+
`Run ${overview} in a few minutes to read the analysis.`,
|
|
823
|
+
].join('\n'));
|
|
824
|
+
if (result._tag === 'Ok' && globals.json)
|
|
825
|
+
writeDiagnostic(runtime, `Note: Analysis runs in the background. Read it with ${overview}. The competitor's collectedAt stays null until the analysis finishes.`);
|
|
826
|
+
return result;
|
|
793
827
|
}
|
|
794
828
|
const response = await withSpinner(runtime, 'Removing competitor', () => resolved.value.api.client.research.removeCompetitor({
|
|
795
829
|
params: { siteId: resolved.value.siteId, competitorId: action.competitorId },
|
|
@@ -976,6 +1010,54 @@ async function sitemapsList(runtime, globals) {
|
|
|
976
1010
|
}, { signal: runtime.requestSignal }));
|
|
977
1011
|
return present(runtime, globals, response, value => renderSitemaps(value.data));
|
|
978
1012
|
}
|
|
1013
|
+
async function sitemapsSubmission(runtime, globals) {
|
|
1014
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1015
|
+
if (resolved._tag === 'Err')
|
|
1016
|
+
return resolved;
|
|
1017
|
+
const response = await withSpinner(runtime, 'Loading Sitemap submission', () => resolved.value.api.client.sitemaps.readSubmission({
|
|
1018
|
+
params: { siteId: resolved.value.siteId },
|
|
1019
|
+
}, { signal: runtime.requestSignal }));
|
|
1020
|
+
return present(runtime, globals, response, value => renderSitemapSubmission(value.data));
|
|
1021
|
+
}
|
|
1022
|
+
async function searchWatches(runtime, globals) {
|
|
1023
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1024
|
+
if (resolved._tag === 'Err')
|
|
1025
|
+
return resolved;
|
|
1026
|
+
const response = await withSpinner(runtime, 'Loading Watched URLs', () => resolved.value.api.client.search.listWatches({
|
|
1027
|
+
params: { siteId: resolved.value.siteId },
|
|
1028
|
+
}, { signal: runtime.requestSignal }));
|
|
1029
|
+
return present(runtime, globals, response, value => renderIndexingWatches(value.data));
|
|
1030
|
+
}
|
|
1031
|
+
async function sitemapsInspect(runtime, globals, rawUrl, engine) {
|
|
1032
|
+
const url = parseAbsolutePageUrl(rawUrl);
|
|
1033
|
+
if (url._tag === 'Err')
|
|
1034
|
+
return url;
|
|
1035
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1036
|
+
if (resolved._tag === 'Err')
|
|
1037
|
+
return resolved;
|
|
1038
|
+
const response = await withSpinner(runtime, 'Reading Sitemap evidence', () => resolved.value.api.client.sitemaps.inspect({
|
|
1039
|
+
params: { siteId: resolved.value.siteId },
|
|
1040
|
+
query: { url: url.value, searchEngine: engine },
|
|
1041
|
+
}, { signal: runtime.requestSignal }));
|
|
1042
|
+
if (response._tag === 'Err')
|
|
1043
|
+
return fromSdkFailure(response.error);
|
|
1044
|
+
if (response.value.data.state._tag === 'unavailable') {
|
|
1045
|
+
const { reason } = response.value.data.state;
|
|
1046
|
+
const exitCode = reason === 'throttled' || reason === 'provider-unavailable'
|
|
1047
|
+
? EXIT_CODE.retryable
|
|
1048
|
+
: reason === 'grant-missing' || reason === 'reauthorization-required'
|
|
1049
|
+
? EXIT_CODE.authentication
|
|
1050
|
+
: reason === 'permission-lost' || reason === 'not-enabled'
|
|
1051
|
+
? EXIT_CODE.authorization
|
|
1052
|
+
: EXIT_CODE.conflict;
|
|
1053
|
+
const incomplete = fail(exitCode, `Sitemap evidence is unavailable: ${reason}.`);
|
|
1054
|
+
if (globals.json)
|
|
1055
|
+
return { _tag: 'Err', error: { ...incomplete.error, protocolResponse: response.value } };
|
|
1056
|
+
writeOutput(runtime, renderSitemapEvidence(response.value.data));
|
|
1057
|
+
return incomplete;
|
|
1058
|
+
}
|
|
1059
|
+
return present(runtime, globals, response, value => renderSitemapEvidence(value.data));
|
|
1060
|
+
}
|
|
979
1061
|
async function sitemapsUrls(runtime, globals, args) {
|
|
980
1062
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 1000, defaultValue: 500 });
|
|
981
1063
|
if (limit._tag === 'Err')
|
|
@@ -1023,7 +1105,19 @@ async function sitemapsAction(runtime, globals, action, rawUrl) {
|
|
|
1023
1105
|
params: { siteId: resolved.value.siteId },
|
|
1024
1106
|
body: { action, sitemapUrl: sitemapUrl.value },
|
|
1025
1107
|
}, { signal: runtime.requestSignal }));
|
|
1026
|
-
|
|
1108
|
+
if (action === 'delete')
|
|
1109
|
+
return present(runtime, globals, response, value => renderSitemapAction(value.data));
|
|
1110
|
+
// Success means Search Console accepted the submit, not that Google read the
|
|
1111
|
+
// file. Name the read that shows the download, so the caller does not take
|
|
1112
|
+
// the submit for a fetch.
|
|
1113
|
+
const list = `\`nuxtseo sitemaps list --site ${resolved.value.siteId}\``;
|
|
1114
|
+
const result = present(runtime, globals, response, value => [
|
|
1115
|
+
renderSitemapAction(value.data),
|
|
1116
|
+
`Google downloads it later. Run ${list} to see when.`,
|
|
1117
|
+
].join('\n'));
|
|
1118
|
+
if (result._tag === 'Ok' && globals.json)
|
|
1119
|
+
writeDiagnostic(runtime, `Note: Search Console accepted the sitemap. Google downloads it later. Run ${list} and read lastDownloaded and isPending.`);
|
|
1120
|
+
return result;
|
|
1027
1121
|
}
|
|
1028
1122
|
async function analyticsQuery(runtime, globals, args) {
|
|
1029
1123
|
const view = parseChoice(args.view, { name: 'view', choices: ANALYTICS_VIEWS });
|
|
@@ -1996,6 +2090,10 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1996
2090
|
const search = defineCommand({
|
|
1997
2091
|
meta: { name: 'search', description: 'Read Search Console state' },
|
|
1998
2092
|
subCommands: {
|
|
2093
|
+
'watches': defineCommand({
|
|
2094
|
+
meta: { name: 'watches', description: 'Read Watched URLs and retained Checkpoints' },
|
|
2095
|
+
run: ({ args }) => capture(execution, () => searchWatches(runtime, globals), args._, 0)(),
|
|
2096
|
+
}),
|
|
1999
2097
|
'status': defineCommand({
|
|
2000
2098
|
meta: { name: 'status', description: 'Read the stored Search Console connection' },
|
|
2001
2099
|
run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
|
|
@@ -2103,6 +2201,18 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
2103
2201
|
const sitemaps = defineCommand({
|
|
2104
2202
|
meta: { name: 'sitemaps', description: 'Read and manage Search Console sitemaps' },
|
|
2105
2203
|
subCommands: {
|
|
2204
|
+
submission: defineCommand({
|
|
2205
|
+
meta: { name: 'submission', description: 'Read site-level Google Sitemap submission readiness' },
|
|
2206
|
+
run: ({ args }) => capture(execution, () => sitemapsSubmission(runtime, globals), args._, 0)(),
|
|
2207
|
+
}),
|
|
2208
|
+
inspect: defineCommand({
|
|
2209
|
+
meta: { name: 'inspect', description: 'Read exact Sitemap listing evidence' },
|
|
2210
|
+
args: {
|
|
2211
|
+
url: positional('sitemap-url', 'Absolute Sitemap URL'),
|
|
2212
|
+
engine: { type: 'enum', options: ['google', 'bing'], default: 'google', description: 'Google live or Bing stored evidence' },
|
|
2213
|
+
},
|
|
2214
|
+
run: ({ args }) => capture(execution, () => sitemapsInspect(runtime, globals, args.url, args.engine), args._, 1)(),
|
|
2215
|
+
}),
|
|
2106
2216
|
list: defineCommand({
|
|
2107
2217
|
meta: { name: 'list', description: 'List the sitemaps Search Console knows' },
|
|
2108
2218
|
run: ({ args }) => capture(execution, () => sitemapsList(runtime, globals), args._, 0)(),
|
package/dist/failures.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare const EXIT_CODE: {
|
|
|
12
12
|
readonly interrupted: 130;
|
|
13
13
|
};
|
|
14
14
|
export type ExitCode = typeof EXIT_CODE[keyof typeof EXIT_CODE];
|
|
15
|
-
export type LocalFailureCode = 'authentication_required' | 'authorization_required' | 'confirmation_required' | 'conflict' | 'contract_violation' | 'infrastructure_failure' | 'interrupted' | 'invalid_cli_input' | 'not_found' | 'paging_cap_reached' | 'pull_incomplete' | 'request_timeout' | 'retryable_failure' | 'site_ambiguous' | 'site_empty' | 'site_not_accessible' | 'state_error' | 'token_input_required' | 'transport_failure' | 'unexpected';
|
|
15
|
+
export type LocalFailureCode = 'authentication_required' | 'authorization_required' | 'confirmation_required' | 'conflict' | 'contract_violation' | 'infrastructure_failure' | 'interrupted' | 'invalid_cli_input' | 'not_found' | 'paging_cap_reached' | 'paging_unstable' | 'pull_incomplete' | 'request_timeout' | 'retryable_failure' | 'site_ambiguous' | 'site_empty' | 'site_not_accessible' | 'state_error' | 'token_input_required' | 'transport_failure' | 'unexpected';
|
|
16
16
|
export type CliFailureCode = LocalFailureCode | Extract<SdkFailure, {
|
|
17
17
|
_tag: 'ApiFailure';
|
|
18
18
|
}>['code'];
|
package/dist/render.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ 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, UrlInspectionRefreshData } from '@nuxtseo/protocol/v1/gsc';
|
|
7
|
+
import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, IndexingWatchesData, 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';
|
|
11
|
-
import type { SiteSitemapActionResponse, SiteSitemapsResponse, SiteSitemapUrlsResponse } from '@nuxtseo/protocol/v1/sitemaps';
|
|
11
|
+
import type { SiteSitemapActionResponse, SiteSitemapEvidenceData, SiteSitemapsResponse, SiteSitemapSubmissionData, SiteSitemapUrlsResponse } from '@nuxtseo/protocol/v1/sitemaps';
|
|
12
12
|
import type { Annotation, AnnotationDeleted, Annotations, TimelineEntries } from '@nuxtseo/protocol/v1/timeline';
|
|
13
13
|
export declare function renderSites(response: SitesListResponse, selectedSiteId?: string): string;
|
|
14
14
|
export declare function renderUsage(response: AccountUsageResponse): string;
|
|
@@ -25,6 +25,9 @@ export declare function renderUrlInspectionRefresh(data: UrlInspectionRefreshDat
|
|
|
25
25
|
export declare function renderUrlInspection(data: UrlInspectionData): string;
|
|
26
26
|
export declare function renderAuditChanges(data: AuditChanges): string;
|
|
27
27
|
export declare function renderLinkStructure(data: AuditLinkStructure): string;
|
|
28
|
+
export declare function renderSitemapSubmission(data: SiteSitemapSubmissionData): string;
|
|
29
|
+
export declare function renderSitemapEvidence(data: SiteSitemapEvidenceData): string;
|
|
30
|
+
export declare function renderIndexingWatches(data: IndexingWatchesData): string;
|
|
28
31
|
export declare function renderSitemaps(data: SiteSitemapsResponse['data']): string;
|
|
29
32
|
export declare function renderSitemapUrls(data: SiteSitemapUrlsResponse['data']): string;
|
|
30
33
|
export declare function renderSitemapAction(data: SiteSitemapActionResponse['data']): string;
|
package/dist/render.js
CHANGED
|
@@ -348,6 +348,23 @@ export function renderLinkStructure(data) {
|
|
|
348
348
|
];
|
|
349
349
|
return [header, ...slots, ...deadEnds, ...anchors].join('\n');
|
|
350
350
|
}
|
|
351
|
+
export function renderSitemapSubmission(data) {
|
|
352
|
+
return `Google Sitemap submission: ${data.state._tag}\n${JSON.stringify(data.state, null, 2)}\nCan act: ${data.callerCanAct}`;
|
|
353
|
+
}
|
|
354
|
+
export function renderSitemapEvidence(data) {
|
|
355
|
+
return [
|
|
356
|
+
`${data.searchEngine} Sitemap: ${data.sitemapUrl}`,
|
|
357
|
+
`State: ${data.state._tag}`,
|
|
358
|
+
data.capture._tag === 'captured' ? `Capture: ${data.capture.source} at ${data.capture.capturedAt}` : 'Capture: unavailable',
|
|
359
|
+
JSON.stringify(data.state, null, 2),
|
|
360
|
+
].join('\n');
|
|
361
|
+
}
|
|
362
|
+
export function renderIndexingWatches(data) {
|
|
363
|
+
return [
|
|
364
|
+
`Watched URLs: ${data.watched.length}/${data.limit}. Cadence: ${data.cadenceDays} days.`,
|
|
365
|
+
...data.watched.map(watch => `${watch.url}\n Due: ${watch.dueAt}\n Checkpoints: ${JSON.stringify(watch.checkpoints)}`),
|
|
366
|
+
].join('\n');
|
|
367
|
+
}
|
|
351
368
|
export function renderSitemaps(data) {
|
|
352
369
|
if (data.sitemaps.length === 0)
|
|
353
370
|
return `No sitemaps are known for ${data.meta.siteUrl}.`;
|
|
@@ -469,6 +486,16 @@ export function renderIndexingDiagnostics(data) {
|
|
|
469
486
|
data.hasMore ? `More URLs available after offset ${data.offset + data.limit}.` : '',
|
|
470
487
|
].filter(Boolean).join('\n');
|
|
471
488
|
}
|
|
489
|
+
function renderResearchCompetitor(row) {
|
|
490
|
+
// A competitor without a collection time is still queued; its zero counts
|
|
491
|
+
// mean nothing yet.
|
|
492
|
+
if (row.collectedAt === null)
|
|
493
|
+
return `${row.domain} analysis pending`;
|
|
494
|
+
// Quick wins come from shared keywords. With none shared, "0 quick wins"
|
|
495
|
+
// says nothing about positions, so name the reason.
|
|
496
|
+
const overlap = row.sharedKeywords.length ? '' : ' no shared keywords';
|
|
497
|
+
return `${row.domain} ${row.metrics.organicTraffic ?? 'unknown'} traffic ${row.quickWins.length} quick wins${overlap}`;
|
|
498
|
+
}
|
|
472
499
|
export function renderResearchOverview(data) {
|
|
473
500
|
const subject = data.subject
|
|
474
501
|
? fields([
|
|
@@ -480,7 +507,7 @@ export function renderResearchOverview(data) {
|
|
|
480
507
|
: 'No stored Site research.';
|
|
481
508
|
const competitors = data.competitors.length === 0
|
|
482
509
|
? 'No stored competitors.'
|
|
483
|
-
: data.competitors.map(
|
|
510
|
+
: data.competitors.map(renderResearchCompetitor).join('\n');
|
|
484
511
|
return [subject, `Competitors: ${data.summary.competitorCount}`, competitors].join('\n');
|
|
485
512
|
}
|
|
486
513
|
export function renderKeywordResearch(data) {
|
package/dist/skill.js
CHANGED
|
@@ -32,6 +32,15 @@ const AGENT_DIRECTORIES = {
|
|
|
32
32
|
claude: '.claude',
|
|
33
33
|
codex: '.codex',
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The notice names the agent. The check reads every agent's install, so a
|
|
37
|
+
* line without the name reads as the caller's own skill when another agent's
|
|
38
|
+
* copy is the stale one.
|
|
39
|
+
*/
|
|
40
|
+
const AGENT_NAMES = {
|
|
41
|
+
claude: 'Claude Code',
|
|
42
|
+
codex: 'Codex',
|
|
43
|
+
};
|
|
35
44
|
/**
|
|
36
45
|
* The skill shipped inside this package. A global install puts it outside the
|
|
37
46
|
* project, so the documented `cp -R node_modules/...` path does not exist for
|
|
@@ -210,7 +219,8 @@ export function skillNoticeFor(installed, current, agent) {
|
|
|
210
219
|
return installed.version === current ? null : { agent, installed: installed.version, current };
|
|
211
220
|
}
|
|
212
221
|
export function skillNoticeLine(notice) {
|
|
213
|
-
const
|
|
222
|
+
const agent = AGENT_NAMES[notice.agent];
|
|
223
|
+
const installed = notice.installed === null ? `${agent} skill version unknown` : `${agent} skill ${notice.installed} installed`;
|
|
214
224
|
if (notice.installed !== null) {
|
|
215
225
|
const installedParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.installed);
|
|
216
226
|
const currentParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.current);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
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.
|
|
37
|
-
"@nuxtseo/sdk": "^0.5.
|
|
36
|
+
"@nuxtseo/protocol": "^0.5.8",
|
|
37
|
+
"@nuxtseo/sdk": "^0.5.8",
|
|
38
38
|
"citty": "^0.2.2",
|
|
39
39
|
"pathe": "^2.0.3"
|
|
40
40
|
},
|
|
@@ -17,7 +17,7 @@ Read the matching file before you act on its subject:
|
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| [references/commands.md](references/commands.md) | use a flag, range, or default. Never guess a flag |
|
|
19
19
|
| [references/protocol.md](references/protocol.md) | parse output, page through a list, or handle an exit code |
|
|
20
|
-
| [references/indexing.md](references/indexing.md) |
|
|
20
|
+
| [references/indexing.md](references/indexing.md) | interpret indexing, Sitemap evidence, or Watched URLs |
|
|
21
21
|
|
|
22
22
|
## Setup
|
|
23
23
|
|
|
@@ -44,9 +44,11 @@ Read the matching file before you act on its subject:
|
|
|
44
44
|
2. **Skill version.** Run `nuxtseo --version --json`. Compare the CLI version
|
|
45
45
|
with this file's frontmatter version. If the CLI is older, update the CLI
|
|
46
46
|
first. If the skill is older, run `nuxtseo skill install --agent claude` and
|
|
47
|
-
re-read this file. An older skill hides commands.
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
re-read this file. An older skill hides commands. The stderr line names
|
|
48
|
+
the agent whose copy differs, such as `Codex skill 0.5.3 installed`.
|
|
49
|
+
Refresh only that agent. If `NUXTSEO_NO_UPDATE_CHECK` is set, the line
|
|
50
|
+
never appears; compare this file's frontmatter `version` with the binary
|
|
51
|
+
instead.
|
|
50
52
|
|
|
51
53
|
3. **Token.** Run `nuxtseo whoami --json`. It returns the Team, role, scopes,
|
|
52
54
|
and token expiry. Exit `3` means no working token. Ask the user to run
|
|
@@ -198,8 +198,11 @@ The monitored Page then follows its own recurring schedule.
|
|
|
198
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 |
|
|
199
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` |
|
|
200
200
|
| `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
|
|
201
|
+
| `sitemaps submission` | Site-level Google Sitemap submission state. Redacts account identity. Does not prove listing for an arbitrary Sitemap URL |
|
|
202
|
+
| `sitemaps inspect <sitemap-url>` | Exact Sitemap listing evidence. `--engine google\|bing` defaults to `google`. Google reads live. Bing reads dated stored data. Missing exits `0`; unavailable exits `3`, `4`, `5`, or `6` |
|
|
203
|
+
| `search watches` | Retained Watched URLs, `dueAt`, and Checkpoints. An empty Checkpoint list means no retained Checkpoint for that watch |
|
|
201
204
|
| `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
|
|
202
|
-
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential |
|
|
205
|
+
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential. Success means Search Console accepted it, not that Google downloaded it. Read `lastDownloaded` and `isPending` in `sitemaps list` |
|
|
203
206
|
| `sitemaps delete <sitemap-url>` | Mutation. Removes one sitemap through the Site's stored Search Console credential |
|
|
204
207
|
|
|
205
208
|
Analysis presets for `search analytics analysis` are `striking-distance`,
|
|
@@ -258,9 +261,9 @@ research boundary in SKILL.md first.
|
|
|
258
261
|
|
|
259
262
|
| Command | What it returns, and its flags |
|
|
260
263
|
| --- | --- |
|
|
261
|
-
| `research overview` | Stored Site and competitor research: metrics, history, gaps, quick wins. Takes no flags. Returns `data.competitors` and `data.summary` |
|
|
264
|
+
| `research overview` | Stored Site and competitor research: metrics, history, gaps, quick wins. Takes no flags. Returns `data.competitors` and `data.summary`. A competitor with `collectedAt: null` is still queued, so its empty lists mean nothing yet. Quick wins come from `sharedKeywords`; when that list is empty, zero quick wins says nothing about positions |
|
|
262
265
|
| `research competitors list` | Tracked competitor domains and ids for this Site. Suggested competitors stay excluded. Reads start no provider work. |
|
|
263
|
-
| `research competitors add <domain>` | Mutation, requires `research:write` and `--yes`. Accepts a public domain or URL. Queues analysis within existing Site and Team limits. Returns `data.competitor.id` and `data.status: queued`. |
|
|
266
|
+
| `research competitors add <domain>` | Mutation, requires `research:write` and `--yes`. Accepts a public domain or URL. Queues analysis within existing Site and Team limits. Returns `data.competitor.id` and `data.status: queued`. The analysis runs in the background; read it with `research overview` |
|
|
264
267
|
| `research competitors remove <competitor-id>` | Mutation, requires `research:write` and `--yes`. Use an id from the list. Removes that Site's competitor without provider work. |
|
|
265
268
|
| `research keywords <topic>` | Live keyword ideas, with volume, difficulty, intent, and cost data. The seed must be 1 to 3 words. The server refuses a longer seed. The refusal arrives as exit `0` with an empty `data.keywords`, so read `data.message` first. Comma-separate several short seeds instead. Special characters are stripped before the lookup. If a seed is under 2 characters after stripping, the server refuses it the same way. The server applies default filters and echoes them in `data.filters`: `--min-volume` (default 10), `--max-volume` (default 10000), `--min-difficulty 0..100` (default 0), `--max-difficulty 0..100` (default 60). So a default run hides keywords over difficulty 60 or under 10 searches. Also `--limit 1..100` (default 20), `--intent`, `--location-code` (default 2840), `--no-related` to drop related keywords, and `--verbose` for full evidence. If `--max-volume` is under `--min-volume`, the run exits `2`. The same holds for `--max-difficulty` under `--min-difficulty` |
|
|
266
269
|
| `research serp <keyword>` | A live SERP snapshot: results, features, fetch time. `--depth 1..20` (default 10) sets how many organic results return. `--location-code` (default 2840). The keyword must be 2 to 200 characters. `data.cached` reports cache use |
|
|
@@ -65,4 +65,19 @@ Keep these reads separate:
|
|
|
65
65
|
| Which URLs appeared in the current period? | `search analytics pages` | Rows with `impressions > 0`, then each `url` |
|
|
66
66
|
| What did Google say at the last inspection? | `search inspect <url>` | The retained verdict and inspection date |
|
|
67
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 |
|
|
68
|
-
| Which URLs
|
|
68
|
+
| Which URLs belong to the Sitemap generation? | `sitemaps list`, then `sitemaps urls` | Sitemap totals and retained membership |
|
|
69
|
+
| Does a provider list this exact Sitemap URL? | `sitemaps inspect <sitemap-url>` | `state`, `capture.source`, and `capture.capturedAt` |
|
|
70
|
+
| Can the Site submit its selected Google Sitemap? | `sitemaps submission` | Site-level `state` and `callerCanAct` |
|
|
71
|
+
| What checks exist for Watched URLs? | `search watches` | Each URL's `dueAt` and retained Checkpoints |
|
|
72
|
+
|
|
73
|
+
`sitemaps inspect` uses `--engine google` by default. Use `--engine bing` for Bing's stored evidence.
|
|
74
|
+
Google reads live. Bing reports its stored capture time; it does not make a live provider request.
|
|
75
|
+
`state._tag: missing` proves absence only within that successful dated read.
|
|
76
|
+
`state._tag: unavailable` provides no listing conclusion and produces a nonzero exit.
|
|
77
|
+
Grant or reauthorization failures exit `3`. Permission loss or disabled access exits `4`.
|
|
78
|
+
Unverified or unchecked evidence exits `5`. Throttling or provider failure exits `6`.
|
|
79
|
+
The JSON response preserves the evidence with these failures.
|
|
80
|
+
|
|
81
|
+
Watched URL due times describe scheduling. They do not prove that checks can run.
|
|
82
|
+
An empty Checkpoint list means no retained Checkpoint for that watch.
|
|
83
|
+
Neither Sitemap listing nor Watched URL enrollment proves that Google indexed a Page.
|
|
@@ -89,6 +89,14 @@ The `--all` loop stops after 50 requests. If pages remain, the CLI exits `9`
|
|
|
89
89
|
and stderr names the argument that resumes the read, for example `--offset 50`.
|
|
90
90
|
Treat exit `9` as a stop, never as the end of the data.
|
|
91
91
|
|
|
92
|
+
Search Console breakdown exports stop with exit `7` and `paging_unstable`
|
|
93
|
+
when an exact row key repeats. The CLI preserves the offending response.
|
|
94
|
+
The export is incomplete. Preserve the responses and report the ordering issue.
|
|
95
|
+
Do not resume from a later page, because overlapping pages can also omit rows.
|
|
96
|
+
Canonical page URLs can repeat. The check uses `gscPage`, Google's exact page string.
|
|
97
|
+
Older page responses can omit `gscPage`, so those rows have no overlap check.
|
|
98
|
+
Analysis shortlists have no unique row contract and are excluded from this check.
|
|
99
|
+
|
|
92
100
|
Pass the reported `offset` or opaque `cursor` for another page. Never edit or
|
|
93
101
|
infer a cursor. Keep server order for actions. Never re-rank merged results.
|
|
94
102
|
|
|
@@ -127,6 +135,9 @@ command returns; never guess a chain. Verified against the response schemas.
|
|
|
127
135
|
| `search index-history` | `data.changes` | none |
|
|
128
136
|
| `search cohorts` | `data.established` and `data.ranked` | `data.rankedTotal` |
|
|
129
137
|
| `sitemaps list` | `data.sitemaps` | none |
|
|
138
|
+
| `sitemaps submission` | no list; `data.state` | none |
|
|
139
|
+
| `sitemaps inspect` | no list; `data.capture`, `data.state` | none |
|
|
140
|
+
| `search watches` | `data.watched`; each item's `checkpoints` | none |
|
|
130
141
|
| `sitemaps urls` | `data.items` | `data.page.nextCursor`, `data.page.limit` |
|
|
131
142
|
| `audit content-decay` | `data.rows` | `data.page`; `data.total`, `data.truncated` |
|
|
132
143
|
| `audit link-opportunities` | `data.rows` | `data.page`; `data.total` |
|