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