@nuxtseo/cli 0.1.3 → 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 +120 -36
- package/dist/api.js +7 -1
- package/dist/cli.js +62 -11
- package/dist/command-contract.d.ts +2 -1
- package/dist/command-contract.js +14 -2
- package/dist/commands.d.ts +19 -0
- package/dist/commands.js +1263 -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 +44 -8
- package/dist/render.js +648 -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 +17 -5
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, 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
|
-
view: view.value,
|
|
472
|
-
issue: args.issue,
|
|
473
|
-
status: status.value,
|
|
474
|
-
limit: limit.value,
|
|
475
|
-
offset: offset.value,
|
|
476
|
-
},
|
|
675
|
+
query: { url: url.value, field: field.value, days: days.value },
|
|
477
676
|
}, { signal: runtime.requestSignal }));
|
|
478
|
-
return present(runtime, globals, response, value =>
|
|
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 },
|
|
689
|
+
}, { signal: runtime.requestSignal }));
|
|
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')
|
|
@@ -606,6 +985,27 @@ async function auditLinkOpportunities(runtime, globals) {
|
|
|
606
985
|
}, { signal: runtime.requestSignal }));
|
|
607
986
|
return present(runtime, globals, response, value => renderLinkOpportunities(value.data));
|
|
608
987
|
}
|
|
988
|
+
async function timelineEntries(runtime, globals, args) {
|
|
989
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
|
|
990
|
+
if (limit._tag === 'Err')
|
|
991
|
+
return limit;
|
|
992
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
993
|
+
if (resolved._tag === 'Err')
|
|
994
|
+
return resolved;
|
|
995
|
+
const response = await withSpinner(runtime, 'Loading Timeline', () => resolved.value.api.client.timeline.entries({
|
|
996
|
+
params: { siteId: resolved.value.siteId },
|
|
997
|
+
query: {
|
|
998
|
+
kinds: args.kind,
|
|
999
|
+
feature: args.feature,
|
|
1000
|
+
severity: args.severity,
|
|
1001
|
+
since: args.since,
|
|
1002
|
+
limit: limit.value,
|
|
1003
|
+
cursor: args.cursor,
|
|
1004
|
+
includeClosed: args.includeClosed,
|
|
1005
|
+
},
|
|
1006
|
+
}, { signal: runtime.requestSignal }));
|
|
1007
|
+
return present(runtime, globals, response, value => renderTimeline(value.data));
|
|
1008
|
+
}
|
|
609
1009
|
async function contentBriefList(runtime, globals, args) {
|
|
610
1010
|
const status = args.status === undefined
|
|
611
1011
|
? ok(undefined)
|
|
@@ -624,11 +1024,17 @@ async function contentBriefList(runtime, globals, args) {
|
|
|
624
1024
|
const resolved = await apiAndSite(runtime, globals);
|
|
625
1025
|
if (resolved._tag === 'Err')
|
|
626
1026
|
return resolved;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
+
});
|
|
632
1038
|
}
|
|
633
1039
|
async function contentBriefShow(runtime, globals, briefId) {
|
|
634
1040
|
const resolved = await apiAndSite(runtime, globals);
|
|
@@ -659,21 +1065,386 @@ async function contentBriefCreate(runtime, globals, args) {
|
|
|
659
1065
|
}, { signal: runtime.requestSignal }));
|
|
660
1066
|
return present(runtime, globals, response, value => renderContentBriefCreated(value.data));
|
|
661
1067
|
}
|
|
662
|
-
|
|
663
|
-
|
|
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 });
|
|
664
1112
|
if (limit._tag === 'Err')
|
|
665
1113
|
return limit;
|
|
666
|
-
const offset = parseInteger(
|
|
1114
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
667
1115
|
if (offset._tag === 'Err')
|
|
668
1116
|
return offset;
|
|
669
1117
|
const resolved = await apiAndSite(runtime, globals);
|
|
670
1118
|
if (resolved._tag === 'Err')
|
|
671
1119
|
return resolved;
|
|
672
|
-
|
|
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 });
|
|
1165
|
+
if (limit._tag === 'Err')
|
|
1166
|
+
return limit;
|
|
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 });
|
|
1227
|
+
if (offset._tag === 'Err')
|
|
1228
|
+
return offset;
|
|
1229
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1230
|
+
if (resolved._tag === 'Err')
|
|
1231
|
+
return resolved;
|
|
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({
|
|
673
1390
|
params: { siteId: resolved.value.siteId },
|
|
674
|
-
query: { limit: limit.value,
|
|
1391
|
+
query: { limit: limit.value, minSectionPages: minPages.value },
|
|
675
1392
|
}, { signal: runtime.requestSignal }));
|
|
676
|
-
return present(runtime, globals, response, value =>
|
|
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({
|
|
1418
|
+
params: { siteId: resolved.value.siteId },
|
|
1419
|
+
query: { domains },
|
|
1420
|
+
}, { signal: runtime.requestSignal }));
|
|
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
|
+
});
|
|
677
1448
|
}
|
|
678
1449
|
async function mentionsList(runtime, globals, args) {
|
|
679
1450
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
|
|
@@ -684,7 +1455,7 @@ async function mentionsList(runtime, globals, args) {
|
|
|
684
1455
|
return resolved;
|
|
685
1456
|
const response = await withSpinner(runtime, 'Loading Mentions', () => resolved.value.api.client.mentions.list({
|
|
686
1457
|
params: { siteId: resolved.value.siteId },
|
|
687
|
-
query: { limit: limit.value, includeFiltered: args.includeFiltered
|
|
1458
|
+
query: { limit: limit.value, includeFiltered: publicV1BooleanFlagValue(Boolean(args.includeFiltered)) },
|
|
688
1459
|
}, { signal: runtime.requestSignal }));
|
|
689
1460
|
return present(runtime, globals, response, value => renderMentions(value.data));
|
|
690
1461
|
}
|
|
@@ -697,8 +1468,20 @@ function capture(execution, task, positionalArguments, expectedPositionals) {
|
|
|
697
1468
|
execution.result = await task();
|
|
698
1469
|
};
|
|
699
1470
|
}
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
};
|
|
702
1485
|
}
|
|
703
1486
|
export function createRootCommand(runtime, globals, execution) {
|
|
704
1487
|
const sites = defineCommand({
|
|
@@ -723,8 +1506,13 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
723
1506
|
args: {
|
|
724
1507
|
limit: { type: 'string', description: 'Maximum actions, 1 to 25' },
|
|
725
1508
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1509
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
726
1510
|
},
|
|
727
|
-
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)(),
|
|
728
1516
|
}),
|
|
729
1517
|
show: defineCommand({
|
|
730
1518
|
meta: { name: 'show', description: 'Show one action and its evidence' },
|
|
@@ -746,6 +1534,17 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
746
1534
|
args: { actionId: positional('action-id', 'Action ID') },
|
|
747
1535
|
run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
|
|
748
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
|
+
}),
|
|
749
1548
|
},
|
|
750
1549
|
});
|
|
751
1550
|
const page = defineCommand({
|
|
@@ -758,14 +1557,41 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
758
1557
|
'include-resolved': { type: 'boolean', description: 'Include resolved observations' },
|
|
759
1558
|
'limit': { type: 'string', description: 'Maximum observations, 1 to 200' },
|
|
760
1559
|
'offset': { type: 'string', description: 'Pagination offset' },
|
|
1560
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
761
1561
|
},
|
|
762
1562
|
run: ({ args }) => capture(execution, () => pageInspect(runtime, globals, {
|
|
763
1563
|
url: args.url,
|
|
764
1564
|
includeResolved: args['include-resolved'],
|
|
765
1565
|
limit: args.limit,
|
|
766
1566
|
offset: args.offset,
|
|
1567
|
+
all: args.all,
|
|
767
1568
|
}), args._, 1)(),
|
|
768
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
|
+
}),
|
|
769
1595
|
scan: defineCommand({
|
|
770
1596
|
meta: { name: 'scan', description: 'Start mobile and desktop Page scans' },
|
|
771
1597
|
args: { url: positional('url', 'Absolute Page URL') },
|
|
@@ -774,15 +1600,164 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
774
1600
|
},
|
|
775
1601
|
});
|
|
776
1602
|
const backlinks = defineCommand({
|
|
777
|
-
meta: { name: 'backlinks', description: 'Read
|
|
1603
|
+
meta: { name: 'backlinks', description: 'Read inbound link evidence for the Site' },
|
|
778
1604
|
subCommands: {
|
|
779
|
-
|
|
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({
|
|
780
1637
|
meta: { name: 'recoverable', description: 'List inbound links whose destination is broken' },
|
|
781
1638
|
args: {
|
|
782
1639
|
limit: { type: 'string', description: 'Maximum links, 1 to 200' },
|
|
783
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' },
|
|
1686
|
+
},
|
|
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' },
|
|
784
1730
|
},
|
|
785
|
-
run: ({ args }) => capture(execution, () =>
|
|
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)(),
|
|
786
1761
|
}),
|
|
787
1762
|
},
|
|
788
1763
|
});
|
|
@@ -805,11 +1780,11 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
805
1780
|
const research = defineCommand({
|
|
806
1781
|
meta: { name: 'research', description: 'Read stored and live search research' },
|
|
807
1782
|
subCommands: {
|
|
808
|
-
overview: defineCommand({
|
|
1783
|
+
'overview': defineCommand({
|
|
809
1784
|
meta: { name: 'overview', description: 'Read stored Site metrics and competitor research' },
|
|
810
1785
|
run: ({ args }) => capture(execution, () => researchOverview(runtime, globals), args._, 0)(),
|
|
811
1786
|
}),
|
|
812
|
-
keywords: defineCommand({
|
|
1787
|
+
'keywords': defineCommand({
|
|
813
1788
|
meta: { name: 'keywords', description: 'Find live keyword ideas with volume and difficulty' },
|
|
814
1789
|
args: {
|
|
815
1790
|
'topic': positional('topic', 'Keyword or topic'),
|
|
@@ -836,7 +1811,17 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
836
1811
|
verbose: args.verbose,
|
|
837
1812
|
}), args._, 1)(),
|
|
838
1813
|
}),
|
|
839
|
-
|
|
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({
|
|
840
1825
|
meta: { name: 'serp', description: 'Analyze one live Google SERP' },
|
|
841
1826
|
args: {
|
|
842
1827
|
'keyword': positional('keyword', 'Keyword'),
|
|
@@ -849,7 +1834,7 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
849
1834
|
locationCode: args['location-code'],
|
|
850
1835
|
}), args._, 1)(),
|
|
851
1836
|
}),
|
|
852
|
-
rankings: defineCommand({
|
|
1837
|
+
'rankings': defineCommand({
|
|
853
1838
|
meta: { name: 'rankings', description: 'Research live keyword rankings for a domain' },
|
|
854
1839
|
args: {
|
|
855
1840
|
'domain': positional('domain', 'Domain without a protocol'),
|
|
@@ -885,6 +1870,21 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
885
1870
|
meta: { name: 'link-opportunities', description: 'List internal link opportunities' },
|
|
886
1871
|
run: ({ args }) => capture(execution, () => auditLinkOpportunities(runtime, globals), args._, 0)(),
|
|
887
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
|
+
}),
|
|
888
1888
|
},
|
|
889
1889
|
});
|
|
890
1890
|
const content = defineCommand({
|
|
@@ -903,11 +1903,13 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
903
1903
|
},
|
|
904
1904
|
limit: { type: 'string', description: 'Maximum Content Briefs, 1 to 100' },
|
|
905
1905
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1906
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; one envelope per page' },
|
|
906
1907
|
},
|
|
907
1908
|
run: ({ args }) => capture(execution, () => contentBriefList(runtime, globals, {
|
|
908
1909
|
status: args.status,
|
|
909
1910
|
limit: args.limit,
|
|
910
1911
|
offset: args.offset,
|
|
1912
|
+
all: args.all,
|
|
911
1913
|
}), args._, 0)(),
|
|
912
1914
|
}),
|
|
913
1915
|
show: defineCommand({
|
|
@@ -933,39 +1935,66 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
933
1935
|
const search = defineCommand({
|
|
934
1936
|
meta: { name: 'search', description: 'Read Search Console state' },
|
|
935
1937
|
subCommands: {
|
|
936
|
-
status: defineCommand({
|
|
1938
|
+
'status': defineCommand({
|
|
937
1939
|
meta: { name: 'status', description: 'Read the stored Search Console connection' },
|
|
938
1940
|
run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
|
|
939
1941
|
}),
|
|
940
|
-
analytics: defineCommand({
|
|
941
|
-
meta: { name: 'analytics', description: '
|
|
1942
|
+
'analytics': defineCommand({
|
|
1943
|
+
meta: { name: 'analytics', description: 'Read a Search Console breakdown, trend, detail, or analysis' },
|
|
942
1944
|
args: {
|
|
943
|
-
'view': positional('
|
|
1945
|
+
'view': positional('view', SEARCH_ANALYTICS_VIEWS.join(', '), SEARCH_ANALYTICS_VIEWS),
|
|
944
1946
|
'period': { type: 'enum', options: ['7d', '28d', '3m', '6m', '12m'], description: 'Search Console period' },
|
|
945
1947
|
'limit': { type: 'string', description: 'Maximum rows, 1 to 100' },
|
|
946
1948
|
'page': { type: 'string', description: 'Result page number' },
|
|
1949
|
+
'all': { type: 'boolean', description: 'Repeat until every page is written; row views only' },
|
|
947
1950
|
'search': { type: 'string', description: 'Filter rows by text' },
|
|
948
1951
|
'sort': { type: 'enum', options: ['clicks', 'impressions', 'ctr', 'position'], description: 'Sort field' },
|
|
949
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' },
|
|
950
1965
|
},
|
|
951
1966
|
run: ({ args }) => capture(execution, () => searchAnalytics(runtime, globals, {
|
|
952
1967
|
view: args.view,
|
|
953
1968
|
period: args.period,
|
|
954
1969
|
limit: args.limit,
|
|
955
1970
|
page: args.page,
|
|
1971
|
+
all: args.all,
|
|
956
1972
|
search: args.search,
|
|
957
1973
|
sort: args.sort,
|
|
958
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'],
|
|
959
1987
|
}), args._, 1)(),
|
|
960
1988
|
}),
|
|
961
|
-
indexing: defineCommand({
|
|
1989
|
+
'indexing': defineCommand({
|
|
962
1990
|
meta: { name: 'indexing', description: 'Read retained Search Console indexing coverage' },
|
|
963
1991
|
args: {
|
|
964
|
-
view: positional('summary-or-urls', 'summary or urls'),
|
|
1992
|
+
view: positional('summary-or-urls', 'summary or urls', SEARCH_INDEXING_VIEWS),
|
|
965
1993
|
issue: { type: 'string', description: 'Filter URLs by issue type' },
|
|
966
1994
|
status: { type: 'enum', options: ['indexed', 'not_indexed', 'pending'], description: 'Filter URLs by indexing status' },
|
|
967
1995
|
limit: { type: 'string', description: 'Maximum URLs, 1 to 500' },
|
|
968
1996
|
offset: { type: 'string', description: 'Pagination offset' },
|
|
1997
|
+
all: { type: 'boolean', description: 'Repeat until every page is written; the urls view only' },
|
|
969
1998
|
},
|
|
970
1999
|
run: ({ args }) => capture(execution, () => searchIndexing(runtime, globals, {
|
|
971
2000
|
view: args.view,
|
|
@@ -973,8 +2002,116 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
973
2002
|
status: args.status,
|
|
974
2003
|
limit: args.limit,
|
|
975
2004
|
offset: args.offset,
|
|
2005
|
+
all: args.all,
|
|
976
2006
|
}), args._, 1)(),
|
|
977
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
|
+
}),
|
|
2089
|
+
},
|
|
2090
|
+
});
|
|
2091
|
+
const timeline = defineCommand({
|
|
2092
|
+
meta: { name: 'timeline', description: 'Read the Timeline — what changed on the Site' },
|
|
2093
|
+
subCommands: {
|
|
2094
|
+
list: defineCommand({
|
|
2095
|
+
meta: { name: 'list', description: 'List Timeline Entries' },
|
|
2096
|
+
args: {
|
|
2097
|
+
'kind': { type: 'string', description: 'Comma-separated Timeline Entry kinds, e.g. traffic.dropped,indexing.changed' },
|
|
2098
|
+
'feature': { type: 'string', description: 'Filter to the kinds registered under this feature' },
|
|
2099
|
+
'severity': { type: 'string', description: 'Comma-separated severities: info, notable, high' },
|
|
2100
|
+
'since': { type: 'string', description: 'Only entries at or after this ISO timestamp' },
|
|
2101
|
+
'limit': { type: 'string', description: 'Maximum Timeline Entries, 1 to 100' },
|
|
2102
|
+
'cursor': { type: 'string', description: 'Opaque pagination cursor from a previous page' },
|
|
2103
|
+
'include-closed': { type: 'boolean', description: 'Include closed episodes' },
|
|
2104
|
+
},
|
|
2105
|
+
run: ({ args }) => capture(execution, () => timelineEntries(runtime, globals, {
|
|
2106
|
+
kind: args.kind,
|
|
2107
|
+
feature: args.feature,
|
|
2108
|
+
severity: args.severity,
|
|
2109
|
+
since: args.since,
|
|
2110
|
+
limit: args.limit,
|
|
2111
|
+
cursor: args.cursor,
|
|
2112
|
+
includeClosed: args['include-closed'],
|
|
2113
|
+
}), args._, 0)(),
|
|
2114
|
+
}),
|
|
978
2115
|
},
|
|
979
2116
|
});
|
|
980
2117
|
return defineCommand({
|
|
@@ -1039,8 +2176,45 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1039
2176
|
meta: { name: 'performance', description: 'Read the stored Site performance overview' },
|
|
1040
2177
|
run: ({ args }) => capture(execution, () => performance(runtime, globals), args._, 0)(),
|
|
1041
2178
|
}),
|
|
2179
|
+
annotations,
|
|
1042
2180
|
research,
|
|
2181
|
+
scans,
|
|
1043
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,
|
|
2217
|
+
timeline,
|
|
1044
2218
|
},
|
|
1045
2219
|
});
|
|
1046
2220
|
}
|