@nuxtseo/cli 0.1.4 → 0.2.0
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 +113 -31
- package/dist/api.js +7 -1
- package/dist/cli.js +62 -11
- package/dist/command-contract.js +10 -0
- package/dist/commands.d.ts +19 -0
- package/dist/commands.js +1215 -89
- package/dist/failures.d.ts +2 -1
- package/dist/failures.js +21 -1
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +21 -0
- package/dist/render.d.ts +43 -9
- package/dist/render.js +636 -5
- package/dist/skill.d.ts +22 -0
- package/dist/skill.js +56 -0
- package/package.json +9 -8
- package/skills/nuxtseo-cli/SKILL.md +236 -107
- package/skills/nuxtseo-cli/references/commands.md +162 -0
- package/skills/nuxtseo-cli/references/indexing.md +60 -0
- package/skills/nuxtseo-cli/references/protocol.md +15 -4
package/dist/commands.js
CHANGED
|
@@ -1,24 +1,64 @@
|
|
|
1
1
|
import * as prompts from '@clack/prompts';
|
|
2
|
+
import { publicV1BooleanFlagValue } from '@nuxtseo/protocol/v1/core';
|
|
2
3
|
import { defineCommand } from 'citty';
|
|
4
|
+
import { dirname } from 'pathe';
|
|
3
5
|
import { createApiClient, fromStateError, loadApiContext } from './api.js';
|
|
4
6
|
import { openBrowser } from './browser.js';
|
|
5
7
|
import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
|
|
6
8
|
import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
|
|
7
|
-
import { parseAbsolutePageUrl, parseChoice, parseInteger } from './parse.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
|
+
import { parseAbsolutePageUrl, parseChoice, parseInteger, parsePageUrlOrPath, parseRatio } from './parse.js';
|
|
10
|
+
import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentBrief, renderContentBriefCreated, renderContentBriefs, 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';
|
|
9
11
|
import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
|
|
10
12
|
import { resolveSite } from './site.js';
|
|
13
|
+
import { installSkill, SKILL_AGENTS } from './skill.js';
|
|
11
14
|
import { clearCredential, getCredentialStatus, readConfig, resolveApiUrl, saveCredential, updateConfig, } from './state/index.js';
|
|
12
15
|
import { VERSION } from './version.js';
|
|
16
|
+
function emit(runtime, globals, value, render) {
|
|
17
|
+
if (globals.json)
|
|
18
|
+
writeProtocolResponse(runtime, value);
|
|
19
|
+
else
|
|
20
|
+
writeOutput(runtime, render(value));
|
|
21
|
+
}
|
|
13
22
|
function present(runtime, globals, result, render) {
|
|
14
23
|
if (result._tag === 'Err')
|
|
15
24
|
return fromSdkFailure(result.error);
|
|
16
|
-
|
|
17
|
-
writeProtocolResponse(runtime, result.value);
|
|
18
|
-
else
|
|
19
|
-
writeOutput(runtime, render(result.value));
|
|
25
|
+
emit(runtime, globals, result.value, render);
|
|
20
26
|
return ok(undefined);
|
|
21
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* `--all` request cap. One page per request stays the default. `--all` repeats
|
|
30
|
+
* the same operation and writes one complete envelope per page, so nothing is
|
|
31
|
+
* merged or renamed. The cap bounds an unattended loop; reaching it exits 9 and
|
|
32
|
+
* names the resume argument, so a caller never mistakes a stop for an end.
|
|
33
|
+
*/
|
|
34
|
+
export const ALL_PAGES_REQUEST_CAP = 50;
|
|
35
|
+
async function collect(runtime, globals, options) {
|
|
36
|
+
let position = options.start;
|
|
37
|
+
for (let request = 1;; request++) {
|
|
38
|
+
const response = await withSpinner(runtime, options.message, () => options.fetch(position));
|
|
39
|
+
if (response._tag === 'Err')
|
|
40
|
+
return fromSdkFailure(response.error);
|
|
41
|
+
emit(runtime, globals, response.value, options.render);
|
|
42
|
+
if (!options.all)
|
|
43
|
+
return ok(undefined);
|
|
44
|
+
const next = options.next(response.value, position);
|
|
45
|
+
if (next._tag === 'Done')
|
|
46
|
+
return ok(undefined);
|
|
47
|
+
if (request >= ALL_PAGES_REQUEST_CAP) {
|
|
48
|
+
return fail(EXIT_CODE.pagingCapReached, [
|
|
49
|
+
`paging_cap_reached: --all stopped after ${ALL_PAGES_REQUEST_CAP} requests.`,
|
|
50
|
+
`More pages remain. Run the command again with ${next.resume}.`,
|
|
51
|
+
].join('\n'), undefined, 'paging_cap_reached');
|
|
52
|
+
}
|
|
53
|
+
position = next.position;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function offsetPage(cursor) {
|
|
57
|
+
const offset = cursor.offset + cursor.limit;
|
|
58
|
+
return cursor.hasMore
|
|
59
|
+
? { _tag: 'More', position: { offset }, resume: `--offset ${offset}` }
|
|
60
|
+
: { _tag: 'Done' };
|
|
61
|
+
}
|
|
22
62
|
async function withSpinner(runtime, message, task) {
|
|
23
63
|
const spinner = runtime.interactive
|
|
24
64
|
? prompts.spinner({ output: runtime.error, signal: runtime.signal })
|
|
@@ -122,8 +162,11 @@ async function login(runtime, globals, options) {
|
|
|
122
162
|
: await pairInBrowser(runtime, apiUrl.value.apiUrl, { openBrowser: !options.noBrowser });
|
|
123
163
|
if (token._tag === 'Err')
|
|
124
164
|
return token;
|
|
165
|
+
// Introspection proves the credential and reports what it can do. Listing
|
|
166
|
+
// Sites proved only that one read succeeded, and read the whole portfolio to
|
|
167
|
+
// do it.
|
|
125
168
|
const client = createApiClient(apiUrl.value.apiUrl, token.value);
|
|
126
|
-
const validated = await withSpinner(runtime, 'Validating token', () => client.
|
|
169
|
+
const validated = await withSpinner(runtime, 'Validating token', () => client.account.token({}, { signal: runtime.requestSignal }));
|
|
127
170
|
if (validated._tag === 'Err')
|
|
128
171
|
return fromSdkFailure(validated.error);
|
|
129
172
|
const saved = await saveCredential(token.value, { paths: runtime.paths });
|
|
@@ -141,10 +184,16 @@ async function login(runtime, globals, options) {
|
|
|
141
184
|
if (runtime.env.NUXTSEO_TOKEN !== undefined) {
|
|
142
185
|
writeDiagnostic(runtime, 'warning: NUXTSEO_TOKEN remains active and takes precedence over the stored credential.');
|
|
143
186
|
}
|
|
144
|
-
if (globals.json)
|
|
187
|
+
if (globals.json) {
|
|
145
188
|
writeProtocolResponse(runtime, validated.value);
|
|
146
|
-
|
|
147
|
-
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
writeOutput(runtime, [
|
|
192
|
+
`Logged in as ${validated.value.data.role} on ${validated.value.data.team.name ?? validated.value.data.team.id}.`,
|
|
193
|
+
`Credential: ${saved.value.storage}.`,
|
|
194
|
+
`Scopes: ${validated.value.data.scopes.join(', ')}`,
|
|
195
|
+
].join('\n'));
|
|
196
|
+
}
|
|
148
197
|
return ok(undefined);
|
|
149
198
|
}
|
|
150
199
|
async function logout(runtime, globals) {
|
|
@@ -168,18 +217,17 @@ async function whoami(runtime, globals) {
|
|
|
168
217
|
const api = await loadApiContext(runtime, globals);
|
|
169
218
|
if (api._tag === 'Err')
|
|
170
219
|
return api;
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
return fromSdkFailure(
|
|
220
|
+
const introspected = await withSpinner(runtime, 'Checking authentication', () => api.value.client.account.token({}, { signal: runtime.requestSignal }));
|
|
221
|
+
if (introspected._tag === 'Err')
|
|
222
|
+
return fromSdkFailure(introspected.error);
|
|
174
223
|
if (globals.json) {
|
|
175
|
-
writeProtocolResponse(runtime,
|
|
224
|
+
writeProtocolResponse(runtime, introspected.value);
|
|
176
225
|
}
|
|
177
226
|
else {
|
|
178
227
|
writeOutput(runtime, [
|
|
179
|
-
|
|
228
|
+
renderAccountToken(introspected.value.data),
|
|
180
229
|
`API: ${api.value.apiUrl}`,
|
|
181
|
-
`Credential: ${api.value.credentialSource}`,
|
|
182
|
-
`Accessible Sites: ${listed.value.data.sites.length}`,
|
|
230
|
+
`Credential source: ${api.value.credentialSource}`,
|
|
183
231
|
`Selected Site: ${api.value.config.siteId ?? 'none'}`,
|
|
184
232
|
].join('\n'));
|
|
185
233
|
}
|
|
@@ -297,21 +345,27 @@ async function usage(runtime, globals, group) {
|
|
|
297
345
|
const response = await withSpinner(runtime, 'Loading usage', () => api.value.client.account.usage({ query: { group } }, { signal: runtime.requestSignal }));
|
|
298
346
|
return present(runtime, globals, response, renderUsage);
|
|
299
347
|
}
|
|
300
|
-
async function actionsList(runtime, globals,
|
|
301
|
-
const limit = parseInteger(
|
|
348
|
+
async function actionsList(runtime, globals, args) {
|
|
349
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 25, defaultValue: 10 });
|
|
302
350
|
if (limit._tag === 'Err')
|
|
303
351
|
return limit;
|
|
304
|
-
const offset = parseInteger(
|
|
352
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
305
353
|
if (offset._tag === 'Err')
|
|
306
354
|
return offset;
|
|
307
355
|
const resolved = await apiAndSite(runtime, globals);
|
|
308
356
|
if (resolved._tag === 'Err')
|
|
309
357
|
return resolved;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
358
|
+
return collect(runtime, globals, {
|
|
359
|
+
all: args.all === true,
|
|
360
|
+
message: 'Loading next actions',
|
|
361
|
+
start: { offset: offset.value },
|
|
362
|
+
fetch: position => resolved.value.api.client.actions.list({
|
|
363
|
+
params: { siteId: resolved.value.siteId },
|
|
364
|
+
query: { limit: limit.value, offset: position.offset ?? 0 },
|
|
365
|
+
}, { signal: runtime.requestSignal }),
|
|
366
|
+
render: value => renderActions(value.data),
|
|
367
|
+
next: value => offsetPage(value.data.page),
|
|
368
|
+
});
|
|
315
369
|
}
|
|
316
370
|
async function actionsShow(runtime, globals, args) {
|
|
317
371
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 50 });
|
|
@@ -362,16 +416,22 @@ async function pageInspect(runtime, globals, args) {
|
|
|
362
416
|
const resolved = await apiAndSite(runtime, globals);
|
|
363
417
|
if (resolved._tag === 'Err')
|
|
364
418
|
return resolved;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
419
|
+
return collect(runtime, globals, {
|
|
420
|
+
all: args.all === true,
|
|
421
|
+
message: 'Inspecting Page',
|
|
422
|
+
start: { offset: offset.value },
|
|
423
|
+
fetch: position => resolved.value.api.client.pages.inspect({
|
|
424
|
+
params: { siteId: resolved.value.siteId },
|
|
425
|
+
query: {
|
|
426
|
+
url: url.value,
|
|
427
|
+
includeResolved: publicV1BooleanFlagValue(Boolean(args.includeResolved)),
|
|
428
|
+
limit: limit.value,
|
|
429
|
+
offset: position.offset ?? 0,
|
|
430
|
+
},
|
|
431
|
+
}, { signal: runtime.requestSignal }),
|
|
432
|
+
render: value => renderPageInspection(value.data),
|
|
433
|
+
next: value => offsetPage(value.data.observations.pagination),
|
|
434
|
+
});
|
|
375
435
|
}
|
|
376
436
|
async function pageScan(runtime, globals, rawUrl) {
|
|
377
437
|
const url = parseAbsolutePageUrl(rawUrl);
|
|
@@ -411,8 +471,48 @@ async function searchStatus(runtime, globals) {
|
|
|
411
471
|
}, { signal: runtime.requestSignal }));
|
|
412
472
|
return present(runtime, globals, response, value => renderSearchStatus(value.data));
|
|
413
473
|
}
|
|
474
|
+
const SEARCH_ANALYTICS_VIEWS = [
|
|
475
|
+
'pages',
|
|
476
|
+
'keywords',
|
|
477
|
+
'countries',
|
|
478
|
+
'devices',
|
|
479
|
+
'timeseries',
|
|
480
|
+
'page-detail',
|
|
481
|
+
'keyword-detail',
|
|
482
|
+
'analysis',
|
|
483
|
+
];
|
|
484
|
+
/** The views the server pages with `total` and `rows`, so `--all` can walk them. */
|
|
485
|
+
const SEARCH_ANALYTICS_ROW_VIEWS = new Set(['pages', 'keywords', 'countries', 'devices', 'analysis']);
|
|
486
|
+
const SEARCH_ANALYSIS_PRESETS = [
|
|
487
|
+
'striking-distance',
|
|
488
|
+
'opportunity',
|
|
489
|
+
'decay',
|
|
490
|
+
'zero-click',
|
|
491
|
+
'non-brand',
|
|
492
|
+
'brand-only',
|
|
493
|
+
'movers-rising',
|
|
494
|
+
'movers-declining',
|
|
495
|
+
];
|
|
496
|
+
const INDEXING_TRANSITION_FIELDS = [
|
|
497
|
+
'indexStatus',
|
|
498
|
+
'coverageState',
|
|
499
|
+
'robotsTxtState',
|
|
500
|
+
'indexingState',
|
|
501
|
+
'pageFetchState',
|
|
502
|
+
'googleCanonical',
|
|
503
|
+
];
|
|
504
|
+
function optionalInteger(input, options) {
|
|
505
|
+
if (input === undefined)
|
|
506
|
+
return ok(undefined);
|
|
507
|
+
return parseInteger(input, { ...options, defaultValue: options.minimum });
|
|
508
|
+
}
|
|
509
|
+
function optionalChoice(input, options) {
|
|
510
|
+
if (input === undefined)
|
|
511
|
+
return ok(undefined);
|
|
512
|
+
return parseChoice(input, options);
|
|
513
|
+
}
|
|
414
514
|
async function searchAnalytics(runtime, globals, args) {
|
|
415
|
-
const view = parseChoice(args.view, { name: 'view', choices:
|
|
515
|
+
const view = parseChoice(args.view, { name: 'view', choices: SEARCH_ANALYTICS_VIEWS });
|
|
416
516
|
if (view._tag === 'Err')
|
|
417
517
|
return view;
|
|
418
518
|
const period = parseChoice(args.period, { name: '--period', choices: ['7d', '28d', '3m', '6m', '12m'], defaultValue: '28d' });
|
|
@@ -430,30 +530,100 @@ async function searchAnalytics(runtime, globals, args) {
|
|
|
430
530
|
const sortDir = parseChoice(args.sortDir, { name: '--sort-dir', choices: ['asc', 'desc'], defaultValue: 'desc' });
|
|
431
531
|
if (sortDir._tag === 'Err')
|
|
432
532
|
return sortDir;
|
|
533
|
+
const filter = optionalChoice(args.filter, { name: '--filter', choices: ['top-level', 'new', 'lost', 'improving', 'declining'] });
|
|
534
|
+
if (filter._tag === 'Err')
|
|
535
|
+
return filter;
|
|
536
|
+
const preset = optionalChoice(args.preset, { name: '--preset', choices: SEARCH_ANALYSIS_PRESETS });
|
|
537
|
+
if (preset._tag === 'Err')
|
|
538
|
+
return preset;
|
|
539
|
+
const pageUrl = args.pageUrl === undefined ? ok(undefined) : parsePageUrlOrPath(args.pageUrl);
|
|
540
|
+
if (pageUrl._tag === 'Err')
|
|
541
|
+
return pageUrl;
|
|
542
|
+
const minClicks = optionalInteger(args.minClicks, { name: '--min-clicks', minimum: 0 });
|
|
543
|
+
if (minClicks._tag === 'Err')
|
|
544
|
+
return minClicks;
|
|
545
|
+
const maxClicks = optionalInteger(args.maxClicks, { name: '--max-clicks', minimum: 0 });
|
|
546
|
+
if (maxClicks._tag === 'Err')
|
|
547
|
+
return maxClicks;
|
|
548
|
+
const minImpressions = optionalInteger(args.minImpressions, { name: '--min-impressions', minimum: 0 });
|
|
549
|
+
if (minImpressions._tag === 'Err')
|
|
550
|
+
return minImpressions;
|
|
551
|
+
const maxImpressions = optionalInteger(args.maxImpressions, { name: '--max-impressions', minimum: 0 });
|
|
552
|
+
if (maxImpressions._tag === 'Err')
|
|
553
|
+
return maxImpressions;
|
|
554
|
+
const minPosition = optionalInteger(args.minPosition, { name: '--min-position', minimum: 1, maximum: 100 });
|
|
555
|
+
if (minPosition._tag === 'Err')
|
|
556
|
+
return minPosition;
|
|
557
|
+
const maxPosition = optionalInteger(args.maxPosition, { name: '--max-position', minimum: 1, maximum: 100 });
|
|
558
|
+
if (maxPosition._tag === 'Err')
|
|
559
|
+
return maxPosition;
|
|
560
|
+
const maxCtr = args.maxCtr === undefined
|
|
561
|
+
? ok(undefined)
|
|
562
|
+
: parseRatio(args.maxCtr, '--max-ctr');
|
|
563
|
+
if (maxCtr._tag === 'Err')
|
|
564
|
+
return maxCtr;
|
|
565
|
+
// The server rejects these combinations too. Refusing here costs no request
|
|
566
|
+
// and names the exact missing argument.
|
|
567
|
+
if (view.value === 'page-detail' && !pageUrl.value)
|
|
568
|
+
return fail(EXIT_CODE.invalidInput, 'The page-detail view requires --page-url.');
|
|
569
|
+
if (view.value === 'keyword-detail' && !args.keyword)
|
|
570
|
+
return fail(EXIT_CODE.invalidInput, 'The keyword-detail view requires --keyword.');
|
|
571
|
+
if (view.value === 'analysis' && !preset.value)
|
|
572
|
+
return fail(EXIT_CODE.invalidInput, 'The analysis view requires --preset.');
|
|
573
|
+
if (view.value === 'analysis' && (preset.value === 'brand-only' || preset.value === 'non-brand') && !args.brandTerms?.trim())
|
|
574
|
+
return fail(EXIT_CODE.invalidInput, `The ${preset.value} preset requires --brand-terms.`);
|
|
575
|
+
if (args.all && !SEARCH_ANALYTICS_ROW_VIEWS.has(view.value))
|
|
576
|
+
return fail(EXIT_CODE.invalidInput, `--all supports only these views: ${[...SEARCH_ANALYTICS_ROW_VIEWS].join(', ')}.`);
|
|
433
577
|
const resolved = await apiAndSite(runtime, globals);
|
|
434
578
|
if (resolved._tag === 'Err')
|
|
435
579
|
return resolved;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
580
|
+
return collect(runtime, globals, {
|
|
581
|
+
all: args.all === true,
|
|
582
|
+
message: 'Loading Search Console rows',
|
|
583
|
+
start: { page: page.value },
|
|
584
|
+
fetch: position => resolved.value.api.client.search.queryAnalytics({
|
|
585
|
+
params: { siteId: resolved.value.siteId },
|
|
586
|
+
query: {
|
|
587
|
+
view: view.value,
|
|
588
|
+
period: period.value,
|
|
589
|
+
limit: limit.value,
|
|
590
|
+
page: position.page ?? 1,
|
|
591
|
+
search: args.search,
|
|
592
|
+
sort: sort.value,
|
|
593
|
+
sortDir: sortDir.value,
|
|
594
|
+
filter: filter.value,
|
|
595
|
+
pageUrl: pageUrl.value,
|
|
596
|
+
keyword: args.keyword,
|
|
597
|
+
preset: preset.value,
|
|
598
|
+
brandTerms: args.brandTerms,
|
|
599
|
+
minClicks: minClicks.value,
|
|
600
|
+
maxClicks: maxClicks.value,
|
|
601
|
+
minImpressions: minImpressions.value,
|
|
602
|
+
maxImpressions: maxImpressions.value,
|
|
603
|
+
minPosition: minPosition.value,
|
|
604
|
+
maxPosition: maxPosition.value,
|
|
605
|
+
maxCtr: maxCtr.value,
|
|
606
|
+
},
|
|
607
|
+
}, { signal: runtime.requestSignal }),
|
|
608
|
+
render: value => renderSearchAnalytics(value.data),
|
|
609
|
+
next: (value, position) => {
|
|
610
|
+
const data = value.data;
|
|
611
|
+
if (!('total' in data) || !('rows' in data))
|
|
612
|
+
return { _tag: 'Done' };
|
|
613
|
+
const current = position.page ?? 1;
|
|
614
|
+
if (data.rows.length === 0 || current * limit.value >= data.total)
|
|
615
|
+
return { _tag: 'Done' };
|
|
616
|
+
return { _tag: 'More', position: { page: current + 1 }, resume: `--page ${current + 1}` };
|
|
446
617
|
},
|
|
447
|
-
}
|
|
448
|
-
return present(runtime, globals, response, value => renderSearchAnalytics(value.data));
|
|
618
|
+
});
|
|
449
619
|
}
|
|
620
|
+
/** The two retained-coverage views, declared once so `--help --json` publishes them. */
|
|
621
|
+
const SEARCH_INDEXING_VIEWS = ['summary', 'urls'];
|
|
450
622
|
async function searchIndexing(runtime, globals, args) {
|
|
451
|
-
const view = parseChoice(args.view, { name: 'view', choices:
|
|
623
|
+
const view = parseChoice(args.view, { name: 'view', choices: SEARCH_INDEXING_VIEWS });
|
|
452
624
|
if (view._tag === 'Err')
|
|
453
625
|
return view;
|
|
454
|
-
const status = args.status
|
|
455
|
-
? ok(undefined)
|
|
456
|
-
: parseChoice(args.status, { name: '--status', choices: ['indexed', 'not_indexed', 'pending'] });
|
|
626
|
+
const status = optionalChoice(args.status, { name: '--status', choices: ['indexed', 'not_indexed', 'pending'] });
|
|
457
627
|
if (status._tag === 'Err')
|
|
458
628
|
return status;
|
|
459
629
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 500, defaultValue: 50 });
|
|
@@ -462,20 +632,62 @@ async function searchIndexing(runtime, globals, args) {
|
|
|
462
632
|
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
463
633
|
if (offset._tag === 'Err')
|
|
464
634
|
return offset;
|
|
635
|
+
if (args.all && view.value !== 'urls')
|
|
636
|
+
return fail(EXIT_CODE.invalidInput, '--all supports the urls view only.');
|
|
637
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
638
|
+
if (resolved._tag === 'Err')
|
|
639
|
+
return resolved;
|
|
640
|
+
return collect(runtime, globals, {
|
|
641
|
+
all: args.all === true,
|
|
642
|
+
message: 'Loading Search Console indexing',
|
|
643
|
+
start: { offset: offset.value },
|
|
644
|
+
fetch: position => resolved.value.api.client.search.readIndexingDiagnostics({
|
|
645
|
+
params: { siteId: resolved.value.siteId },
|
|
646
|
+
query: {
|
|
647
|
+
view: view.value,
|
|
648
|
+
issue: args.issue,
|
|
649
|
+
status: status.value,
|
|
650
|
+
limit: limit.value,
|
|
651
|
+
offset: position.offset ?? 0,
|
|
652
|
+
},
|
|
653
|
+
}, { signal: runtime.requestSignal }),
|
|
654
|
+
render: value => renderIndexingDiagnostics(value.data),
|
|
655
|
+
next: value => value.data.view === 'urls'
|
|
656
|
+
? offsetPage({ offset: value.data.offset, limit: value.data.limit, hasMore: value.data.hasMore })
|
|
657
|
+
: { _tag: 'Done' },
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async function searchIndexHistory(runtime, globals, args) {
|
|
661
|
+
const url = args.url === undefined ? ok(undefined) : parsePageUrlOrPath(args.url);
|
|
662
|
+
if (url._tag === 'Err')
|
|
663
|
+
return url;
|
|
664
|
+
const field = optionalChoice(args.field, { name: '--field', choices: INDEXING_TRANSITION_FIELDS });
|
|
665
|
+
if (field._tag === 'Err')
|
|
666
|
+
return field;
|
|
667
|
+
const days = parseInteger(args.days, { name: '--days', minimum: 1, maximum: 730, defaultValue: 180 });
|
|
668
|
+
if (days._tag === 'Err')
|
|
669
|
+
return days;
|
|
465
670
|
const resolved = await apiAndSite(runtime, globals);
|
|
466
671
|
if (resolved._tag === 'Err')
|
|
467
672
|
return resolved;
|
|
468
|
-
const response = await withSpinner(runtime, 'Loading
|
|
673
|
+
const response = await withSpinner(runtime, 'Loading indexing history', () => resolved.value.api.client.search.readIndexingHistory({
|
|
469
674
|
params: { siteId: resolved.value.siteId },
|
|
470
|
-
query: {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
675
|
+
query: { url: url.value, field: field.value, days: days.value },
|
|
676
|
+
}, { signal: runtime.requestSignal }));
|
|
677
|
+
return present(runtime, globals, response, value => renderIndexingHistory(value.data));
|
|
678
|
+
}
|
|
679
|
+
async function searchInspect(runtime, globals, rawUrl) {
|
|
680
|
+
const url = parsePageUrlOrPath(rawUrl);
|
|
681
|
+
if (url._tag === 'Err')
|
|
682
|
+
return url;
|
|
683
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
684
|
+
if (resolved._tag === 'Err')
|
|
685
|
+
return resolved;
|
|
686
|
+
const response = await withSpinner(runtime, 'Loading URL Inspection', () => resolved.value.api.client.search.readUrlInspection({
|
|
687
|
+
params: { siteId: resolved.value.siteId },
|
|
688
|
+
query: { url: url.value },
|
|
477
689
|
}, { signal: runtime.requestSignal }));
|
|
478
|
-
return present(runtime, globals, response, value =>
|
|
690
|
+
return present(runtime, globals, response, value => renderUrlInspection(value.data));
|
|
479
691
|
}
|
|
480
692
|
async function researchOverview(runtime, globals) {
|
|
481
693
|
const resolved = await apiAndSite(runtime, globals);
|
|
@@ -579,6 +791,173 @@ async function researchRankings(runtime, globals, args) {
|
|
|
579
791
|
}, { signal: runtime.requestSignal }));
|
|
580
792
|
return present(runtime, globals, response, value => renderRankingsResearch(value.data));
|
|
581
793
|
}
|
|
794
|
+
const ANALYTICS_VIEWS = [
|
|
795
|
+
'performance',
|
|
796
|
+
'top-pages',
|
|
797
|
+
'source-medium',
|
|
798
|
+
'key-events',
|
|
799
|
+
'countries',
|
|
800
|
+
'devices',
|
|
801
|
+
'dimension',
|
|
802
|
+
];
|
|
803
|
+
const ANALYTICS_DIMENSIONS = [
|
|
804
|
+
'date',
|
|
805
|
+
'page',
|
|
806
|
+
'hostname',
|
|
807
|
+
'source',
|
|
808
|
+
'medium',
|
|
809
|
+
'campaign',
|
|
810
|
+
'country',
|
|
811
|
+
'region',
|
|
812
|
+
'city',
|
|
813
|
+
'deviceCategory',
|
|
814
|
+
'browser',
|
|
815
|
+
'operatingSystem',
|
|
816
|
+
];
|
|
817
|
+
async function auditChanges(runtime, globals, args) {
|
|
818
|
+
const from = optionalInteger(args.from, { name: '--from', minimum: 1 });
|
|
819
|
+
if (from._tag === 'Err')
|
|
820
|
+
return from;
|
|
821
|
+
const to = optionalInteger(args.to, { name: '--to', minimum: 1 });
|
|
822
|
+
if (to._tag === 'Err')
|
|
823
|
+
return to;
|
|
824
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
825
|
+
if (resolved._tag === 'Err')
|
|
826
|
+
return resolved;
|
|
827
|
+
const response = await withSpinner(runtime, 'Loading crawl changes', () => resolved.value.api.client.audit.changes({
|
|
828
|
+
params: { siteId: resolved.value.siteId },
|
|
829
|
+
query: { from: from.value, to: to.value },
|
|
830
|
+
}, { signal: runtime.requestSignal }));
|
|
831
|
+
return present(runtime, globals, response, value => renderAuditChanges(value.data));
|
|
832
|
+
}
|
|
833
|
+
async function auditLinkStructure(runtime, globals) {
|
|
834
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
835
|
+
if (resolved._tag === 'Err')
|
|
836
|
+
return resolved;
|
|
837
|
+
const response = await withSpinner(runtime, 'Loading link structure', () => resolved.value.api.client.audit.linkStructure({
|
|
838
|
+
params: { siteId: resolved.value.siteId },
|
|
839
|
+
}, { signal: runtime.requestSignal }));
|
|
840
|
+
return present(runtime, globals, response, value => renderLinkStructure(value.data));
|
|
841
|
+
}
|
|
842
|
+
async function sitemapsList(runtime, globals) {
|
|
843
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
844
|
+
if (resolved._tag === 'Err')
|
|
845
|
+
return resolved;
|
|
846
|
+
const response = await withSpinner(runtime, 'Loading sitemaps', () => resolved.value.api.client.sitemaps.list({
|
|
847
|
+
params: { siteId: resolved.value.siteId },
|
|
848
|
+
}, { signal: runtime.requestSignal }));
|
|
849
|
+
return present(runtime, globals, response, value => renderSitemaps(value.data));
|
|
850
|
+
}
|
|
851
|
+
async function sitemapsUrls(runtime, globals, args) {
|
|
852
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 1000, defaultValue: 500 });
|
|
853
|
+
if (limit._tag === 'Err')
|
|
854
|
+
return limit;
|
|
855
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
856
|
+
if (resolved._tag === 'Err')
|
|
857
|
+
return resolved;
|
|
858
|
+
return collect(runtime, globals, {
|
|
859
|
+
all: args.all === true,
|
|
860
|
+
message: 'Loading sitemap URLs',
|
|
861
|
+
start: { cursor: args.cursor },
|
|
862
|
+
fetch: position => resolved.value.api.client.sitemaps.listUrls({
|
|
863
|
+
params: { siteId: resolved.value.siteId },
|
|
864
|
+
query: {
|
|
865
|
+
generationId: args.generationId,
|
|
866
|
+
feedpath: args.feedpath,
|
|
867
|
+
cursor: position.cursor,
|
|
868
|
+
limit: limit.value,
|
|
869
|
+
},
|
|
870
|
+
}, { signal: runtime.requestSignal }),
|
|
871
|
+
render: value => renderSitemapUrls(value.data),
|
|
872
|
+
next: (value) => {
|
|
873
|
+
const cursor = value.data.page.nextCursor;
|
|
874
|
+
return cursor
|
|
875
|
+
? { _tag: 'More', position: { cursor }, resume: `--cursor ${cursor}` }
|
|
876
|
+
: { _tag: 'Done' };
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function sitemapsAction(runtime, globals, action, rawUrl) {
|
|
881
|
+
const sitemapUrl = parseAbsolutePageUrl(rawUrl);
|
|
882
|
+
if (sitemapUrl._tag === 'Err')
|
|
883
|
+
return sitemapUrl;
|
|
884
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
885
|
+
if (resolved._tag === 'Err')
|
|
886
|
+
return resolved;
|
|
887
|
+
const confirmed = await confirmMutation(runtime, globals, `${action === 'submit' ? 'Submit' : 'Delete'} ${sitemapUrl.value} in Search Console?`);
|
|
888
|
+
if (confirmed._tag === 'Err')
|
|
889
|
+
return confirmed;
|
|
890
|
+
if (!confirmed.value) {
|
|
891
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
892
|
+
return ok(undefined);
|
|
893
|
+
}
|
|
894
|
+
const response = await withSpinner(runtime, `Sending the sitemap ${action}`, () => resolved.value.api.client.sitemaps.createAction({
|
|
895
|
+
params: { siteId: resolved.value.siteId },
|
|
896
|
+
body: { action, sitemapUrl: sitemapUrl.value },
|
|
897
|
+
}, { signal: runtime.requestSignal }));
|
|
898
|
+
return present(runtime, globals, response, value => renderSitemapAction(value.data));
|
|
899
|
+
}
|
|
900
|
+
async function analyticsQuery(runtime, globals, args) {
|
|
901
|
+
const view = parseChoice(args.view, { name: 'view', choices: ANALYTICS_VIEWS });
|
|
902
|
+
if (view._tag === 'Err')
|
|
903
|
+
return view;
|
|
904
|
+
const compare = parseChoice(args.compare, { name: '--compare', choices: ['previous', 'year', 'none'], defaultValue: 'previous' });
|
|
905
|
+
if (compare._tag === 'Err')
|
|
906
|
+
return compare;
|
|
907
|
+
const phase = parseChoice(args.performancePhase, { name: '--phase', choices: ['current', 'prior', 'full'], defaultValue: 'full' });
|
|
908
|
+
if (phase._tag === 'Err')
|
|
909
|
+
return phase;
|
|
910
|
+
const dimension = optionalChoice(args.dimension, { name: '--dimension', choices: ANALYTICS_DIMENSIONS });
|
|
911
|
+
if (dimension._tag === 'Err')
|
|
912
|
+
return dimension;
|
|
913
|
+
if (view.value === 'dimension' && !dimension.value)
|
|
914
|
+
return fail(EXIT_CODE.invalidInput, 'The dimension view requires --dimension.');
|
|
915
|
+
const period = args.period?.trim();
|
|
916
|
+
if (period !== undefined && period.length === 0)
|
|
917
|
+
return fail(EXIT_CODE.invalidInput, '--period cannot be empty.');
|
|
918
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
919
|
+
if (resolved._tag === 'Err')
|
|
920
|
+
return resolved;
|
|
921
|
+
const response = await withSpinner(runtime, 'Loading Web Analytics', () => resolved.value.api.client.analytics.query({
|
|
922
|
+
params: { siteId: resolved.value.siteId, view: view.value },
|
|
923
|
+
query: {
|
|
924
|
+
period: period ?? '28d',
|
|
925
|
+
compare: compare.value,
|
|
926
|
+
stableData: args.stableData ?? true,
|
|
927
|
+
filters: args.filters,
|
|
928
|
+
hostScope: args.hostScope ?? true,
|
|
929
|
+
dimension: dimension.value,
|
|
930
|
+
performancePhase: phase.value,
|
|
931
|
+
comparePrior: args.comparePrior ?? true,
|
|
932
|
+
fresh: args.fresh ?? false,
|
|
933
|
+
},
|
|
934
|
+
}, { signal: runtime.requestSignal }));
|
|
935
|
+
return present(runtime, globals, response, value => renderAnalytics(view.value, value.data));
|
|
936
|
+
}
|
|
937
|
+
async function skillInstallCommand(runtime, globals, args) {
|
|
938
|
+
const agent = parseChoice(args.agent, { name: '--agent', choices: SKILL_AGENTS, defaultValue: 'claude' });
|
|
939
|
+
if (agent._tag === 'Err')
|
|
940
|
+
return agent;
|
|
941
|
+
// The state directory is `<home>/.nuxtseo`, so its parent is the home the
|
|
942
|
+
// agent directories sit beside.
|
|
943
|
+
const homeDirectory = dirname(runtime.paths.directory);
|
|
944
|
+
const installed = await installSkill({ agent: agent.value, homeDirectory, target: args.target });
|
|
945
|
+
if (installed._tag === 'Err')
|
|
946
|
+
return installed;
|
|
947
|
+
if (globals.json) {
|
|
948
|
+
writeCliResponse(runtime, {
|
|
949
|
+
_tag: 'CliSkillInstall',
|
|
950
|
+
schemaVersion: 1,
|
|
951
|
+
agent: installed.value.agent,
|
|
952
|
+
source: installed.value.source,
|
|
953
|
+
destination: installed.value.destination,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
writeOutput(runtime, `Installed the nuxtseo-cli skill for ${installed.value.agent} at ${installed.value.destination}.`);
|
|
958
|
+
}
|
|
959
|
+
return ok(undefined);
|
|
960
|
+
}
|
|
582
961
|
async function auditContentDecay(runtime, globals) {
|
|
583
962
|
const resolved = await apiAndSite(runtime, globals);
|
|
584
963
|
if (resolved._tag === 'Err')
|
|
@@ -645,11 +1024,17 @@ async function contentBriefList(runtime, globals, args) {
|
|
|
645
1024
|
const resolved = await apiAndSite(runtime, globals);
|
|
646
1025
|
if (resolved._tag === 'Err')
|
|
647
1026
|
return resolved;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1027
|
+
return collect(runtime, globals, {
|
|
1028
|
+
all: args.all === true,
|
|
1029
|
+
message: 'Loading Content Briefs',
|
|
1030
|
+
start: { offset: offset.value },
|
|
1031
|
+
fetch: position => resolved.value.api.client.content.listBriefs({
|
|
1032
|
+
params: { siteId: resolved.value.siteId },
|
|
1033
|
+
query: { status: status.value, limit: limit.value, offset: position.offset ?? 0 },
|
|
1034
|
+
}, { signal: runtime.requestSignal }),
|
|
1035
|
+
render: value => renderContentBriefs(value.data),
|
|
1036
|
+
next: value => offsetPage(value.data.page),
|
|
1037
|
+
});
|
|
653
1038
|
}
|
|
654
1039
|
async function contentBriefShow(runtime, globals, briefId) {
|
|
655
1040
|
const resolved = await apiAndSite(runtime, globals);
|
|
@@ -680,21 +1065,386 @@ async function contentBriefCreate(runtime, globals, args) {
|
|
|
680
1065
|
}, { signal: runtime.requestSignal }));
|
|
681
1066
|
return present(runtime, globals, response, value => renderContentBriefCreated(value.data));
|
|
682
1067
|
}
|
|
683
|
-
|
|
684
|
-
|
|
1068
|
+
const VITALS_FORM_FACTORS = ['phone', 'desktop', 'all'];
|
|
1069
|
+
const VITALS_METRICS = ['lcp', 'inp', 'cls'];
|
|
1070
|
+
const VITALS_FORM_FACTOR_WIRE = {
|
|
1071
|
+
phone: 'PHONE',
|
|
1072
|
+
desktop: 'DESKTOP',
|
|
1073
|
+
all: 'ALL_FORM_FACTORS',
|
|
1074
|
+
};
|
|
1075
|
+
/**
|
|
1076
|
+
* CrUX field vitals. This is a different dataset from `nuxtseo performance`,
|
|
1077
|
+
* which reads the stored Lighthouse lab overview (ADR-0129).
|
|
1078
|
+
*/
|
|
1079
|
+
async function fieldVitals(runtime, globals, args) {
|
|
1080
|
+
const formFactor = parseChoice(args.formFactor, {
|
|
1081
|
+
name: '--form-factor',
|
|
1082
|
+
choices: VITALS_FORM_FACTORS,
|
|
1083
|
+
defaultValue: 'phone',
|
|
1084
|
+
});
|
|
1085
|
+
if (formFactor._tag === 'Err')
|
|
1086
|
+
return formFactor;
|
|
1087
|
+
const url = args.url === undefined
|
|
1088
|
+
? ok(undefined)
|
|
1089
|
+
: parseAbsolutePageUrl(args.url);
|
|
1090
|
+
if (url._tag === 'Err')
|
|
1091
|
+
return url;
|
|
1092
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1093
|
+
if (resolved._tag === 'Err')
|
|
1094
|
+
return resolved;
|
|
1095
|
+
const response = await withSpinner(runtime, 'Loading field vitals', () => resolved.value.api.client.performance.vitals({
|
|
1096
|
+
params: { siteId: resolved.value.siteId },
|
|
1097
|
+
query: {
|
|
1098
|
+
url: url.value,
|
|
1099
|
+
formFactor: VITALS_FORM_FACTOR_WIRE[formFactor.value],
|
|
1100
|
+
view: args.view,
|
|
1101
|
+
},
|
|
1102
|
+
}, { signal: runtime.requestSignal }));
|
|
1103
|
+
return present(runtime, globals, response, value => renderFieldVitals(value.data));
|
|
1104
|
+
}
|
|
1105
|
+
async function fieldVitalFindings(runtime, globals, args) {
|
|
1106
|
+
const metric = args.metric === undefined
|
|
1107
|
+
? ok(undefined)
|
|
1108
|
+
: parseChoice(args.metric, { name: '--metric', choices: VITALS_METRICS, defaultValue: 'lcp' });
|
|
1109
|
+
if (metric._tag === 'Err')
|
|
1110
|
+
return metric;
|
|
1111
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 50, defaultValue: 20 });
|
|
1112
|
+
if (limit._tag === 'Err')
|
|
1113
|
+
return limit;
|
|
1114
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
1115
|
+
if (offset._tag === 'Err')
|
|
1116
|
+
return offset;
|
|
1117
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1118
|
+
if (resolved._tag === 'Err')
|
|
1119
|
+
return resolved;
|
|
1120
|
+
return collect(runtime, globals, {
|
|
1121
|
+
all: args.all === true,
|
|
1122
|
+
message: 'Loading field vital findings',
|
|
1123
|
+
start: { offset: offset.value },
|
|
1124
|
+
fetch: position => resolved.value.api.client.performance.vitalFindings({
|
|
1125
|
+
params: { siteId: resolved.value.siteId },
|
|
1126
|
+
query: {
|
|
1127
|
+
metrics: metric.value === undefined ? undefined : [metric.value],
|
|
1128
|
+
onlyFailing: publicV1BooleanFlagValue(args.onlyFailing ?? true),
|
|
1129
|
+
limit: limit.value,
|
|
1130
|
+
offset: position.offset ?? 0,
|
|
1131
|
+
},
|
|
1132
|
+
}, { signal: runtime.requestSignal }),
|
|
1133
|
+
render: value => renderFieldVitalFindings(value.data),
|
|
1134
|
+
next: value => offsetPage({
|
|
1135
|
+
offset: value.data.offset,
|
|
1136
|
+
limit: limit.value,
|
|
1137
|
+
hasMore: value.data.offset + value.data.findings.length < value.data.total,
|
|
1138
|
+
}),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async function backlinksSummary(runtime, globals) {
|
|
1142
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1143
|
+
if (resolved._tag === 'Err')
|
|
1144
|
+
return resolved;
|
|
1145
|
+
const response = await withSpinner(runtime, 'Loading Backlinks summary', () => resolved.value.api.client.backlinks.summary({
|
|
1146
|
+
params: { siteId: resolved.value.siteId },
|
|
1147
|
+
}, { signal: runtime.requestSignal }));
|
|
1148
|
+
return present(runtime, globals, response, value => renderBacklinksSummary(value.data));
|
|
1149
|
+
}
|
|
1150
|
+
async function backlinksReferringDomains(runtime, globals, args) {
|
|
1151
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 1000, defaultValue: 100 });
|
|
1152
|
+
if (limit._tag === 'Err')
|
|
1153
|
+
return limit;
|
|
1154
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1155
|
+
if (resolved._tag === 'Err')
|
|
1156
|
+
return resolved;
|
|
1157
|
+
const response = await withSpinner(runtime, 'Loading referring domains', () => resolved.value.api.client.backlinks.referringDomains({
|
|
1158
|
+
params: { siteId: resolved.value.siteId },
|
|
1159
|
+
query: { limit: limit.value },
|
|
1160
|
+
}, { signal: runtime.requestSignal }));
|
|
1161
|
+
return present(runtime, globals, response, value => renderReferringDomains(value.data));
|
|
1162
|
+
}
|
|
1163
|
+
async function backlinksAnchors(runtime, globals, args) {
|
|
1164
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 1000, defaultValue: 100 });
|
|
685
1165
|
if (limit._tag === 'Err')
|
|
686
1166
|
return limit;
|
|
687
|
-
const
|
|
1167
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1168
|
+
if (resolved._tag === 'Err')
|
|
1169
|
+
return resolved;
|
|
1170
|
+
const response = await withSpinner(runtime, 'Loading Backlink anchors', () => resolved.value.api.client.backlinks.anchors({
|
|
1171
|
+
params: { siteId: resolved.value.siteId },
|
|
1172
|
+
query: { limit: limit.value },
|
|
1173
|
+
}, { signal: runtime.requestSignal }));
|
|
1174
|
+
return present(runtime, globals, response, value => renderBacklinkAnchors(value.data));
|
|
1175
|
+
}
|
|
1176
|
+
async function backlinksHistory(runtime, globals, args) {
|
|
1177
|
+
if (args.from !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(args.from))
|
|
1178
|
+
return fail(EXIT_CODE.invalidInput, '--from must be a date in YYYY-MM-DD form.', undefined, 'invalid_cli_input');
|
|
1179
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1180
|
+
if (resolved._tag === 'Err')
|
|
1181
|
+
return resolved;
|
|
1182
|
+
const response = await withSpinner(runtime, 'Loading Backlinks history', () => resolved.value.api.client.backlinks.history({
|
|
1183
|
+
params: { siteId: resolved.value.siteId },
|
|
1184
|
+
query: { dateFrom: args.from },
|
|
1185
|
+
}, { signal: runtime.requestSignal }));
|
|
1186
|
+
return present(runtime, globals, response, value => renderBacklinksHistory(value.data));
|
|
1187
|
+
}
|
|
1188
|
+
async function siteStatus(runtime, globals) {
|
|
1189
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1190
|
+
if (resolved._tag === 'Err')
|
|
1191
|
+
return resolved;
|
|
1192
|
+
const response = await withSpinner(runtime, 'Loading Site status', () => resolved.value.api.client.sites.status({
|
|
1193
|
+
params: { siteId: resolved.value.siteId },
|
|
1194
|
+
}, { signal: runtime.requestSignal }));
|
|
1195
|
+
return present(runtime, globals, response, value => renderSiteStatus(value.data));
|
|
1196
|
+
}
|
|
1197
|
+
async function actionDismiss(runtime, globals, args) {
|
|
1198
|
+
if (!args.actionId)
|
|
1199
|
+
return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
|
|
1200
|
+
if (!args.artifactVersion)
|
|
1201
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
|
|
1202
|
+
const confirmed = await confirmMutation(runtime, globals, `Dismiss action ${args.actionId}? The page stays removed.`);
|
|
1203
|
+
if (confirmed._tag === 'Err')
|
|
1204
|
+
return confirmed;
|
|
1205
|
+
if (!confirmed.value) {
|
|
1206
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
1207
|
+
return ok(undefined);
|
|
1208
|
+
}
|
|
1209
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1210
|
+
if (resolved._tag === 'Err')
|
|
1211
|
+
return resolved;
|
|
1212
|
+
const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
|
|
1213
|
+
params: { siteId: resolved.value.siteId, actionId: args.actionId },
|
|
1214
|
+
body: { artifactVersion: args.artifactVersion },
|
|
1215
|
+
}, { signal: runtime.requestSignal }));
|
|
1216
|
+
return present(runtime, globals, response, value => renderActionDismissal(value.data));
|
|
1217
|
+
}
|
|
1218
|
+
async function pageIssues(runtime, globals, args) {
|
|
1219
|
+
if (args.actionId && (args.source || args.issue))
|
|
1220
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --action-id, or pass --source and --issue, not both.', undefined, 'invalid_cli_input');
|
|
1221
|
+
if (!args.actionId && !(args.source && args.issue))
|
|
1222
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --action-id, or pass both --source and --issue.', undefined, 'invalid_cli_input');
|
|
1223
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
|
|
1224
|
+
if (limit._tag === 'Err')
|
|
1225
|
+
return limit;
|
|
1226
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
688
1227
|
if (offset._tag === 'Err')
|
|
689
1228
|
return offset;
|
|
690
1229
|
const resolved = await apiAndSite(runtime, globals);
|
|
691
1230
|
if (resolved._tag === 'Err')
|
|
692
1231
|
return resolved;
|
|
693
|
-
|
|
1232
|
+
return collect(runtime, globals, {
|
|
1233
|
+
all: args.all === true,
|
|
1234
|
+
message: 'Loading Page Issues',
|
|
1235
|
+
start: { offset: offset.value },
|
|
1236
|
+
fetch: position => resolved.value.api.client.pages.issues({
|
|
1237
|
+
params: { siteId: resolved.value.siteId },
|
|
1238
|
+
query: {
|
|
1239
|
+
actionId: args.actionId,
|
|
1240
|
+
source: args.source,
|
|
1241
|
+
issue: args.issue,
|
|
1242
|
+
url: args.url,
|
|
1243
|
+
pathPrefix: args.pathPrefix,
|
|
1244
|
+
includeResolved: publicV1BooleanFlagValue(args.includeResolved === true),
|
|
1245
|
+
limit: limit.value,
|
|
1246
|
+
offset: position.offset ?? 0,
|
|
1247
|
+
},
|
|
1248
|
+
}, { signal: runtime.requestSignal }),
|
|
1249
|
+
render: value => renderPageIssues(value.data),
|
|
1250
|
+
next: value => offsetPage({
|
|
1251
|
+
offset: value.data.offset,
|
|
1252
|
+
limit: limit.value,
|
|
1253
|
+
hasMore: value.data.offset + value.data.issues.length < value.data.total,
|
|
1254
|
+
}),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
async function performanceScans(runtime, globals, args) {
|
|
1258
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
|
|
1259
|
+
if (limit._tag === 'Err')
|
|
1260
|
+
return limit;
|
|
1261
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1262
|
+
if (resolved._tag === 'Err')
|
|
1263
|
+
return resolved;
|
|
1264
|
+
const response = await withSpinner(runtime, 'Loading Scans', () => resolved.value.api.client.performance.scans({
|
|
1265
|
+
params: { siteId: resolved.value.siteId },
|
|
1266
|
+
query: { limit: limit.value },
|
|
1267
|
+
}, { signal: runtime.requestSignal }));
|
|
1268
|
+
return present(runtime, globals, response, value => renderScans(value.data));
|
|
1269
|
+
}
|
|
1270
|
+
async function performanceScanDetail(runtime, globals, scanId) {
|
|
1271
|
+
if (!scanId)
|
|
1272
|
+
return fail(EXIT_CODE.invalidInput, 'Pass the Scan id. List them with `nuxtseo scans list`.', undefined, 'invalid_cli_input');
|
|
1273
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1274
|
+
if (resolved._tag === 'Err')
|
|
1275
|
+
return resolved;
|
|
1276
|
+
const response = await withSpinner(runtime, 'Loading Scan', () => resolved.value.api.client.performance.scanDetail({
|
|
1277
|
+
params: { siteId: resolved.value.siteId, scanId },
|
|
1278
|
+
}, { signal: runtime.requestSignal }));
|
|
1279
|
+
return present(runtime, globals, response, value => renderScanDetail(value.data));
|
|
1280
|
+
}
|
|
1281
|
+
async function monitoredPages(runtime, globals) {
|
|
1282
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1283
|
+
if (resolved._tag === 'Err')
|
|
1284
|
+
return resolved;
|
|
1285
|
+
const response = await withSpinner(runtime, 'Loading monitored Pages', () => resolved.value.api.client.performance.monitoredPages({
|
|
1286
|
+
params: { siteId: resolved.value.siteId },
|
|
1287
|
+
}, { signal: runtime.requestSignal }));
|
|
1288
|
+
return present(runtime, globals, response, value => renderMonitoredPages(value.data));
|
|
1289
|
+
}
|
|
1290
|
+
async function annotationsList(runtime, globals) {
|
|
1291
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1292
|
+
if (resolved._tag === 'Err')
|
|
1293
|
+
return resolved;
|
|
1294
|
+
const response = await withSpinner(runtime, 'Loading annotations', () => resolved.value.api.client.timeline.listAnnotations({
|
|
1295
|
+
params: { siteId: resolved.value.siteId },
|
|
1296
|
+
}, { signal: runtime.requestSignal }));
|
|
1297
|
+
return present(runtime, globals, response, value => renderAnnotations(value.data));
|
|
1298
|
+
}
|
|
1299
|
+
const ANNOTATION_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
1300
|
+
/**
|
|
1301
|
+
* An optional annotation field on the wire. `note` and `url` are nullable, and
|
|
1302
|
+
* the operation clears one when it is sent as null, so the CLI needs a way to
|
|
1303
|
+
* SAY null. An empty flag value is that way: `--note ""` and `--url ""` clear.
|
|
1304
|
+
* Without it there was no clearing at all, and `--url ""` failed SDK body
|
|
1305
|
+
* validation with exit 2 and no request, which is the opposite of the
|
|
1306
|
+
* operation's own documented behaviour.
|
|
1307
|
+
*/
|
|
1308
|
+
function annotationField(value) {
|
|
1309
|
+
if (value === undefined)
|
|
1310
|
+
return undefined;
|
|
1311
|
+
// Only an exactly empty value clears. Whitespace is content the server
|
|
1312
|
+
// trims, so treating it as a clear would guess at intent.
|
|
1313
|
+
return value === '' ? null : value;
|
|
1314
|
+
}
|
|
1315
|
+
async function annotationCreate(runtime, globals, args) {
|
|
1316
|
+
if (!args.date || !ANNOTATION_DAY.test(args.date))
|
|
1317
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --date as YYYY-MM-DD.', undefined, 'invalid_cli_input');
|
|
1318
|
+
if (!args.title)
|
|
1319
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --title, the marker label.', undefined, 'invalid_cli_input');
|
|
1320
|
+
const confirmed = await confirmMutation(runtime, globals, `Place an annotation on ${args.date}: ${args.title}?`);
|
|
1321
|
+
if (confirmed._tag === 'Err')
|
|
1322
|
+
return confirmed;
|
|
1323
|
+
if (!confirmed.value) {
|
|
1324
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
1325
|
+
return ok(undefined);
|
|
1326
|
+
}
|
|
1327
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1328
|
+
if (resolved._tag === 'Err')
|
|
1329
|
+
return resolved;
|
|
1330
|
+
const response = await withSpinner(runtime, 'Creating annotation', () => resolved.value.api.client.timeline.createAnnotation({
|
|
1331
|
+
params: { siteId: resolved.value.siteId },
|
|
1332
|
+
body: { date: args.date, title: args.title, note: annotationField(args.note) ?? null, url: annotationField(args.url) ?? null },
|
|
1333
|
+
}, { signal: runtime.requestSignal }));
|
|
1334
|
+
return present(runtime, globals, response, value => renderAnnotation(value.data));
|
|
1335
|
+
}
|
|
1336
|
+
async function annotationUpdate(runtime, globals, args) {
|
|
1337
|
+
if (!args.annotationId)
|
|
1338
|
+
return fail(EXIT_CODE.invalidInput, 'Pass the annotation id. List them with `nuxtseo annotations list`.', undefined, 'invalid_cli_input');
|
|
1339
|
+
if (args.date !== undefined && !ANNOTATION_DAY.test(args.date))
|
|
1340
|
+
return fail(EXIT_CODE.invalidInput, '--date must be YYYY-MM-DD.', undefined, 'invalid_cli_input');
|
|
1341
|
+
if (args.date === undefined && args.title === undefined && args.note === undefined && args.url === undefined)
|
|
1342
|
+
return fail(EXIT_CODE.invalidInput, 'Pass at least one of --date, --title, --note or --url.', undefined, 'invalid_cli_input');
|
|
1343
|
+
if (args.title !== undefined && args.title.trim() === '')
|
|
1344
|
+
return fail(EXIT_CODE.invalidInput, '--title cannot be empty. Every annotation needs a label.', undefined, 'invalid_cli_input');
|
|
1345
|
+
const confirmed = await confirmMutation(runtime, globals, `Change annotation ${args.annotationId}?`);
|
|
1346
|
+
if (confirmed._tag === 'Err')
|
|
1347
|
+
return confirmed;
|
|
1348
|
+
if (!confirmed.value) {
|
|
1349
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
1350
|
+
return ok(undefined);
|
|
1351
|
+
}
|
|
1352
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1353
|
+
if (resolved._tag === 'Err')
|
|
1354
|
+
return resolved;
|
|
1355
|
+
const response = await withSpinner(runtime, 'Updating annotation', () => resolved.value.api.client.timeline.updateAnnotation({
|
|
1356
|
+
params: { siteId: resolved.value.siteId, annotationId: args.annotationId },
|
|
1357
|
+
body: { date: args.date, title: args.title, note: annotationField(args.note), url: annotationField(args.url) },
|
|
1358
|
+
}, { signal: runtime.requestSignal }));
|
|
1359
|
+
return present(runtime, globals, response, value => renderAnnotation(value.data));
|
|
1360
|
+
}
|
|
1361
|
+
async function annotationDelete(runtime, globals, args) {
|
|
1362
|
+
if (!args.annotationId)
|
|
1363
|
+
return fail(EXIT_CODE.invalidInput, 'Pass the annotation id. List them with `nuxtseo annotations list`.', undefined, 'invalid_cli_input');
|
|
1364
|
+
const confirmed = await confirmMutation(runtime, globals, `Delete annotation ${args.annotationId}? This cannot be undone.`);
|
|
1365
|
+
if (confirmed._tag === 'Err')
|
|
1366
|
+
return confirmed;
|
|
1367
|
+
if (!confirmed.value) {
|
|
1368
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
1369
|
+
return ok(undefined);
|
|
1370
|
+
}
|
|
1371
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1372
|
+
if (resolved._tag === 'Err')
|
|
1373
|
+
return resolved;
|
|
1374
|
+
const response = await withSpinner(runtime, 'Deleting annotation', () => resolved.value.api.client.timeline.deleteAnnotation({
|
|
1375
|
+
params: { siteId: resolved.value.siteId, annotationId: args.annotationId },
|
|
1376
|
+
}, { signal: runtime.requestSignal }));
|
|
1377
|
+
return present(runtime, globals, response, value => renderAnnotationDeleted(value.data));
|
|
1378
|
+
}
|
|
1379
|
+
async function searchCohorts(runtime, globals, args) {
|
|
1380
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 50, defaultValue: 12 });
|
|
1381
|
+
if (limit._tag === 'Err')
|
|
1382
|
+
return limit;
|
|
1383
|
+
const minPages = parseInteger(args.minPages, { name: '--min-pages', minimum: 1, maximum: 500, defaultValue: 5 });
|
|
1384
|
+
if (minPages._tag === 'Err')
|
|
1385
|
+
return minPages;
|
|
1386
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1387
|
+
if (resolved._tag === 'Err')
|
|
1388
|
+
return resolved;
|
|
1389
|
+
const response = await withSpinner(runtime, 'Loading route-family indexing', () => resolved.value.api.client.search.readIndexingCohorts({
|
|
1390
|
+
params: { siteId: resolved.value.siteId },
|
|
1391
|
+
query: { limit: limit.value, minSectionPages: minPages.value },
|
|
1392
|
+
}, { signal: runtime.requestSignal }));
|
|
1393
|
+
return present(runtime, globals, response, value => renderIndexCohorts(value.data));
|
|
1394
|
+
}
|
|
1395
|
+
async function researchDomainTraffic(runtime, globals, domain) {
|
|
1396
|
+
if (!domain)
|
|
1397
|
+
return fail(EXIT_CODE.invalidInput, 'Pass the domain, for example nuxtseo.com.', undefined, 'invalid_cli_input');
|
|
1398
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1399
|
+
if (resolved._tag === 'Err')
|
|
1400
|
+
return resolved;
|
|
1401
|
+
const response = await withSpinner(runtime, 'Estimating domain traffic', () => resolved.value.api.client.research.domainTraffic({
|
|
1402
|
+
params: { siteId: resolved.value.siteId },
|
|
1403
|
+
query: { domain },
|
|
1404
|
+
}, { signal: runtime.requestSignal }));
|
|
1405
|
+
return present(runtime, globals, response, value => renderDomainTraffic(value.data));
|
|
1406
|
+
}
|
|
1407
|
+
/** Domains arrive as one comma-separated positional, so the count is explicit and quoting is not needed. */
|
|
1408
|
+
async function researchDomainAvailability(runtime, globals, input) {
|
|
1409
|
+
const domains = (input ?? '').split(',').map(value => value.trim()).filter(Boolean);
|
|
1410
|
+
if (domains.length === 0)
|
|
1411
|
+
return fail(EXIT_CODE.invalidInput, 'Pass at least one domain.', undefined, 'invalid_cli_input');
|
|
1412
|
+
if (domains.length > 10)
|
|
1413
|
+
return fail(EXIT_CODE.invalidInput, 'Pass at most 10 domains, comma separated.', undefined, 'invalid_cli_input');
|
|
1414
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1415
|
+
if (resolved._tag === 'Err')
|
|
1416
|
+
return resolved;
|
|
1417
|
+
const response = await withSpinner(runtime, 'Checking domain availability', () => resolved.value.api.client.research.domainAvailability({
|
|
694
1418
|
params: { siteId: resolved.value.siteId },
|
|
695
|
-
query: {
|
|
1419
|
+
query: { domains },
|
|
696
1420
|
}, { signal: runtime.requestSignal }));
|
|
697
|
-
return present(runtime, globals, response, value =>
|
|
1421
|
+
return present(runtime, globals, response, value => renderDomainAvailability(value.data));
|
|
1422
|
+
}
|
|
1423
|
+
async function backlinksRecoverable(runtime, globals, args) {
|
|
1424
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
|
|
1425
|
+
if (limit._tag === 'Err')
|
|
1426
|
+
return limit;
|
|
1427
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
1428
|
+
if (offset._tag === 'Err')
|
|
1429
|
+
return offset;
|
|
1430
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1431
|
+
if (resolved._tag === 'Err')
|
|
1432
|
+
return resolved;
|
|
1433
|
+
return collect(runtime, globals, {
|
|
1434
|
+
all: args.all === true,
|
|
1435
|
+
message: 'Loading recoverable Backlinks',
|
|
1436
|
+
start: { offset: offset.value },
|
|
1437
|
+
fetch: position => resolved.value.api.client.backlinks.recoverable({
|
|
1438
|
+
params: { siteId: resolved.value.siteId },
|
|
1439
|
+
query: { limit: limit.value, offset: position.offset ?? 0 },
|
|
1440
|
+
}, { signal: runtime.requestSignal }),
|
|
1441
|
+
render: value => renderRecoverableBacklinks(value.data),
|
|
1442
|
+
next: value => offsetPage({
|
|
1443
|
+
offset: value.data.offset,
|
|
1444
|
+
limit: limit.value,
|
|
1445
|
+
hasMore: value.data.offset + value.data.items.length < value.data.total,
|
|
1446
|
+
}),
|
|
1447
|
+
});
|
|
698
1448
|
}
|
|
699
1449
|
async function mentionsList(runtime, globals, args) {
|
|
700
1450
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
|
|
@@ -705,7 +1455,7 @@ async function mentionsList(runtime, globals, args) {
|
|
|
705
1455
|
return resolved;
|
|
706
1456
|
const response = await withSpinner(runtime, 'Loading Mentions', () => resolved.value.api.client.mentions.list({
|
|
707
1457
|
params: { siteId: resolved.value.siteId },
|
|
708
|
-
query: { limit: limit.value, includeFiltered: args.includeFiltered
|
|
1458
|
+
query: { limit: limit.value, includeFiltered: publicV1BooleanFlagValue(Boolean(args.includeFiltered)) },
|
|
709
1459
|
}, { signal: runtime.requestSignal }));
|
|
710
1460
|
return present(runtime, globals, response, value => renderMentions(value.data));
|
|
711
1461
|
}
|
|
@@ -718,8 +1468,20 @@ function capture(execution, task, positionalArguments, expectedPositionals) {
|
|
|
718
1468
|
execution.result = await task();
|
|
719
1469
|
};
|
|
720
1470
|
}
|
|
721
|
-
|
|
722
|
-
|
|
1471
|
+
/**
|
|
1472
|
+
* `options` is the machine-readable list of legal values for a positional that
|
|
1473
|
+
* takes one of a fixed set. `--help --json` publishes it, so the skill-command
|
|
1474
|
+
* gate can check a documented `search indexing summary` against what the binary
|
|
1475
|
+
* really accepts instead of taking the tail on trust.
|
|
1476
|
+
*/
|
|
1477
|
+
function positional(name, description, options) {
|
|
1478
|
+
return {
|
|
1479
|
+
type: 'positional',
|
|
1480
|
+
required: true,
|
|
1481
|
+
valueHint: name,
|
|
1482
|
+
description,
|
|
1483
|
+
...(options ? { options: [...options] } : {}),
|
|
1484
|
+
};
|
|
723
1485
|
}
|
|
724
1486
|
export function createRootCommand(runtime, globals, execution) {
|
|
725
1487
|
const sites = defineCommand({
|
|
@@ -744,8 +1506,13 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
744
1506
|
args: {
|
|
745
1507
|
limit: { type: 'string', description: 'Maximum actions, 1 to 25' },
|
|
746
1508
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1509
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
747
1510
|
},
|
|
748
|
-
run: ({ args }) => capture(execution, () => actionsList(runtime, globals,
|
|
1511
|
+
run: ({ args }) => capture(execution, () => actionsList(runtime, globals, {
|
|
1512
|
+
limit: args.limit,
|
|
1513
|
+
offset: args.offset,
|
|
1514
|
+
all: args.all,
|
|
1515
|
+
}), args._, 0)(),
|
|
749
1516
|
}),
|
|
750
1517
|
show: defineCommand({
|
|
751
1518
|
meta: { name: 'show', description: 'Show one action and its evidence' },
|
|
@@ -767,6 +1534,17 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
767
1534
|
args: { actionId: positional('action-id', 'Action ID') },
|
|
768
1535
|
run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
|
|
769
1536
|
}),
|
|
1537
|
+
dismiss: defineCommand({
|
|
1538
|
+
meta: { name: 'dismiss', description: 'Dismiss a broken-page action so the page stays removed' },
|
|
1539
|
+
args: {
|
|
1540
|
+
'actionId': positional('action-id', 'Action ID'),
|
|
1541
|
+
'artifact-version': { type: 'string', description: 'The artifactVersion returned with this action' },
|
|
1542
|
+
},
|
|
1543
|
+
run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
|
|
1544
|
+
actionId: args.actionId,
|
|
1545
|
+
artifactVersion: args['artifact-version'],
|
|
1546
|
+
}), args._, 1)(),
|
|
1547
|
+
}),
|
|
770
1548
|
},
|
|
771
1549
|
});
|
|
772
1550
|
const page = defineCommand({
|
|
@@ -779,14 +1557,41 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
779
1557
|
'include-resolved': { type: 'boolean', description: 'Include resolved observations' },
|
|
780
1558
|
'limit': { type: 'string', description: 'Maximum observations, 1 to 200' },
|
|
781
1559
|
'offset': { type: 'string', description: 'Pagination offset' },
|
|
1560
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
782
1561
|
},
|
|
783
1562
|
run: ({ args }) => capture(execution, () => pageInspect(runtime, globals, {
|
|
784
1563
|
url: args.url,
|
|
785
1564
|
includeResolved: args['include-resolved'],
|
|
786
1565
|
limit: args.limit,
|
|
787
1566
|
offset: args.offset,
|
|
1567
|
+
all: args.all,
|
|
788
1568
|
}), args._, 1)(),
|
|
789
1569
|
}),
|
|
1570
|
+
issues: defineCommand({
|
|
1571
|
+
meta: { name: 'issues', description: 'Browse raw Page Issue rows by action, or by source and issue' },
|
|
1572
|
+
args: {
|
|
1573
|
+
'action-id': { type: 'string', description: 'Action whose stamped observation keys to browse' },
|
|
1574
|
+
'source': { type: 'string', description: 'Page Issue source family, for example crawl' },
|
|
1575
|
+
'issue': { type: 'string', description: 'Source-scoped issue type, for example broken-internal-links' },
|
|
1576
|
+
'url': { type: 'string', description: 'Exact affected URL' },
|
|
1577
|
+
'path-prefix': { type: 'string', description: 'Only routes whose path starts with this prefix' },
|
|
1578
|
+
'include-resolved': { type: 'boolean', description: 'Include closed rows and why they closed' },
|
|
1579
|
+
'limit': { type: 'string', description: 'Maximum rows, 1 to 200' },
|
|
1580
|
+
'offset': { type: 'string', description: 'Pagination offset' },
|
|
1581
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
1582
|
+
},
|
|
1583
|
+
run: ({ args }) => capture(execution, () => pageIssues(runtime, globals, {
|
|
1584
|
+
actionId: args['action-id'],
|
|
1585
|
+
source: args.source,
|
|
1586
|
+
issue: args.issue,
|
|
1587
|
+
url: args.url,
|
|
1588
|
+
pathPrefix: args['path-prefix'],
|
|
1589
|
+
includeResolved: args['include-resolved'],
|
|
1590
|
+
limit: args.limit,
|
|
1591
|
+
offset: args.offset,
|
|
1592
|
+
all: args.all,
|
|
1593
|
+
}), args._, 0)(),
|
|
1594
|
+
}),
|
|
790
1595
|
scan: defineCommand({
|
|
791
1596
|
meta: { name: 'scan', description: 'Start mobile and desktop Page scans' },
|
|
792
1597
|
args: { url: positional('url', 'Absolute Page URL') },
|
|
@@ -795,15 +1600,164 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
795
1600
|
},
|
|
796
1601
|
});
|
|
797
1602
|
const backlinks = defineCommand({
|
|
798
|
-
meta: { name: 'backlinks', description: 'Read
|
|
1603
|
+
meta: { name: 'backlinks', description: 'Read inbound link evidence for the Site' },
|
|
799
1604
|
subCommands: {
|
|
800
|
-
|
|
1605
|
+
'summary': defineCommand({
|
|
1606
|
+
meta: { name: 'summary', description: 'Read whole-domain inbound link totals' },
|
|
1607
|
+
run: ({ args }) => capture(execution, () => backlinksSummary(runtime, globals), args._, 0)(),
|
|
1608
|
+
}),
|
|
1609
|
+
'referring-domains': defineCommand({
|
|
1610
|
+
meta: { name: 'referring-domains', description: 'List the domains that link to the Site' },
|
|
1611
|
+
args: {
|
|
1612
|
+
limit: { type: 'string', description: 'Maximum domains, 1 to 1000' },
|
|
1613
|
+
},
|
|
1614
|
+
run: ({ args }) => capture(execution, () => backlinksReferringDomains(runtime, globals, {
|
|
1615
|
+
limit: args.limit,
|
|
1616
|
+
}), args._, 0)(),
|
|
1617
|
+
}),
|
|
1618
|
+
'anchors': defineCommand({
|
|
1619
|
+
meta: { name: 'anchors', description: 'List the anchor text of inbound links' },
|
|
1620
|
+
args: {
|
|
1621
|
+
limit: { type: 'string', description: 'Maximum anchors, 1 to 1000' },
|
|
1622
|
+
},
|
|
1623
|
+
run: ({ args }) => capture(execution, () => backlinksAnchors(runtime, globals, {
|
|
1624
|
+
limit: args.limit,
|
|
1625
|
+
}), args._, 0)(),
|
|
1626
|
+
}),
|
|
1627
|
+
'history': defineCommand({
|
|
1628
|
+
meta: { name: 'history', description: 'Read the monthly inbound link series' },
|
|
1629
|
+
args: {
|
|
1630
|
+
from: { type: 'string', description: 'Start date, YYYY-MM-DD' },
|
|
1631
|
+
},
|
|
1632
|
+
run: ({ args }) => capture(execution, () => backlinksHistory(runtime, globals, {
|
|
1633
|
+
from: args.from,
|
|
1634
|
+
}), args._, 0)(),
|
|
1635
|
+
}),
|
|
1636
|
+
'recoverable': defineCommand({
|
|
801
1637
|
meta: { name: 'recoverable', description: 'List inbound links whose destination is broken' },
|
|
802
1638
|
args: {
|
|
803
1639
|
limit: { type: 'string', description: 'Maximum links, 1 to 200' },
|
|
804
1640
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1641
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
1642
|
+
},
|
|
1643
|
+
run: ({ args }) => capture(execution, () => backlinksRecoverable(runtime, globals, {
|
|
1644
|
+
limit: args.limit,
|
|
1645
|
+
offset: args.offset,
|
|
1646
|
+
all: args.all,
|
|
1647
|
+
}), args._, 0)(),
|
|
1648
|
+
}),
|
|
1649
|
+
},
|
|
1650
|
+
});
|
|
1651
|
+
const vitals = defineCommand({
|
|
1652
|
+
meta: { name: 'vitals', description: 'Read field Core Web Vitals from real-user data' },
|
|
1653
|
+
subCommands: {
|
|
1654
|
+
summary: defineCommand({
|
|
1655
|
+
meta: { name: 'summary', description: 'Read the latest field p75 and its change' },
|
|
1656
|
+
args: {
|
|
1657
|
+
'url': { type: 'string', description: 'One absolute Page URL; omit for the Site origin' },
|
|
1658
|
+
'form-factor': { type: 'enum', options: [...VITALS_FORM_FACTORS], description: 'Device class' },
|
|
1659
|
+
},
|
|
1660
|
+
run: ({ args }) => capture(execution, () => fieldVitals(runtime, globals, {
|
|
1661
|
+
view: 'summary',
|
|
1662
|
+
url: args.url,
|
|
1663
|
+
formFactor: args['form-factor'],
|
|
1664
|
+
}), args._, 0)(),
|
|
1665
|
+
}),
|
|
1666
|
+
trend: defineCommand({
|
|
1667
|
+
meta: { name: 'trend', description: 'Read the 25-week field p75 series' },
|
|
1668
|
+
args: {
|
|
1669
|
+
'url': { type: 'string', description: 'One absolute Page URL; omit for the Site origin' },
|
|
1670
|
+
'form-factor': { type: 'enum', options: [...VITALS_FORM_FACTORS], description: 'Device class' },
|
|
1671
|
+
},
|
|
1672
|
+
run: ({ args }) => capture(execution, () => fieldVitals(runtime, globals, {
|
|
1673
|
+
view: 'trend',
|
|
1674
|
+
url: args.url,
|
|
1675
|
+
formFactor: args['form-factor'],
|
|
1676
|
+
}), args._, 0)(),
|
|
1677
|
+
}),
|
|
1678
|
+
findings: defineCommand({
|
|
1679
|
+
meta: { name: 'findings', description: 'List failing field vitals with the element and a fix prompt' },
|
|
1680
|
+
args: {
|
|
1681
|
+
'metric': { type: 'enum', options: [...VITALS_METRICS], description: 'Restrict to one vital' },
|
|
1682
|
+
'only-failing': { type: 'boolean', default: true, description: 'Keep only failing elements; use --no-only-failing for every element' },
|
|
1683
|
+
'limit': { type: 'string', description: 'Maximum findings, 1 to 50' },
|
|
1684
|
+
'offset': { type: 'string', description: 'Pagination offset' },
|
|
1685
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
805
1686
|
},
|
|
806
|
-
run: ({ args }) => capture(execution, () =>
|
|
1687
|
+
run: ({ args }) => capture(execution, () => fieldVitalFindings(runtime, globals, {
|
|
1688
|
+
metric: args.metric,
|
|
1689
|
+
onlyFailing: args['only-failing'],
|
|
1690
|
+
limit: args.limit,
|
|
1691
|
+
offset: args.offset,
|
|
1692
|
+
all: args.all,
|
|
1693
|
+
}), args._, 0)(),
|
|
1694
|
+
}),
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
const scans = defineCommand({
|
|
1698
|
+
meta: { name: 'scans', description: 'Read retained Lighthouse Scans and the Pages behind them' },
|
|
1699
|
+
subCommands: {
|
|
1700
|
+
list: defineCommand({
|
|
1701
|
+
meta: { name: 'list', description: 'List retained Scans, newest first' },
|
|
1702
|
+
args: { limit: { type: 'string', description: 'Maximum Scans, 1 to 100' } },
|
|
1703
|
+
run: ({ args }) => capture(execution, () => performanceScans(runtime, globals, { limit: args.limit }), args._, 0)(),
|
|
1704
|
+
}),
|
|
1705
|
+
show: defineCommand({
|
|
1706
|
+
meta: { name: 'show', description: 'Show one Scan with the failing checks behind its scores' },
|
|
1707
|
+
args: { scanId: positional('scan-id', 'Scan ID') },
|
|
1708
|
+
run: ({ args }) => capture(execution, () => performanceScanDetail(runtime, globals, args.scanId), args._, 1)(),
|
|
1709
|
+
}),
|
|
1710
|
+
pages: defineCommand({
|
|
1711
|
+
meta: { name: 'pages', description: 'List the Pages this Site scans on a schedule' },
|
|
1712
|
+
run: ({ args }) => capture(execution, () => monitoredPages(runtime, globals), args._, 0)(),
|
|
1713
|
+
}),
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
const annotations = defineCommand({
|
|
1717
|
+
meta: { name: 'annotations', description: 'Mark what changed on the charts for this Site' },
|
|
1718
|
+
subCommands: {
|
|
1719
|
+
list: defineCommand({
|
|
1720
|
+
meta: { name: 'list', description: 'List the chart annotations for this Site' },
|
|
1721
|
+
run: ({ args }) => capture(execution, () => annotationsList(runtime, globals), args._, 0)(),
|
|
1722
|
+
}),
|
|
1723
|
+
create: defineCommand({
|
|
1724
|
+
meta: { name: 'create', description: 'Place one dated marker on the charts for this Site' },
|
|
1725
|
+
args: {
|
|
1726
|
+
date: { type: 'string', description: 'Day the change happened, YYYY-MM-DD' },
|
|
1727
|
+
title: { type: 'string', description: 'Marker label' },
|
|
1728
|
+
note: { type: 'string', description: 'Optional hover description' },
|
|
1729
|
+
url: { type: 'string', description: 'Optional http(s) link' },
|
|
1730
|
+
},
|
|
1731
|
+
run: ({ args }) => capture(execution, () => annotationCreate(runtime, globals, {
|
|
1732
|
+
date: args.date,
|
|
1733
|
+
title: args.title,
|
|
1734
|
+
note: args.note,
|
|
1735
|
+
url: args.url,
|
|
1736
|
+
}), args._, 0)(),
|
|
1737
|
+
}),
|
|
1738
|
+
update: defineCommand({
|
|
1739
|
+
meta: { name: 'update', description: 'Change one annotation' },
|
|
1740
|
+
args: {
|
|
1741
|
+
annotationId: positional('annotation-id', 'Annotation ID'),
|
|
1742
|
+
date: { type: 'string', description: 'Day the change happened, YYYY-MM-DD' },
|
|
1743
|
+
title: { type: 'string', description: 'Marker label' },
|
|
1744
|
+
note: { type: 'string', description: 'Hover description. Pass an empty string to clear it' },
|
|
1745
|
+
url: { type: 'string', description: 'http(s) link. Pass an empty string to clear it' },
|
|
1746
|
+
},
|
|
1747
|
+
run: ({ args }) => capture(execution, () => annotationUpdate(runtime, globals, {
|
|
1748
|
+
annotationId: args.annotationId,
|
|
1749
|
+
date: args.date,
|
|
1750
|
+
title: args.title,
|
|
1751
|
+
note: args.note,
|
|
1752
|
+
url: args.url,
|
|
1753
|
+
}), args._, 1)(),
|
|
1754
|
+
}),
|
|
1755
|
+
delete: defineCommand({
|
|
1756
|
+
meta: { name: 'delete', description: 'Remove one annotation permanently' },
|
|
1757
|
+
args: { annotationId: positional('annotation-id', 'Annotation ID') },
|
|
1758
|
+
run: ({ args }) => capture(execution, () => annotationDelete(runtime, globals, {
|
|
1759
|
+
annotationId: args.annotationId,
|
|
1760
|
+
}), args._, 1)(),
|
|
807
1761
|
}),
|
|
808
1762
|
},
|
|
809
1763
|
});
|
|
@@ -826,11 +1780,11 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
826
1780
|
const research = defineCommand({
|
|
827
1781
|
meta: { name: 'research', description: 'Read stored and live search research' },
|
|
828
1782
|
subCommands: {
|
|
829
|
-
overview: defineCommand({
|
|
1783
|
+
'overview': defineCommand({
|
|
830
1784
|
meta: { name: 'overview', description: 'Read stored Site metrics and competitor research' },
|
|
831
1785
|
run: ({ args }) => capture(execution, () => researchOverview(runtime, globals), args._, 0)(),
|
|
832
1786
|
}),
|
|
833
|
-
keywords: defineCommand({
|
|
1787
|
+
'keywords': defineCommand({
|
|
834
1788
|
meta: { name: 'keywords', description: 'Find live keyword ideas with volume and difficulty' },
|
|
835
1789
|
args: {
|
|
836
1790
|
'topic': positional('topic', 'Keyword or topic'),
|
|
@@ -857,7 +1811,17 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
857
1811
|
verbose: args.verbose,
|
|
858
1812
|
}), args._, 1)(),
|
|
859
1813
|
}),
|
|
860
|
-
|
|
1814
|
+
'domain-traffic': defineCommand({
|
|
1815
|
+
meta: { name: 'domain-traffic', description: 'Estimate the organic search traffic of any domain' },
|
|
1816
|
+
args: { domain: positional('domain', 'Domain, for example nuxtseo.com') },
|
|
1817
|
+
run: ({ args }) => capture(execution, () => researchDomainTraffic(runtime, globals, args.domain), args._, 1)(),
|
|
1818
|
+
}),
|
|
1819
|
+
'domain-availability': defineCommand({
|
|
1820
|
+
meta: { name: 'domain-availability', description: 'Check whether domains are registered, up to 10' },
|
|
1821
|
+
args: { domains: positional('domains', 'Comma-separated domains, up to 10') },
|
|
1822
|
+
run: ({ args }) => capture(execution, () => researchDomainAvailability(runtime, globals, args.domains), args._, 1)(),
|
|
1823
|
+
}),
|
|
1824
|
+
'serp': defineCommand({
|
|
861
1825
|
meta: { name: 'serp', description: 'Analyze one live Google SERP' },
|
|
862
1826
|
args: {
|
|
863
1827
|
'keyword': positional('keyword', 'Keyword'),
|
|
@@ -870,7 +1834,7 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
870
1834
|
locationCode: args['location-code'],
|
|
871
1835
|
}), args._, 1)(),
|
|
872
1836
|
}),
|
|
873
|
-
rankings: defineCommand({
|
|
1837
|
+
'rankings': defineCommand({
|
|
874
1838
|
meta: { name: 'rankings', description: 'Research live keyword rankings for a domain' },
|
|
875
1839
|
args: {
|
|
876
1840
|
'domain': positional('domain', 'Domain without a protocol'),
|
|
@@ -906,6 +1870,21 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
906
1870
|
meta: { name: 'link-opportunities', description: 'List internal link opportunities' },
|
|
907
1871
|
run: ({ args }) => capture(execution, () => auditLinkOpportunities(runtime, globals), args._, 0)(),
|
|
908
1872
|
}),
|
|
1873
|
+
'link-structure': defineCommand({
|
|
1874
|
+
meta: { name: 'link-structure', description: 'Read template links, dead ends, and generic anchors' },
|
|
1875
|
+
run: ({ args }) => capture(execution, () => auditLinkStructure(runtime, globals), args._, 0)(),
|
|
1876
|
+
}),
|
|
1877
|
+
'changes': defineCommand({
|
|
1878
|
+
meta: { name: 'changes', description: 'Compare the latest crawl with its baseline' },
|
|
1879
|
+
args: {
|
|
1880
|
+
from: { type: 'string', description: 'Baseline crawl generation' },
|
|
1881
|
+
to: { type: 'string', description: 'Comparison crawl generation' },
|
|
1882
|
+
},
|
|
1883
|
+
run: ({ args }) => capture(execution, () => auditChanges(runtime, globals, {
|
|
1884
|
+
from: args.from,
|
|
1885
|
+
to: args.to,
|
|
1886
|
+
}), args._, 0)(),
|
|
1887
|
+
}),
|
|
909
1888
|
},
|
|
910
1889
|
});
|
|
911
1890
|
const content = defineCommand({
|
|
@@ -924,11 +1903,13 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
924
1903
|
},
|
|
925
1904
|
limit: { type: 'string', description: 'Maximum Content Briefs, 1 to 100' },
|
|
926
1905
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1906
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
927
1907
|
},
|
|
928
1908
|
run: ({ args }) => capture(execution, () => contentBriefList(runtime, globals, {
|
|
929
1909
|
status: args.status,
|
|
930
1910
|
limit: args.limit,
|
|
931
1911
|
offset: args.offset,
|
|
1912
|
+
all: args.all,
|
|
932
1913
|
}), args._, 0)(),
|
|
933
1914
|
}),
|
|
934
1915
|
show: defineCommand({
|
|
@@ -954,39 +1935,66 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
954
1935
|
const search = defineCommand({
|
|
955
1936
|
meta: { name: 'search', description: 'Read Search Console state' },
|
|
956
1937
|
subCommands: {
|
|
957
|
-
status: defineCommand({
|
|
1938
|
+
'status': defineCommand({
|
|
958
1939
|
meta: { name: 'status', description: 'Read the stored Search Console connection' },
|
|
959
1940
|
run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
|
|
960
1941
|
}),
|
|
961
|
-
analytics: defineCommand({
|
|
962
|
-
meta: { name: 'analytics', description: '
|
|
1942
|
+
'analytics': defineCommand({
|
|
1943
|
+
meta: { name: 'analytics', description: 'Read a Search Console breakdown, trend, detail, or analysis' },
|
|
963
1944
|
args: {
|
|
964
|
-
'view': positional('
|
|
1945
|
+
'view': positional('view', SEARCH_ANALYTICS_VIEWS.join(', '), SEARCH_ANALYTICS_VIEWS),
|
|
965
1946
|
'period': { type: 'enum', options: ['7d', '28d', '3m', '6m', '12m'], description: 'Search Console period' },
|
|
966
1947
|
'limit': { type: 'string', description: 'Maximum rows, 1 to 100' },
|
|
967
1948
|
'page': { type: 'string', description: 'Result page number' },
|
|
1949
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; row views only' },
|
|
968
1950
|
'search': { type: 'string', description: 'Filter rows by text' },
|
|
969
1951
|
'sort': { type: 'enum', options: ['clicks', 'impressions', 'ctr', 'position'], description: 'Sort field' },
|
|
970
1952
|
'sort-dir': { type: 'enum', options: ['asc', 'desc'], description: 'Sort direction' },
|
|
1953
|
+
'filter': { type: 'enum', options: ['top-level', 'new', 'lost', 'improving', 'declining'], description: 'Row set filter' },
|
|
1954
|
+
'page-url': { type: 'string', description: 'Page URL or path; required for page-detail' },
|
|
1955
|
+
'keyword': { type: 'string', description: 'Keyword; required for keyword-detail' },
|
|
1956
|
+
'preset': { type: 'enum', options: [...SEARCH_ANALYSIS_PRESETS], description: 'Analysis preset; required for analysis' },
|
|
1957
|
+
'brand-terms': { type: 'string', description: 'Comma separated brand terms; required for brand analysis' },
|
|
1958
|
+
'min-clicks': { type: 'string', description: 'Minimum clicks' },
|
|
1959
|
+
'max-clicks': { type: 'string', description: 'Maximum clicks' },
|
|
1960
|
+
'min-impressions': { type: 'string', description: 'Minimum impressions' },
|
|
1961
|
+
'max-impressions': { type: 'string', description: 'Maximum impressions' },
|
|
1962
|
+
'min-position': { type: 'string', description: 'Minimum position, 1 to 100' },
|
|
1963
|
+
'max-position': { type: 'string', description: 'Maximum position, 1 to 100' },
|
|
1964
|
+
'max-ctr': { type: 'string', description: 'Maximum click-through rate, 0 to 1' },
|
|
971
1965
|
},
|
|
972
1966
|
run: ({ args }) => capture(execution, () => searchAnalytics(runtime, globals, {
|
|
973
1967
|
view: args.view,
|
|
974
1968
|
period: args.period,
|
|
975
1969
|
limit: args.limit,
|
|
976
1970
|
page: args.page,
|
|
1971
|
+
all: args.all,
|
|
977
1972
|
search: args.search,
|
|
978
1973
|
sort: args.sort,
|
|
979
1974
|
sortDir: args['sort-dir'],
|
|
1975
|
+
filter: args.filter,
|
|
1976
|
+
pageUrl: args['page-url'],
|
|
1977
|
+
keyword: args.keyword,
|
|
1978
|
+
preset: args.preset,
|
|
1979
|
+
brandTerms: args['brand-terms'],
|
|
1980
|
+
minClicks: args['min-clicks'],
|
|
1981
|
+
maxClicks: args['max-clicks'],
|
|
1982
|
+
minImpressions: args['min-impressions'],
|
|
1983
|
+
maxImpressions: args['max-impressions'],
|
|
1984
|
+
minPosition: args['min-position'],
|
|
1985
|
+
maxPosition: args['max-position'],
|
|
1986
|
+
maxCtr: args['max-ctr'],
|
|
980
1987
|
}), args._, 1)(),
|
|
981
1988
|
}),
|
|
982
|
-
indexing: defineCommand({
|
|
1989
|
+
'indexing': defineCommand({
|
|
983
1990
|
meta: { name: 'indexing', description: 'Read retained Search Console indexing coverage' },
|
|
984
1991
|
args: {
|
|
985
|
-
view: positional('summary-or-urls', 'summary or urls'),
|
|
1992
|
+
view: positional('summary-or-urls', 'summary or urls', SEARCH_INDEXING_VIEWS),
|
|
986
1993
|
issue: { type: 'string', description: 'Filter URLs by issue type' },
|
|
987
1994
|
status: { type: 'enum', options: ['indexed', 'not_indexed', 'pending'], description: 'Filter URLs by indexing status' },
|
|
988
1995
|
limit: { type: 'string', description: 'Maximum URLs, 1 to 500' },
|
|
989
1996
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1997
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; the urls view only' },
|
|
990
1998
|
},
|
|
991
1999
|
run: ({ args }) => capture(execution, () => searchIndexing(runtime, globals, {
|
|
992
2000
|
view: args.view,
|
|
@@ -994,8 +2002,90 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
994
2002
|
status: args.status,
|
|
995
2003
|
limit: args.limit,
|
|
996
2004
|
offset: args.offset,
|
|
2005
|
+
all: args.all,
|
|
997
2006
|
}), args._, 1)(),
|
|
998
2007
|
}),
|
|
2008
|
+
'cohorts': defineCommand({
|
|
2009
|
+
meta: { name: 'cohorts', description: 'Read the not-indexed rate per route family' },
|
|
2010
|
+
args: {
|
|
2011
|
+
'limit': { type: 'string', description: 'Maximum route families, 1 to 50' },
|
|
2012
|
+
'min-pages': { type: 'string', description: 'Ignore families under this many crawled pages, 1 to 500' },
|
|
2013
|
+
},
|
|
2014
|
+
run: ({ args }) => capture(execution, () => searchCohorts(runtime, globals, {
|
|
2015
|
+
limit: args.limit,
|
|
2016
|
+
minPages: args['min-pages'],
|
|
2017
|
+
}), args._, 0)(),
|
|
2018
|
+
}),
|
|
2019
|
+
'index-history': defineCommand({
|
|
2020
|
+
meta: { name: 'index-history', description: 'Read retained indexing changes and their candidate causes' },
|
|
2021
|
+
args: {
|
|
2022
|
+
url: { type: 'string', description: 'One Page URL or path; omit for the whole Site' },
|
|
2023
|
+
field: { type: 'enum', options: [...INDEXING_TRANSITION_FIELDS], description: 'Indexing field to track' },
|
|
2024
|
+
days: { type: 'string', description: 'Window in days, 1 to 730' },
|
|
2025
|
+
},
|
|
2026
|
+
run: ({ args }) => capture(execution, () => searchIndexHistory(runtime, globals, {
|
|
2027
|
+
url: args.url,
|
|
2028
|
+
field: args.field,
|
|
2029
|
+
days: args.days,
|
|
2030
|
+
}), args._, 0)(),
|
|
2031
|
+
}),
|
|
2032
|
+
'inspect': defineCommand({
|
|
2033
|
+
meta: { name: 'inspect', description: 'Read the latest URL Inspection result for one URL' },
|
|
2034
|
+
args: { url: positional('url', 'Page URL or path') },
|
|
2035
|
+
run: ({ args }) => capture(execution, () => searchInspect(runtime, globals, args.url), args._, 1)(),
|
|
2036
|
+
}),
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
const sitemaps = defineCommand({
|
|
2040
|
+
meta: { name: 'sitemaps', description: 'Read and manage Search Console sitemaps' },
|
|
2041
|
+
subCommands: {
|
|
2042
|
+
list: defineCommand({
|
|
2043
|
+
meta: { name: 'list', description: 'List the sitemaps Search Console knows' },
|
|
2044
|
+
run: ({ args }) => capture(execution, () => sitemapsList(runtime, globals), args._, 0)(),
|
|
2045
|
+
}),
|
|
2046
|
+
urls: defineCommand({
|
|
2047
|
+
meta: { name: 'urls', description: 'List sitemap URL membership for one generation' },
|
|
2048
|
+
args: {
|
|
2049
|
+
'generation-id': { type: 'string', description: 'Pin the read to one upstream generation' },
|
|
2050
|
+
'feedpath': { type: 'string', description: 'Restrict to one sitemap path' },
|
|
2051
|
+
'cursor': { type: 'string', description: 'Opaque cursor from a previous response' },
|
|
2052
|
+
'limit': { type: 'string', description: 'Maximum URLs, 1 to 1000' },
|
|
2053
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
2054
|
+
},
|
|
2055
|
+
run: ({ args }) => capture(execution, () => sitemapsUrls(runtime, globals, {
|
|
2056
|
+
generationId: args['generation-id'],
|
|
2057
|
+
feedpath: args.feedpath,
|
|
2058
|
+
cursor: args.cursor,
|
|
2059
|
+
limit: args.limit,
|
|
2060
|
+
all: args.all,
|
|
2061
|
+
}), args._, 0)(),
|
|
2062
|
+
}),
|
|
2063
|
+
submit: defineCommand({
|
|
2064
|
+
meta: { name: 'submit', description: 'Submit one sitemap to Search Console' },
|
|
2065
|
+
args: { url: positional('sitemap-url', 'Absolute sitemap URL') },
|
|
2066
|
+
run: ({ args }) => capture(execution, () => sitemapsAction(runtime, globals, 'submit', args.url), args._, 1)(),
|
|
2067
|
+
}),
|
|
2068
|
+
delete: defineCommand({
|
|
2069
|
+
meta: { name: 'delete', description: 'Delete one sitemap from Search Console' },
|
|
2070
|
+
args: { url: positional('sitemap-url', 'Absolute sitemap URL') },
|
|
2071
|
+
run: ({ args }) => capture(execution, () => sitemapsAction(runtime, globals, 'delete', args.url), args._, 1)(),
|
|
2072
|
+
}),
|
|
2073
|
+
},
|
|
2074
|
+
});
|
|
2075
|
+
const skill = defineCommand({
|
|
2076
|
+
meta: { name: 'skill', description: 'Install the packaged coding agent skill' },
|
|
2077
|
+
subCommands: {
|
|
2078
|
+
install: defineCommand({
|
|
2079
|
+
meta: { name: 'install', description: 'Copy the nuxtseo-cli skill into an agent skill directory' },
|
|
2080
|
+
args: {
|
|
2081
|
+
agent: { type: 'enum', options: [...SKILL_AGENTS], description: 'Agent skill directory; default claude' },
|
|
2082
|
+
target: { type: 'string', description: 'Write into this directory instead' },
|
|
2083
|
+
},
|
|
2084
|
+
run: ({ args }) => capture(execution, () => skillInstallCommand(runtime, globals, {
|
|
2085
|
+
agent: args.agent,
|
|
2086
|
+
target: args.target,
|
|
2087
|
+
}), args._, 0)(),
|
|
2088
|
+
}),
|
|
999
2089
|
},
|
|
1000
2090
|
});
|
|
1001
2091
|
const timeline = defineCommand({
|
|
@@ -1086,8 +2176,44 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1086
2176
|
meta: { name: 'performance', description: 'Read the stored Site performance overview' },
|
|
1087
2177
|
run: ({ args }) => capture(execution, () => performance(runtime, globals), args._, 0)(),
|
|
1088
2178
|
}),
|
|
2179
|
+
annotations,
|
|
1089
2180
|
research,
|
|
2181
|
+
scans,
|
|
1090
2182
|
search,
|
|
2183
|
+
sitemaps,
|
|
2184
|
+
status: defineCommand({
|
|
2185
|
+
meta: { name: 'status', description: 'Read the Site assessment and the one thing to do next' },
|
|
2186
|
+
run: ({ args }) => capture(execution, () => siteStatus(runtime, globals), args._, 0)(),
|
|
2187
|
+
}),
|
|
2188
|
+
vitals,
|
|
2189
|
+
analytics: defineCommand({
|
|
2190
|
+
meta: { name: 'analytics', description: 'Read Web Analytics for the selected Site' },
|
|
2191
|
+
args: {
|
|
2192
|
+
'view': positional('view', ANALYTICS_VIEWS.join(', ')),
|
|
2193
|
+
'period': { type: 'string', description: 'Analytics period, for example 28d' },
|
|
2194
|
+
'compare': { type: 'enum', options: ['previous', 'year', 'none'], description: 'Comparison window' },
|
|
2195
|
+
'dimension': { type: 'enum', options: [...ANALYTICS_DIMENSIONS], description: 'Breakdown dimension; required for the dimension view' },
|
|
2196
|
+
'filters': { type: 'string', description: 'JSON array of dimension filters' },
|
|
2197
|
+
'phase': { type: 'enum', options: ['current', 'prior', 'full'], description: 'Performance phase' },
|
|
2198
|
+
'stable-data': { type: 'boolean', default: true, description: 'Exclude unstable recent days; use --no-stable-data to include them' },
|
|
2199
|
+
'host-scope': { type: 'boolean', default: true, description: 'Restrict to the Site host; use --no-host-scope to include every host' },
|
|
2200
|
+
'compare-prior': { type: 'boolean', default: true, description: 'Include the prior window; use --no-compare-prior to skip it' },
|
|
2201
|
+
'fresh': { type: 'boolean', description: 'Bypass the server cache' },
|
|
2202
|
+
},
|
|
2203
|
+
run: ({ args }) => capture(execution, () => analyticsQuery(runtime, globals, {
|
|
2204
|
+
view: args.view,
|
|
2205
|
+
period: args.period,
|
|
2206
|
+
compare: args.compare,
|
|
2207
|
+
dimension: args.dimension,
|
|
2208
|
+
filters: args.filters,
|
|
2209
|
+
performancePhase: args.phase,
|
|
2210
|
+
stableData: args['stable-data'],
|
|
2211
|
+
hostScope: args['host-scope'],
|
|
2212
|
+
comparePrior: args['compare-prior'],
|
|
2213
|
+
fresh: args.fresh,
|
|
2214
|
+
}), args._, 1)(),
|
|
2215
|
+
}),
|
|
2216
|
+
skill,
|
|
1091
2217
|
timeline,
|
|
1092
2218
|
},
|
|
1093
2219
|
});
|