@nuxtseo/cli 0.1.0 → 0.1.2
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 +39 -7
- package/dist/cli.js +205 -31
- package/dist/commands.js +377 -4
- package/dist/failures.js +18 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/parse.d.ts +5 -0
- package/dist/parse.js +7 -0
- package/dist/render.d.ts +15 -1
- package/dist/render.js +134 -9
- package/dist/runtime.d.ts +1 -0
- package/dist/state/paths.d.ts +2 -0
- package/dist/state/paths.js +2 -0
- package/dist/update-check.d.ts +25 -0
- package/dist/update-check.js +107 -0
- package/package.json +5 -8
- package/skills/nuxtseo-cli/SKILL.md +41 -31
- package/skills/nuxtseo-cli/references/protocol.md +17 -1
package/dist/commands.js
CHANGED
|
@@ -4,8 +4,8 @@ import { createApiClient, fromStateError, loadApiContext } from './api.js';
|
|
|
4
4
|
import { openBrowser } from './browser.js';
|
|
5
5
|
import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
|
|
6
6
|
import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
|
|
7
|
-
import { parseAbsolutePageUrl, parseInteger } from './parse.js';
|
|
8
|
-
import { renderAction, renderActionResolution, renderActions, renderMentions, renderPageInspection, renderPageScan, renderPerformance, renderRecoverableBacklinks, renderSearchStatus, renderSites, renderUsage, } from './render.js';
|
|
7
|
+
import { parseAbsolutePageUrl, parseChoice, parseInteger } from './parse.js';
|
|
8
|
+
import { renderAction, renderActionResolution, renderActions, renderContentBrief, renderContentBriefCreated, renderContentBriefs, renderContentDecay, renderDuplicateClusters, renderKeywordResearch, renderLinkOpportunities, renderMentions, renderPageInspection, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderResearchOverview, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSites, renderUsage, } from './render.js';
|
|
9
9
|
import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
|
|
10
10
|
import { resolveSite } from './site.js';
|
|
11
11
|
import { clearCredential, getCredentialStatus, readConfig, resolveApiUrl, saveCredential, updateConfig, } from './state/index.js';
|
|
@@ -23,7 +23,10 @@ async function withSpinner(runtime, message, task) {
|
|
|
23
23
|
const spinner = runtime.interactive
|
|
24
24
|
? prompts.spinner({ output: runtime.error, signal: runtime.signal })
|
|
25
25
|
: null;
|
|
26
|
-
|
|
26
|
+
const deadline = runtime.requestTimeoutMs === undefined
|
|
27
|
+
? ''
|
|
28
|
+
: `, ${Math.ceil(runtime.requestTimeoutMs / 1_000)}s limit`;
|
|
29
|
+
spinner?.start(`${message}${deadline}`);
|
|
27
30
|
return task().then((result) => {
|
|
28
31
|
spinner?.stop();
|
|
29
32
|
return result;
|
|
@@ -408,6 +411,224 @@ async function searchStatus(runtime, globals) {
|
|
|
408
411
|
}, { signal: runtime.requestSignal }));
|
|
409
412
|
return present(runtime, globals, response, value => renderSearchStatus(value.data));
|
|
410
413
|
}
|
|
414
|
+
async function searchAnalytics(runtime, globals, args) {
|
|
415
|
+
const view = parseChoice(args.view, { name: 'view', choices: ['pages', 'keywords'] });
|
|
416
|
+
if (view._tag === 'Err')
|
|
417
|
+
return view;
|
|
418
|
+
const period = parseChoice(args.period, { name: '--period', choices: ['7d', '28d', '3m', '6m', '12m'], defaultValue: '28d' });
|
|
419
|
+
if (period._tag === 'Err')
|
|
420
|
+
return period;
|
|
421
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
|
|
422
|
+
if (limit._tag === 'Err')
|
|
423
|
+
return limit;
|
|
424
|
+
const page = parseInteger(args.page, { name: '--page', minimum: 1, defaultValue: 1 });
|
|
425
|
+
if (page._tag === 'Err')
|
|
426
|
+
return page;
|
|
427
|
+
const sort = parseChoice(args.sort, { name: '--sort', choices: ['clicks', 'impressions', 'ctr', 'position'], defaultValue: 'clicks' });
|
|
428
|
+
if (sort._tag === 'Err')
|
|
429
|
+
return sort;
|
|
430
|
+
const sortDir = parseChoice(args.sortDir, { name: '--sort-dir', choices: ['asc', 'desc'], defaultValue: 'desc' });
|
|
431
|
+
if (sortDir._tag === 'Err')
|
|
432
|
+
return sortDir;
|
|
433
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
434
|
+
if (resolved._tag === 'Err')
|
|
435
|
+
return resolved;
|
|
436
|
+
const response = await withSpinner(runtime, 'Loading Search Console rows', () => resolved.value.api.client.search.queryAnalytics({
|
|
437
|
+
params: { siteId: resolved.value.siteId },
|
|
438
|
+
query: {
|
|
439
|
+
view: view.value,
|
|
440
|
+
period: period.value,
|
|
441
|
+
limit: limit.value,
|
|
442
|
+
page: page.value,
|
|
443
|
+
search: args.search,
|
|
444
|
+
sort: sort.value,
|
|
445
|
+
sortDir: sortDir.value,
|
|
446
|
+
},
|
|
447
|
+
}, { signal: runtime.requestSignal }));
|
|
448
|
+
return present(runtime, globals, response, value => renderSearchAnalytics(value.data));
|
|
449
|
+
}
|
|
450
|
+
async function researchOverview(runtime, globals) {
|
|
451
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
452
|
+
if (resolved._tag === 'Err')
|
|
453
|
+
return resolved;
|
|
454
|
+
const response = await withSpinner(runtime, 'Loading stored research', () => resolved.value.api.client.research.overview({
|
|
455
|
+
params: { siteId: resolved.value.siteId },
|
|
456
|
+
}, { signal: runtime.requestSignal }));
|
|
457
|
+
return present(runtime, globals, response, value => renderResearchOverview(value.data));
|
|
458
|
+
}
|
|
459
|
+
async function researchKeywordIdeas(runtime, globals, args) {
|
|
460
|
+
const minVolume = parseInteger(args.minVolume, { name: '--min-volume', minimum: 0, defaultValue: 10 });
|
|
461
|
+
if (minVolume._tag === 'Err')
|
|
462
|
+
return minVolume;
|
|
463
|
+
const maxVolume = parseInteger(args.maxVolume, { name: '--max-volume', minimum: 0, defaultValue: 10_000 });
|
|
464
|
+
if (maxVolume._tag === 'Err')
|
|
465
|
+
return maxVolume;
|
|
466
|
+
const minDifficulty = parseInteger(args.minDifficulty, { name: '--min-difficulty', minimum: 0, maximum: 100, defaultValue: 0 });
|
|
467
|
+
if (minDifficulty._tag === 'Err')
|
|
468
|
+
return minDifficulty;
|
|
469
|
+
const maxDifficulty = parseInteger(args.maxDifficulty, { name: '--max-difficulty', minimum: 0, maximum: 100, defaultValue: 60 });
|
|
470
|
+
if (maxDifficulty._tag === 'Err')
|
|
471
|
+
return maxDifficulty;
|
|
472
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 20 });
|
|
473
|
+
if (limit._tag === 'Err')
|
|
474
|
+
return limit;
|
|
475
|
+
const locationCode = parseInteger(args.locationCode, { name: '--location-code', minimum: 1, defaultValue: 2840 });
|
|
476
|
+
if (locationCode._tag === 'Err')
|
|
477
|
+
return locationCode;
|
|
478
|
+
if (maxVolume.value < minVolume.value)
|
|
479
|
+
return fail(EXIT_CODE.invalidInput, '--max-volume must be at least --min-volume.');
|
|
480
|
+
if (maxDifficulty.value < minDifficulty.value)
|
|
481
|
+
return fail(EXIT_CODE.invalidInput, '--max-difficulty must be at least --min-difficulty.');
|
|
482
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
483
|
+
if (resolved._tag === 'Err')
|
|
484
|
+
return resolved;
|
|
485
|
+
const response = await withSpinner(runtime, 'Researching keyword ideas', () => resolved.value.api.client.research.keywords({
|
|
486
|
+
params: { siteId: resolved.value.siteId },
|
|
487
|
+
query: {
|
|
488
|
+
topic: args.topic,
|
|
489
|
+
minVolume: minVolume.value,
|
|
490
|
+
maxVolume: maxVolume.value,
|
|
491
|
+
minDifficulty: minDifficulty.value,
|
|
492
|
+
maxDifficulty: maxDifficulty.value,
|
|
493
|
+
intent: args.intent,
|
|
494
|
+
includeRelated: args.related ?? true,
|
|
495
|
+
verbose: args.verbose ?? false,
|
|
496
|
+
limit: limit.value,
|
|
497
|
+
locationCode: locationCode.value,
|
|
498
|
+
},
|
|
499
|
+
}, { signal: runtime.requestSignal }));
|
|
500
|
+
return present(runtime, globals, response, value => renderKeywordResearch(value.data));
|
|
501
|
+
}
|
|
502
|
+
async function researchSerp(runtime, globals, args) {
|
|
503
|
+
const depth = parseInteger(args.depth, { name: '--depth', minimum: 1, maximum: 20, defaultValue: 10 });
|
|
504
|
+
if (depth._tag === 'Err')
|
|
505
|
+
return depth;
|
|
506
|
+
const locationCode = parseInteger(args.locationCode, { name: '--location-code', minimum: 1, defaultValue: 2840 });
|
|
507
|
+
if (locationCode._tag === 'Err')
|
|
508
|
+
return locationCode;
|
|
509
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
510
|
+
if (resolved._tag === 'Err')
|
|
511
|
+
return resolved;
|
|
512
|
+
const response = await withSpinner(runtime, 'Analyzing the SERP', () => resolved.value.api.client.research.serp({
|
|
513
|
+
params: { siteId: resolved.value.siteId },
|
|
514
|
+
query: { keyword: args.keyword, depth: depth.value, locationCode: locationCode.value },
|
|
515
|
+
}, { signal: runtime.requestSignal }));
|
|
516
|
+
return present(runtime, globals, response, value => renderSerpResearch(value.data));
|
|
517
|
+
}
|
|
518
|
+
async function researchRankings(runtime, globals, args) {
|
|
519
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 50 });
|
|
520
|
+
if (limit._tag === 'Err')
|
|
521
|
+
return limit;
|
|
522
|
+
const minPosition = parseInteger(args.minPosition, { name: '--min-position', minimum: 1, maximum: 100, defaultValue: 1 });
|
|
523
|
+
if (minPosition._tag === 'Err')
|
|
524
|
+
return minPosition;
|
|
525
|
+
const maxPosition = parseInteger(args.maxPosition, { name: '--max-position', minimum: 1, maximum: 100, defaultValue: 20 });
|
|
526
|
+
if (maxPosition._tag === 'Err')
|
|
527
|
+
return maxPosition;
|
|
528
|
+
const locationCode = parseInteger(args.locationCode, { name: '--location-code', minimum: 1, defaultValue: 2840 });
|
|
529
|
+
if (locationCode._tag === 'Err')
|
|
530
|
+
return locationCode;
|
|
531
|
+
const order = parseChoice(args.order, { name: '--order', choices: ['position', 'traffic'], defaultValue: 'position' });
|
|
532
|
+
if (order._tag === 'Err')
|
|
533
|
+
return order;
|
|
534
|
+
if (maxPosition.value < minPosition.value)
|
|
535
|
+
return fail(EXIT_CODE.invalidInput, '--max-position must be at least --min-position.');
|
|
536
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
537
|
+
if (resolved._tag === 'Err')
|
|
538
|
+
return resolved;
|
|
539
|
+
const response = await withSpinner(runtime, 'Researching domain rankings', () => resolved.value.api.client.research.rankings({
|
|
540
|
+
params: { siteId: resolved.value.siteId },
|
|
541
|
+
query: {
|
|
542
|
+
domain: args.domain,
|
|
543
|
+
limit: limit.value,
|
|
544
|
+
minPosition: minPosition.value,
|
|
545
|
+
maxPosition: maxPosition.value,
|
|
546
|
+
locationCode: locationCode.value,
|
|
547
|
+
order: order.value,
|
|
548
|
+
},
|
|
549
|
+
}, { signal: runtime.requestSignal }));
|
|
550
|
+
return present(runtime, globals, response, value => renderRankingsResearch(value.data));
|
|
551
|
+
}
|
|
552
|
+
async function auditContentDecay(runtime, globals) {
|
|
553
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
554
|
+
if (resolved._tag === 'Err')
|
|
555
|
+
return resolved;
|
|
556
|
+
const response = await withSpinner(runtime, 'Loading content decay', () => resolved.value.api.client.audit.contentDecay({
|
|
557
|
+
params: { siteId: resolved.value.siteId },
|
|
558
|
+
}, { signal: runtime.requestSignal }));
|
|
559
|
+
return present(runtime, globals, response, value => renderContentDecay(value.data));
|
|
560
|
+
}
|
|
561
|
+
async function auditDuplicateClusters(runtime, globals) {
|
|
562
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
563
|
+
if (resolved._tag === 'Err')
|
|
564
|
+
return resolved;
|
|
565
|
+
const response = await withSpinner(runtime, 'Loading duplicate clusters', () => resolved.value.api.client.audit.duplicateClusters({
|
|
566
|
+
params: { siteId: resolved.value.siteId },
|
|
567
|
+
}, { signal: runtime.requestSignal }));
|
|
568
|
+
return present(runtime, globals, response, value => renderDuplicateClusters(value.data));
|
|
569
|
+
}
|
|
570
|
+
async function auditLinkOpportunities(runtime, globals) {
|
|
571
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
572
|
+
if (resolved._tag === 'Err')
|
|
573
|
+
return resolved;
|
|
574
|
+
const response = await withSpinner(runtime, 'Loading link opportunities', () => resolved.value.api.client.audit.linkOpportunities({
|
|
575
|
+
params: { siteId: resolved.value.siteId },
|
|
576
|
+
}, { signal: runtime.requestSignal }));
|
|
577
|
+
return present(runtime, globals, response, value => renderLinkOpportunities(value.data));
|
|
578
|
+
}
|
|
579
|
+
async function contentBriefList(runtime, globals, args) {
|
|
580
|
+
const status = args.status === undefined
|
|
581
|
+
? ok(undefined)
|
|
582
|
+
: parseChoice(args.status, {
|
|
583
|
+
name: '--status',
|
|
584
|
+
choices: ['queued', 'researching', 'ready', 'written', 'published', 'stale', 'failed', 'archived'],
|
|
585
|
+
});
|
|
586
|
+
if (status._tag === 'Err')
|
|
587
|
+
return status;
|
|
588
|
+
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 25 });
|
|
589
|
+
if (limit._tag === 'Err')
|
|
590
|
+
return limit;
|
|
591
|
+
const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
|
|
592
|
+
if (offset._tag === 'Err')
|
|
593
|
+
return offset;
|
|
594
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
595
|
+
if (resolved._tag === 'Err')
|
|
596
|
+
return resolved;
|
|
597
|
+
const response = await withSpinner(runtime, 'Loading Content Briefs', () => resolved.value.api.client.content.listBriefs({
|
|
598
|
+
params: { siteId: resolved.value.siteId },
|
|
599
|
+
query: { status: status.value, limit: limit.value, offset: offset.value },
|
|
600
|
+
}, { signal: runtime.requestSignal }));
|
|
601
|
+
return present(runtime, globals, response, value => renderContentBriefs(value.data));
|
|
602
|
+
}
|
|
603
|
+
async function contentBriefShow(runtime, globals, briefId) {
|
|
604
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
605
|
+
if (resolved._tag === 'Err')
|
|
606
|
+
return resolved;
|
|
607
|
+
const response = await withSpinner(runtime, 'Loading Content Brief', () => resolved.value.api.client.content.showBrief({
|
|
608
|
+
params: { siteId: resolved.value.siteId, briefId },
|
|
609
|
+
}, { signal: runtime.requestSignal }));
|
|
610
|
+
return present(runtime, globals, response, value => renderContentBrief(value.data));
|
|
611
|
+
}
|
|
612
|
+
async function contentBriefCreate(runtime, globals, args) {
|
|
613
|
+
const targetPage = args.targetPage === undefined ? ok(undefined) : parseAbsolutePageUrl(args.targetPage);
|
|
614
|
+
if (targetPage._tag === 'Err')
|
|
615
|
+
return targetPage;
|
|
616
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
617
|
+
if (resolved._tag === 'Err')
|
|
618
|
+
return resolved;
|
|
619
|
+
const confirmed = await confirmMutation(runtime, globals, `Create a Content Brief for ${args.keyword}?`);
|
|
620
|
+
if (confirmed._tag === 'Err')
|
|
621
|
+
return confirmed;
|
|
622
|
+
if (!confirmed.value) {
|
|
623
|
+
writeDiagnostic(runtime, 'Mutation cancelled.');
|
|
624
|
+
return ok(undefined);
|
|
625
|
+
}
|
|
626
|
+
const response = await withSpinner(runtime, 'Creating Content Brief', () => resolved.value.api.client.content.createBrief({
|
|
627
|
+
params: { siteId: resolved.value.siteId },
|
|
628
|
+
body: { keyword: args.keyword, targetPage: targetPage.value },
|
|
629
|
+
}, { signal: runtime.requestSignal }));
|
|
630
|
+
return present(runtime, globals, response, value => renderContentBriefCreated(value.data));
|
|
631
|
+
}
|
|
411
632
|
async function backlinksRecoverable(runtime, globals, limitInput, offsetInput) {
|
|
412
633
|
const limit = parseInteger(limitInput, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
|
|
413
634
|
if (limit._tag === 'Err')
|
|
@@ -551,13 +772,162 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
551
772
|
}),
|
|
552
773
|
},
|
|
553
774
|
});
|
|
775
|
+
const research = defineCommand({
|
|
776
|
+
meta: { name: 'research', description: 'Read stored and live search research' },
|
|
777
|
+
subCommands: {
|
|
778
|
+
overview: defineCommand({
|
|
779
|
+
meta: { name: 'overview', description: 'Read stored Site metrics and competitor research' },
|
|
780
|
+
run: ({ args }) => capture(execution, () => researchOverview(runtime, globals), args._, 0)(),
|
|
781
|
+
}),
|
|
782
|
+
keywords: defineCommand({
|
|
783
|
+
meta: { name: 'keywords', description: 'Find live keyword ideas with volume and difficulty' },
|
|
784
|
+
args: {
|
|
785
|
+
'topic': positional('topic', 'Keyword or topic'),
|
|
786
|
+
'min-volume': { type: 'string', description: 'Minimum monthly volume' },
|
|
787
|
+
'max-volume': { type: 'string', description: 'Maximum monthly volume' },
|
|
788
|
+
'min-difficulty': { type: 'string', description: 'Minimum difficulty, 0 to 100' },
|
|
789
|
+
'max-difficulty': { type: 'string', description: 'Maximum difficulty, 0 to 100' },
|
|
790
|
+
'limit': { type: 'string', description: 'Maximum keywords, 1 to 100' },
|
|
791
|
+
'location-code': { type: 'string', description: 'DataForSEO location code' },
|
|
792
|
+
'intent': { type: 'string', description: 'Search intent filter' },
|
|
793
|
+
'related': { type: 'boolean', default: true, description: 'Include related keywords; use --no-related to exclude them' },
|
|
794
|
+
'verbose': { type: 'boolean', description: 'Include full keyword evidence' },
|
|
795
|
+
},
|
|
796
|
+
run: ({ args }) => capture(execution, () => researchKeywordIdeas(runtime, globals, {
|
|
797
|
+
topic: args.topic,
|
|
798
|
+
minVolume: args['min-volume'],
|
|
799
|
+
maxVolume: args['max-volume'],
|
|
800
|
+
minDifficulty: args['min-difficulty'],
|
|
801
|
+
maxDifficulty: args['max-difficulty'],
|
|
802
|
+
limit: args.limit,
|
|
803
|
+
locationCode: args['location-code'],
|
|
804
|
+
intent: args.intent,
|
|
805
|
+
related: args.related,
|
|
806
|
+
verbose: args.verbose,
|
|
807
|
+
}), args._, 1)(),
|
|
808
|
+
}),
|
|
809
|
+
serp: defineCommand({
|
|
810
|
+
meta: { name: 'serp', description: 'Analyze one live Google SERP' },
|
|
811
|
+
args: {
|
|
812
|
+
'keyword': positional('keyword', 'Keyword'),
|
|
813
|
+
'depth': { type: 'string', description: 'Organic result depth, 1 to 20' },
|
|
814
|
+
'location-code': { type: 'string', description: 'DataForSEO location code' },
|
|
815
|
+
},
|
|
816
|
+
run: ({ args }) => capture(execution, () => researchSerp(runtime, globals, {
|
|
817
|
+
keyword: args.keyword,
|
|
818
|
+
depth: args.depth,
|
|
819
|
+
locationCode: args['location-code'],
|
|
820
|
+
}), args._, 1)(),
|
|
821
|
+
}),
|
|
822
|
+
rankings: defineCommand({
|
|
823
|
+
meta: { name: 'rankings', description: 'Research live keyword rankings for a domain' },
|
|
824
|
+
args: {
|
|
825
|
+
'domain': positional('domain', 'Domain without a protocol'),
|
|
826
|
+
'limit': { type: 'string', description: 'Maximum keywords, 1 to 100' },
|
|
827
|
+
'min-position': { type: 'string', description: 'Minimum position, 1 to 100' },
|
|
828
|
+
'max-position': { type: 'string', description: 'Maximum position, 1 to 100' },
|
|
829
|
+
'location-code': { type: 'string', description: 'DataForSEO location code' },
|
|
830
|
+
'order': { type: 'enum', options: ['position', 'traffic'], description: 'Sort rankings' },
|
|
831
|
+
},
|
|
832
|
+
run: ({ args }) => capture(execution, () => researchRankings(runtime, globals, {
|
|
833
|
+
domain: args.domain,
|
|
834
|
+
limit: args.limit,
|
|
835
|
+
minPosition: args['min-position'],
|
|
836
|
+
maxPosition: args['max-position'],
|
|
837
|
+
locationCode: args['location-code'],
|
|
838
|
+
order: args.order,
|
|
839
|
+
}), args._, 1)(),
|
|
840
|
+
}),
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
const audit = defineCommand({
|
|
844
|
+
meta: { name: 'audit', description: 'Read stored Site audit reports' },
|
|
845
|
+
subCommands: {
|
|
846
|
+
'content-decay': defineCommand({
|
|
847
|
+
meta: { name: 'content-decay', description: 'List Pages losing Search Console clicks' },
|
|
848
|
+
run: ({ args }) => capture(execution, () => auditContentDecay(runtime, globals), args._, 0)(),
|
|
849
|
+
}),
|
|
850
|
+
'duplicates': defineCommand({
|
|
851
|
+
meta: { name: 'duplicates', description: 'List duplicate Page clusters' },
|
|
852
|
+
run: ({ args }) => capture(execution, () => auditDuplicateClusters(runtime, globals), args._, 0)(),
|
|
853
|
+
}),
|
|
854
|
+
'link-opportunities': defineCommand({
|
|
855
|
+
meta: { name: 'link-opportunities', description: 'List internal link opportunities' },
|
|
856
|
+
run: ({ args }) => capture(execution, () => auditLinkOpportunities(runtime, globals), args._, 0)(),
|
|
857
|
+
}),
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
const content = defineCommand({
|
|
861
|
+
meta: { name: 'content', description: 'Read and create Content Briefs' },
|
|
862
|
+
subCommands: {
|
|
863
|
+
briefs: defineCommand({
|
|
864
|
+
meta: { name: 'briefs', description: 'Manage Content Briefs' },
|
|
865
|
+
subCommands: {
|
|
866
|
+
list: defineCommand({
|
|
867
|
+
meta: { name: 'list', description: 'List Content Briefs' },
|
|
868
|
+
args: {
|
|
869
|
+
status: {
|
|
870
|
+
type: 'enum',
|
|
871
|
+
options: ['queued', 'researching', 'ready', 'written', 'published', 'stale', 'failed', 'archived'],
|
|
872
|
+
description: 'Filter by status',
|
|
873
|
+
},
|
|
874
|
+
limit: { type: 'string', description: 'Maximum Content Briefs, 1 to 100' },
|
|
875
|
+
offset: { type: 'string', description: 'Pagination offset' },
|
|
876
|
+
},
|
|
877
|
+
run: ({ args }) => capture(execution, () => contentBriefList(runtime, globals, {
|
|
878
|
+
status: args.status,
|
|
879
|
+
limit: args.limit,
|
|
880
|
+
offset: args.offset,
|
|
881
|
+
}), args._, 0)(),
|
|
882
|
+
}),
|
|
883
|
+
show: defineCommand({
|
|
884
|
+
meta: { name: 'show', description: 'Show one Content Brief' },
|
|
885
|
+
args: { briefId: positional('brief-id', 'Content Brief ID') },
|
|
886
|
+
run: ({ args }) => capture(execution, () => contentBriefShow(runtime, globals, args.briefId), args._, 1)(),
|
|
887
|
+
}),
|
|
888
|
+
create: defineCommand({
|
|
889
|
+
meta: { name: 'create', description: 'Create a Content Brief' },
|
|
890
|
+
args: {
|
|
891
|
+
'keyword': positional('keyword', 'Target keyword'),
|
|
892
|
+
'target-page': { type: 'string', description: 'Absolute target Page URL' },
|
|
893
|
+
},
|
|
894
|
+
run: ({ args }) => capture(execution, () => contentBriefCreate(runtime, globals, {
|
|
895
|
+
keyword: args.keyword,
|
|
896
|
+
targetPage: args['target-page'],
|
|
897
|
+
}), args._, 1)(),
|
|
898
|
+
}),
|
|
899
|
+
},
|
|
900
|
+
}),
|
|
901
|
+
},
|
|
902
|
+
});
|
|
554
903
|
const search = defineCommand({
|
|
555
904
|
meta: { name: 'search', description: 'Read Search Console state' },
|
|
556
905
|
subCommands: {
|
|
557
906
|
status: defineCommand({
|
|
558
|
-
meta: { name: 'status', description: 'Read Search Console
|
|
907
|
+
meta: { name: 'status', description: 'Read the stored Search Console connection' },
|
|
559
908
|
run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
|
|
560
909
|
}),
|
|
910
|
+
analytics: defineCommand({
|
|
911
|
+
meta: { name: 'analytics', description: 'List Search Console query or Page rows' },
|
|
912
|
+
args: {
|
|
913
|
+
'view': positional('pages-or-keywords', 'pages or keywords'),
|
|
914
|
+
'period': { type: 'enum', options: ['7d', '28d', '3m', '6m', '12m'], description: 'Search Console period' },
|
|
915
|
+
'limit': { type: 'string', description: 'Maximum rows, 1 to 100' },
|
|
916
|
+
'page': { type: 'string', description: 'Result page number' },
|
|
917
|
+
'search': { type: 'string', description: 'Filter rows by text' },
|
|
918
|
+
'sort': { type: 'enum', options: ['clicks', 'impressions', 'ctr', 'position'], description: 'Sort field' },
|
|
919
|
+
'sort-dir': { type: 'enum', options: ['asc', 'desc'], description: 'Sort direction' },
|
|
920
|
+
},
|
|
921
|
+
run: ({ args }) => capture(execution, () => searchAnalytics(runtime, globals, {
|
|
922
|
+
view: args.view,
|
|
923
|
+
period: args.period,
|
|
924
|
+
limit: args.limit,
|
|
925
|
+
page: args.page,
|
|
926
|
+
search: args.search,
|
|
927
|
+
sort: args.sort,
|
|
928
|
+
sortDir: args['sort-dir'],
|
|
929
|
+
}), args._, 1)(),
|
|
930
|
+
}),
|
|
561
931
|
},
|
|
562
932
|
});
|
|
563
933
|
return defineCommand({
|
|
@@ -613,13 +983,16 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
613
983
|
run: ({ args }) => capture(execution, () => usage(runtime, globals, args.group), args._, 0)(),
|
|
614
984
|
}),
|
|
615
985
|
actions,
|
|
986
|
+
audit,
|
|
616
987
|
backlinks,
|
|
988
|
+
content,
|
|
617
989
|
mentions,
|
|
618
990
|
page,
|
|
619
991
|
performance: defineCommand({
|
|
620
992
|
meta: { name: 'performance', description: 'Read the stored Site performance overview' },
|
|
621
993
|
run: ({ args }) => capture(execution, () => performance(runtime, globals), args._, 0)(),
|
|
622
994
|
}),
|
|
995
|
+
research,
|
|
623
996
|
search,
|
|
624
997
|
},
|
|
625
998
|
});
|
package/dist/failures.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { VERSION } from './version.js';
|
|
2
|
+
const UPDATE_COMMAND = 'pnpm add -g @nuxtseo/cli';
|
|
3
|
+
// `contract_violation` most often means this CLI is older than the server
|
|
4
|
+
// contract. One line routes an agent to update before anything else.
|
|
5
|
+
const CONTRACT_REMEDIATION = [
|
|
6
|
+
'Most common cause: this CLI is older than the server contract.',
|
|
7
|
+
`Update first with ${UPDATE_COMMAND}, then run the command again.`,
|
|
8
|
+
].join(' ');
|
|
1
9
|
export const EXIT_CODE = {
|
|
2
10
|
success: 0,
|
|
3
11
|
invalidInput: 2,
|
|
@@ -89,15 +97,23 @@ export function fromSdkFailure(error) {
|
|
|
89
97
|
message: [
|
|
90
98
|
`${error.code}: ${error.message}`,
|
|
91
99
|
...metadataLines(error),
|
|
92
|
-
|
|
100
|
+
// `unauthorized` is the caller's own credential. `auth_expired` is the
|
|
101
|
+
// Site's Search Console connection, which no token change repairs.
|
|
102
|
+
error.code === 'unauthorized'
|
|
93
103
|
? 'Replace NUXTSEO_TOKEN, or run `nuxtseo login` when using a stored credential.'
|
|
94
104
|
: undefined,
|
|
105
|
+
error.code === 'auth_expired'
|
|
106
|
+
? 'Open site settings in the dashboard, then Search Console, to reconnect.'
|
|
107
|
+
: undefined,
|
|
108
|
+
error.code === 'contract_violation'
|
|
109
|
+
? CONTRACT_REMEDIATION
|
|
110
|
+
: undefined,
|
|
95
111
|
].filter((line) => line !== undefined).join('\n'),
|
|
96
112
|
protocolResponse: error.response,
|
|
97
113
|
},
|
|
98
114
|
};
|
|
99
115
|
case 'ContractFailure':
|
|
100
|
-
return fail(EXIT_CODE.infrastructure, [`contract_violation: ${error.message}`, error.requestId ? `Request ID: ${error.requestId}` : undefined]
|
|
116
|
+
return fail(EXIT_CODE.infrastructure, [`contract_violation: ${error.message}`, error.requestId ? `Request ID: ${error.requestId}` : undefined, CONTRACT_REMEDIATION, `Current CLI version: ${VERSION}`]
|
|
101
117
|
.filter((line) => line !== undefined)
|
|
102
118
|
.join('\n'), undefined, 'contract_violation');
|
|
103
119
|
case 'TransportFailure': {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,6 @@ export { runCli } from './cli.js';
|
|
|
2
2
|
export { EXIT_CODE } from './failures.js';
|
|
3
3
|
export type { CliFailure, CliFailureCode, CliResult, ExitCode, LocalFailureCode } from './failures.js';
|
|
4
4
|
export type { CliRuntime } from './runtime.js';
|
|
5
|
+
export { checkForUpdate, isNewerVersion, updateNoticeFor } from './update-check.js';
|
|
6
|
+
export type { UpdateNotice } from './update-check.js';
|
|
5
7
|
export { VERSION } from './version.js';
|
package/dist/index.js
CHANGED
package/dist/parse.d.ts
CHANGED
|
@@ -21,3 +21,8 @@ export declare function parseInteger(input: string | undefined, options: {
|
|
|
21
21
|
maximum?: number;
|
|
22
22
|
defaultValue: number;
|
|
23
23
|
}): CliResult<number>;
|
|
24
|
+
export declare function parseChoice<const TChoice extends string>(input: string | undefined, options: {
|
|
25
|
+
name: string;
|
|
26
|
+
choices: readonly TChoice[];
|
|
27
|
+
defaultValue?: TChoice;
|
|
28
|
+
}): CliResult<TChoice>;
|
package/dist/parse.js
CHANGED
|
@@ -124,3 +124,10 @@ export function parseInteger(input, options) {
|
|
|
124
124
|
}
|
|
125
125
|
return ok(parsed);
|
|
126
126
|
}
|
|
127
|
+
export function parseChoice(input, options) {
|
|
128
|
+
if (input === undefined && options.defaultValue !== undefined)
|
|
129
|
+
return ok(options.defaultValue);
|
|
130
|
+
if (input !== undefined && options.choices.includes(input))
|
|
131
|
+
return ok(input);
|
|
132
|
+
return fail(EXIT_CODE.invalidInput, `${options.name} must be one of: ${options.choices.join(', ')}.`);
|
|
133
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { AccountUsageResponse, SitesListResponse } from '@nuxtseo/protocol/v1';
|
|
2
2
|
import type { ActionList, ActionResolve, ActionShow } from '@nuxtseo/protocol/v1/actions';
|
|
3
|
+
import type { AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities } from '@nuxtseo/protocol/v1/audit';
|
|
3
4
|
import type { Mentions, RecoverableBacklinks } from '@nuxtseo/protocol/v1/backlinks';
|
|
4
|
-
import type {
|
|
5
|
+
import type { ContentBrief, ContentBriefCreateData, ContentBriefListData } from '@nuxtseo/protocol/v1/content';
|
|
6
|
+
import type { SearchAnalyticsData, SearchStatusData } from '@nuxtseo/protocol/v1/gsc';
|
|
5
7
|
import type { PageInspect, PageScan } from '@nuxtseo/protocol/v1/pages';
|
|
6
8
|
import type { SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
|
|
9
|
+
import type { KeywordResearchData, RankingsResearchData, SerpResearchData, StoredResearchOverview } from '@nuxtseo/protocol/v1/research';
|
|
7
10
|
export declare function renderSites(response: SitesListResponse, selectedSiteId?: string): string;
|
|
8
11
|
export declare function renderUsage(response: AccountUsageResponse): string;
|
|
9
12
|
export declare function renderActions(data: ActionList): string;
|
|
@@ -13,5 +16,16 @@ export declare function renderPageInspection(data: PageInspect): string;
|
|
|
13
16
|
export declare function renderPageScan(data: PageScan): string;
|
|
14
17
|
export declare function renderPerformance(data: SitePerformanceOverview): string;
|
|
15
18
|
export declare function renderSearchStatus(data: SearchStatusData): string;
|
|
19
|
+
export declare function renderSearchAnalytics(data: SearchAnalyticsData): string;
|
|
20
|
+
export declare function renderResearchOverview(data: StoredResearchOverview): string;
|
|
21
|
+
export declare function renderKeywordResearch(data: KeywordResearchData): string;
|
|
22
|
+
export declare function renderSerpResearch(data: SerpResearchData): string;
|
|
23
|
+
export declare function renderRankingsResearch(data: RankingsResearchData): string;
|
|
24
|
+
export declare function renderContentBriefs(data: ContentBriefListData): string;
|
|
25
|
+
export declare function renderContentBrief(data: ContentBrief): string;
|
|
26
|
+
export declare function renderContentBriefCreated(data: ContentBriefCreateData): string;
|
|
27
|
+
export declare function renderContentDecay(data: AuditContentDecay): string;
|
|
28
|
+
export declare function renderDuplicateClusters(data: AuditDuplicateClusters): string;
|
|
29
|
+
export declare function renderLinkOpportunities(data: AuditLinkOpportunities): string;
|
|
16
30
|
export declare function renderRecoverableBacklinks(data: RecoverableBacklinks): string;
|
|
17
31
|
export declare function renderMentions(data: Mentions): string;
|