@rankcli/cli 0.0.18 → 0.0.20
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 +17 -12
- package/dist/index.js +1417 -210
- package/dist/index.mjs +1379 -168
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -284,7 +284,7 @@ async function analyze(options) {
|
|
|
284
284
|
// src/commands/audit.ts
|
|
285
285
|
var import_agent_runtime = require("@rankcli/agent-runtime");
|
|
286
286
|
var import_ora = __toESM(require("ora"));
|
|
287
|
-
var
|
|
287
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
288
288
|
|
|
289
289
|
// src/lib/config.ts
|
|
290
290
|
var import_conf = __toESM(require("conf"));
|
|
@@ -479,6 +479,191 @@ async function getAuthenticatedClient() {
|
|
|
479
479
|
return supabase;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
// src/lib/upsell.ts
|
|
483
|
+
var import_chalk = __toESM(require("chalk"));
|
|
484
|
+
var WEB_APP_URL2 = "https://rankcli.dev";
|
|
485
|
+
var TIER_LIMITS = {
|
|
486
|
+
free_anon: {
|
|
487
|
+
checks: 50,
|
|
488
|
+
sites: 0,
|
|
489
|
+
domains_per_month: 1,
|
|
490
|
+
features: { geo: false, auto_fix: false, github: false, ai: false }
|
|
491
|
+
},
|
|
492
|
+
free: {
|
|
493
|
+
checks: 100,
|
|
494
|
+
sites: 1,
|
|
495
|
+
domains_per_month: 2,
|
|
496
|
+
features: { geo: "basic", auto_fix: false, github: false, ai: false }
|
|
497
|
+
},
|
|
498
|
+
solo: {
|
|
499
|
+
checks: 280,
|
|
500
|
+
sites: 3,
|
|
501
|
+
domains_per_month: 5,
|
|
502
|
+
features: { geo: true, auto_fix: true, github: false, ai: true }
|
|
503
|
+
},
|
|
504
|
+
pro: {
|
|
505
|
+
checks: 280,
|
|
506
|
+
sites: 10,
|
|
507
|
+
domains_per_month: 15,
|
|
508
|
+
features: { geo: true, auto_fix: true, github: true, ai: true }
|
|
509
|
+
},
|
|
510
|
+
agency: {
|
|
511
|
+
checks: 280,
|
|
512
|
+
sites: 50,
|
|
513
|
+
domains_per_month: null,
|
|
514
|
+
// unlimited
|
|
515
|
+
features: { geo: true, auto_fix: true, github: true, ai: true }
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
var UPSELL_MESSAGES = {
|
|
519
|
+
// After free audit completes
|
|
520
|
+
free_audit_complete: () => [
|
|
521
|
+
"",
|
|
522
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
523
|
+
import_chalk.default.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
|
|
524
|
+
import_chalk.default.dim(" Create a free account to unlock 100 checks and save your results."),
|
|
525
|
+
"",
|
|
526
|
+
import_chalk.default.cyan(` \u2192 rankcli login`),
|
|
527
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
528
|
+
""
|
|
529
|
+
],
|
|
530
|
+
// When checks limit is reached
|
|
531
|
+
checks_limit: (ctx) => [
|
|
532
|
+
"",
|
|
533
|
+
import_chalk.default.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
|
|
534
|
+
import_chalk.default.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
|
|
535
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
|
|
536
|
+
""
|
|
537
|
+
],
|
|
538
|
+
// When domain limit is reached
|
|
539
|
+
domain_limit: (ctx) => [
|
|
540
|
+
"",
|
|
541
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
542
|
+
import_chalk.default.red.bold(" \u2717 Domain limit reached"),
|
|
543
|
+
import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
|
|
544
|
+
"",
|
|
545
|
+
import_chalk.default.dim(" Upgrade to audit more sites:"),
|
|
546
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
|
|
547
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
548
|
+
""
|
|
549
|
+
],
|
|
550
|
+
// When GEO features are limited
|
|
551
|
+
geo_limited: (ctx) => [
|
|
552
|
+
"",
|
|
553
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
554
|
+
import_chalk.default.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
|
|
555
|
+
import_chalk.default.dim(" You're seeing basic AI crawler access checks."),
|
|
556
|
+
import_chalk.default.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
|
|
557
|
+
"",
|
|
558
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
|
|
559
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
560
|
+
""
|
|
561
|
+
],
|
|
562
|
+
// When auto-fix is attempted without access
|
|
563
|
+
auto_fix_blocked: () => [
|
|
564
|
+
"",
|
|
565
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
566
|
+
import_chalk.default.red.bold(" \u2717 Auto-fix requires Solo plan"),
|
|
567
|
+
import_chalk.default.dim(" Apply framework-specific SEO fixes directly to your codebase."),
|
|
568
|
+
"",
|
|
569
|
+
import_chalk.default.dim(" Solo ($9/mo) includes:"),
|
|
570
|
+
import_chalk.default.dim(" \u2022 Local auto-fix via CLI"),
|
|
571
|
+
import_chalk.default.dim(" \u2022 25+ framework support"),
|
|
572
|
+
import_chalk.default.dim(" \u2022 Full 280+ checks"),
|
|
573
|
+
"",
|
|
574
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
|
|
575
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
576
|
+
""
|
|
577
|
+
],
|
|
578
|
+
// When GitHub integration is attempted without access
|
|
579
|
+
github_blocked: () => [
|
|
580
|
+
"",
|
|
581
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
582
|
+
import_chalk.default.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
|
|
583
|
+
import_chalk.default.dim(" Automatically create fix PRs when SEO issues are detected."),
|
|
584
|
+
"",
|
|
585
|
+
import_chalk.default.dim(" Pro ($29/mo) includes:"),
|
|
586
|
+
import_chalk.default.dim(" \u2022 GitHub App integration"),
|
|
587
|
+
import_chalk.default.dim(" \u2022 Auto-fix PRs"),
|
|
588
|
+
import_chalk.default.dim(" \u2022 Daily scheduled audits"),
|
|
589
|
+
import_chalk.default.dim(" \u2022 Slack/Discord alerts"),
|
|
590
|
+
"",
|
|
591
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
|
|
592
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
593
|
+
""
|
|
594
|
+
],
|
|
595
|
+
// Keyword tracking limit reached
|
|
596
|
+
keyword_limit: (ctx) => [
|
|
597
|
+
"",
|
|
598
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
599
|
+
import_chalk.default.red.bold(" \u2717 Keyword tracking limit reached"),
|
|
600
|
+
import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
|
|
601
|
+
import_chalk.default.dim(` Currently tracking: ${ctx.current}`),
|
|
602
|
+
"",
|
|
603
|
+
import_chalk.default.dim(" Upgrade for more keywords:"),
|
|
604
|
+
import_chalk.default.dim(" \u2022 Solo ($9/mo): 100 keywords"),
|
|
605
|
+
import_chalk.default.dim(" \u2022 Pro ($29/mo): 500 keywords"),
|
|
606
|
+
import_chalk.default.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
|
|
607
|
+
"",
|
|
608
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
|
|
609
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
610
|
+
""
|
|
611
|
+
],
|
|
612
|
+
// Rate limit exceeded
|
|
613
|
+
rate_limit: (ctx) => [
|
|
614
|
+
"",
|
|
615
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
616
|
+
import_chalk.default.red.bold(" \u2717 API rate limit exceeded"),
|
|
617
|
+
import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
|
|
618
|
+
"",
|
|
619
|
+
import_chalk.default.dim(" Need more? Upgrade for higher limits:"),
|
|
620
|
+
import_chalk.default.dim(" \u2022 Solo: 100 req/day"),
|
|
621
|
+
import_chalk.default.dim(" \u2022 Pro: 1,000 req/day"),
|
|
622
|
+
import_chalk.default.dim(" \u2022 Agency: 10,000 req/day"),
|
|
623
|
+
"",
|
|
624
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
|
|
625
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
626
|
+
""
|
|
627
|
+
],
|
|
628
|
+
// Schedule exceeded (for GitHub Action)
|
|
629
|
+
schedule_exceeded: (ctx) => [
|
|
630
|
+
"",
|
|
631
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
632
|
+
import_chalk.default.yellow.bold(" \u23F0 Scheduled audit skipped"),
|
|
633
|
+
import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
|
|
634
|
+
"",
|
|
635
|
+
import_chalk.default.dim(" Upgrade for more frequent audits:"),
|
|
636
|
+
import_chalk.default.dim(" \u2022 Solo: Weekly"),
|
|
637
|
+
import_chalk.default.dim(" \u2022 Pro: Daily"),
|
|
638
|
+
import_chalk.default.dim(" \u2022 Agency: Hourly"),
|
|
639
|
+
"",
|
|
640
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
|
|
641
|
+
import_chalk.default.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
642
|
+
""
|
|
643
|
+
],
|
|
644
|
+
// Login prompt (soft)
|
|
645
|
+
login_prompt: () => [
|
|
646
|
+
"",
|
|
647
|
+
import_chalk.default.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
|
|
648
|
+
import_chalk.default.dim(" rankcli login"),
|
|
649
|
+
""
|
|
650
|
+
]
|
|
651
|
+
};
|
|
652
|
+
function showUpsell(messageKey, context) {
|
|
653
|
+
const messageFn = UPSELL_MESSAGES[messageKey];
|
|
654
|
+
if (!messageFn) return;
|
|
655
|
+
const ctx = {
|
|
656
|
+
tier: "free",
|
|
657
|
+
...context
|
|
658
|
+
};
|
|
659
|
+
const lines = messageFn(ctx);
|
|
660
|
+
lines.forEach((line) => console.log(line));
|
|
661
|
+
}
|
|
662
|
+
function hasFeature(tier, feature) {
|
|
663
|
+
const limits = TIER_LIMITS[tier];
|
|
664
|
+
return limits?.features?.[feature] ?? false;
|
|
665
|
+
}
|
|
666
|
+
|
|
482
667
|
// src/commands/audit.ts
|
|
483
668
|
var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = import_agent_runtime.frameworks;
|
|
484
669
|
async function fetchHtml(url, timeout = 1e4) {
|
|
@@ -546,7 +731,7 @@ Return JSON with:
|
|
|
546
731
|
}
|
|
547
732
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
548
733
|
if (!response.ok) {
|
|
549
|
-
console.error(
|
|
734
|
+
console.error(import_chalk2.default.yellow(`AI analysis failed: ${response.status}`));
|
|
550
735
|
return null;
|
|
551
736
|
}
|
|
552
737
|
const data = await response.json();
|
|
@@ -556,7 +741,7 @@ Return JSON with:
|
|
|
556
741
|
if (!jsonMatch) return null;
|
|
557
742
|
return JSON.parse(jsonMatch[0]);
|
|
558
743
|
} catch (error) {
|
|
559
|
-
console.error(
|
|
744
|
+
console.error(import_chalk2.default.yellow(`AI analysis error: ${error}`));
|
|
560
745
|
return null;
|
|
561
746
|
}
|
|
562
747
|
}
|
|
@@ -575,7 +760,7 @@ function mergeIssues(issueArrays) {
|
|
|
575
760
|
}
|
|
576
761
|
return Array.from(issueMap.values());
|
|
577
762
|
}
|
|
578
|
-
async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis) {
|
|
763
|
+
async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis, durationMs, checksRun) {
|
|
579
764
|
const supabase = await getAuthenticatedClient();
|
|
580
765
|
if (!supabase) return null;
|
|
581
766
|
const { userId, sitesLimit } = getUserInfo();
|
|
@@ -591,9 +776,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
591
776
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
592
777
|
const limit = sitesLimit ?? 1;
|
|
593
778
|
if (count !== null && count >= limit) {
|
|
594
|
-
console.log(
|
|
779
|
+
console.log(import_chalk2.default.yellow(`
|
|
595
780
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
596
|
-
console.log(
|
|
781
|
+
console.log(import_chalk2.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
597
782
|
return null;
|
|
598
783
|
}
|
|
599
784
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -603,7 +788,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
603
788
|
domain
|
|
604
789
|
}).select("id").single();
|
|
605
790
|
if (projectError || !newProject) {
|
|
606
|
-
console.error(
|
|
791
|
+
console.error(import_chalk2.default.dim("Failed to create project"));
|
|
607
792
|
return null;
|
|
608
793
|
}
|
|
609
794
|
projectId = newProject.id;
|
|
@@ -615,8 +800,11 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
615
800
|
onPage: report.healthScore?.onPage || 0,
|
|
616
801
|
content: report.healthScore?.content || 0,
|
|
617
802
|
performance: report.healthScore?.performance || 0,
|
|
618
|
-
security: report.healthScore?.security || 0
|
|
803
|
+
security: report.healthScore?.security || 0,
|
|
804
|
+
localSeo: report.healthScore?.localSeo || 0,
|
|
805
|
+
aiReadiness: report.healthScore?.aiReadiness || 0
|
|
619
806
|
};
|
|
807
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
620
808
|
const { data: audit2, error: auditError } = await supabase.from("audits").insert({
|
|
621
809
|
project_id: projectId,
|
|
622
810
|
triggered_by: "cli",
|
|
@@ -625,21 +813,23 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
625
813
|
score,
|
|
626
814
|
pages_crawled: pagesAudited,
|
|
627
815
|
issues_found: mergedIssues.length,
|
|
628
|
-
checks_run:
|
|
629
|
-
// Approximate
|
|
816
|
+
checks_run: checksRun,
|
|
630
817
|
health_scores: healthScores,
|
|
631
|
-
|
|
818
|
+
started_at: new Date(Date.now() - durationMs).toISOString(),
|
|
819
|
+
completed_at: now,
|
|
820
|
+
duration_ms: durationMs,
|
|
632
821
|
summary: {
|
|
633
822
|
errors: mergedIssues.filter((i) => i.severity === "error").length,
|
|
634
823
|
warnings: mergedIssues.filter((i) => i.severity === "warning").length,
|
|
635
824
|
notices: mergedIssues.filter((i) => i.severity === "notice").length,
|
|
636
825
|
pagesAudited,
|
|
826
|
+
checksRun,
|
|
637
827
|
source: "cli"
|
|
638
828
|
},
|
|
639
829
|
ai_analysis: aiAnalysis
|
|
640
830
|
}).select("id").single();
|
|
641
831
|
if (auditError || !audit2) {
|
|
642
|
-
console.error(
|
|
832
|
+
console.error(import_chalk2.default.dim("Failed to save audit"));
|
|
643
833
|
return null;
|
|
644
834
|
}
|
|
645
835
|
if (mergedIssues.length > 0) {
|
|
@@ -660,12 +850,14 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
660
850
|
await supabase.from("audit_issues").insert(issueRecords);
|
|
661
851
|
}
|
|
662
852
|
await supabase.from("projects").update({
|
|
663
|
-
last_audit_at:
|
|
664
|
-
last_audit_score: score
|
|
853
|
+
last_audit_at: now,
|
|
854
|
+
last_audit_score: score,
|
|
855
|
+
last_scheduled_audit_at: now,
|
|
856
|
+
scheduled_audit_source: "cli"
|
|
665
857
|
}).eq("id", projectId);
|
|
666
858
|
return { projectId, auditId: audit2.id };
|
|
667
859
|
} catch (error) {
|
|
668
|
-
console.error(
|
|
860
|
+
console.error(import_chalk2.default.dim("Cloud sync failed"));
|
|
669
861
|
return null;
|
|
670
862
|
}
|
|
671
863
|
}
|
|
@@ -674,7 +866,7 @@ async function audit(options) {
|
|
|
674
866
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
675
867
|
const enableAI = options.ai || false;
|
|
676
868
|
const aiProvider = options.aiProvider || "openai";
|
|
677
|
-
const aiKey = options.aiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || "";
|
|
869
|
+
const aiKey = options.aiKey || (aiProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) || "";
|
|
678
870
|
const loggedIn = isLoggedIn();
|
|
679
871
|
if (!url) {
|
|
680
872
|
console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
|
|
@@ -686,8 +878,8 @@ async function audit(options) {
|
|
|
686
878
|
console.log(" --ai-key <key> API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)");
|
|
687
879
|
console.log(" --output json Output as JSON");
|
|
688
880
|
if (!loggedIn) {
|
|
689
|
-
console.log(
|
|
690
|
-
console.log(
|
|
881
|
+
console.log(import_chalk2.default.dim("\nTip: Log in to sync results to the web dashboard:"));
|
|
882
|
+
console.log(import_chalk2.default.dim(" rankcli login"));
|
|
691
883
|
}
|
|
692
884
|
console.log("");
|
|
693
885
|
return;
|
|
@@ -771,12 +963,17 @@ async function audit(options) {
|
|
|
771
963
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
772
964
|
}
|
|
773
965
|
} else if (enableAI && !aiKey) {
|
|
774
|
-
console.log(
|
|
775
|
-
console.log(
|
|
966
|
+
console.log(import_chalk2.default.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
|
|
967
|
+
console.log(import_chalk2.default.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
|
|
776
968
|
}
|
|
777
969
|
const totalTime = Date.now() - startTime;
|
|
778
970
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
971
|
+
const actualChecksRun = (primaryReport.summary?.passed || 0) + (primaryReport.summary?.errors || 0) + (primaryReport.summary?.warnings || 0) || 200;
|
|
779
972
|
if (options.output === "json") {
|
|
973
|
+
let syncResult = null;
|
|
974
|
+
if (loggedIn) {
|
|
975
|
+
syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
976
|
+
}
|
|
780
977
|
const jsonOutput = {
|
|
781
978
|
url,
|
|
782
979
|
score: primaryReport.healthScore?.overall || 0,
|
|
@@ -785,85 +982,106 @@ async function audit(options) {
|
|
|
785
982
|
framework: frameworkName,
|
|
786
983
|
issues: mergedIssues,
|
|
787
984
|
healthScores: primaryReport.healthScore,
|
|
788
|
-
aiAnalysis
|
|
985
|
+
aiAnalysis,
|
|
986
|
+
// Include cloud sync info if synced
|
|
987
|
+
...syncResult && {
|
|
988
|
+
projectId: syncResult.projectId,
|
|
989
|
+
auditId: syncResult.auditId,
|
|
990
|
+
dashboardUrl: `${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}`
|
|
991
|
+
}
|
|
789
992
|
};
|
|
790
993
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
791
994
|
} else {
|
|
792
|
-
console.log("\n" +
|
|
793
|
-
console.log(
|
|
794
|
-
console.log(
|
|
995
|
+
console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
|
|
996
|
+
console.log(import_chalk2.default.bold.cyan(` SEO AUDIT REPORT: ${url}`));
|
|
997
|
+
console.log(import_chalk2.default.bold("\u2550".repeat(60)));
|
|
795
998
|
const score = primaryReport.healthScore?.overall || 0;
|
|
796
|
-
const scoreColor = score >= 80 ?
|
|
999
|
+
const scoreColor = score >= 80 ? import_chalk2.default.green : score >= 60 ? import_chalk2.default.yellow : import_chalk2.default.red;
|
|
797
1000
|
console.log(`
|
|
798
|
-
${
|
|
799
|
-
console.log(` ${
|
|
800
|
-
console.log(` ${
|
|
801
|
-
console.log(` ${
|
|
1001
|
+
${import_chalk2.default.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
|
|
1002
|
+
console.log(` ${import_chalk2.default.bold("Pages audited:")} ${successCount}`);
|
|
1003
|
+
console.log(` ${import_chalk2.default.bold("Issues found:")} ${mergedIssues.length}`);
|
|
1004
|
+
console.log(` ${import_chalk2.default.bold("Time:")} ${totalTime}ms`);
|
|
802
1005
|
if (frameworkName) {
|
|
803
|
-
console.log(` ${
|
|
1006
|
+
console.log(` ${import_chalk2.default.bold("Framework:")} ${import_chalk2.default.cyan(frameworkName)}`);
|
|
804
1007
|
}
|
|
805
1008
|
if (primaryReport.healthScore) {
|
|
806
|
-
console.log("\n" +
|
|
1009
|
+
console.log("\n" + import_chalk2.default.bold(" Health Scores:"));
|
|
807
1010
|
const hs = primaryReport.healthScore;
|
|
808
1011
|
const bar = (score2) => {
|
|
809
1012
|
const filled = Math.round(score2 / 5);
|
|
810
1013
|
const empty = 20 - filled;
|
|
811
|
-
const color = score2 >= 80 ?
|
|
812
|
-
return color("\u2588".repeat(filled)) +
|
|
1014
|
+
const color = score2 >= 80 ? import_chalk2.default.green : score2 >= 60 ? import_chalk2.default.yellow : import_chalk2.default.red;
|
|
1015
|
+
return color("\u2588".repeat(filled)) + import_chalk2.default.gray("\u2591".repeat(empty));
|
|
813
1016
|
};
|
|
814
1017
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
815
1018
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
816
1019
|
console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
|
|
817
1020
|
console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
|
|
818
1021
|
console.log(` Security: ${bar(hs.security || 0)} ${hs.security || 0}%`);
|
|
1022
|
+
console.log(` Local SEO: ${bar(hs.localSeo || 0)} ${hs.localSeo || 0}%`);
|
|
1023
|
+
console.log(` AI Readiness: ${bar(hs.aiReadiness || 0)} ${hs.aiReadiness || 0}%`);
|
|
819
1024
|
}
|
|
820
1025
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
821
1026
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
822
1027
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
823
|
-
console.log("\n" +
|
|
824
|
-
console.log(` ${
|
|
825
|
-
console.log(` ${
|
|
826
|
-
console.log(` ${
|
|
827
|
-
console.log("\n" +
|
|
1028
|
+
console.log("\n" + import_chalk2.default.bold(" Issues by Severity:"));
|
|
1029
|
+
console.log(` ${import_chalk2.default.red("\u25CF")} Errors: ${errors.length}`);
|
|
1030
|
+
console.log(` ${import_chalk2.default.yellow("\u25CF")} Warnings: ${warnings.length}`);
|
|
1031
|
+
console.log(` ${import_chalk2.default.blue("\u25CF")} Notices: ${notices.length}`);
|
|
1032
|
+
console.log("\n" + import_chalk2.default.bold(" Top Issues:"));
|
|
828
1033
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
829
1034
|
topIssues.forEach((issue, i) => {
|
|
830
|
-
const icon = issue.severity === "error" ?
|
|
1035
|
+
const icon = issue.severity === "error" ? import_chalk2.default.red("\u2717") : issue.severity === "warning" ? import_chalk2.default.yellow("!") : import_chalk2.default.blue("i");
|
|
831
1036
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
832
1037
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
833
|
-
console.log(
|
|
1038
|
+
console.log(import_chalk2.default.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
834
1039
|
}
|
|
835
1040
|
});
|
|
836
1041
|
if (aiAnalysis) {
|
|
837
|
-
console.log("\n" +
|
|
1042
|
+
console.log("\n" + import_chalk2.default.bold(" AI Analysis:"));
|
|
838
1043
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
839
|
-
console.log("\n " +
|
|
1044
|
+
console.log("\n " + import_chalk2.default.bold("Suggestions:"));
|
|
840
1045
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
841
1046
|
console.log(` ${i + 1}. ${s}`);
|
|
842
1047
|
});
|
|
843
1048
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
844
|
-
console.log("\n " +
|
|
845
|
-
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((
|
|
846
|
-
console.log(` ${i + 1}. [P${
|
|
847
|
-
console.log(
|
|
1049
|
+
console.log("\n " + import_chalk2.default.bold("Priority Fixes:"));
|
|
1050
|
+
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1051
|
+
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1052
|
+
console.log(import_chalk2.default.dim(` ${fix2.rationale}`));
|
|
848
1053
|
});
|
|
849
1054
|
}
|
|
850
1055
|
}
|
|
851
|
-
console.log("\n" +
|
|
1056
|
+
console.log("\n" + import_chalk2.default.bold("\u2550".repeat(60)));
|
|
1057
|
+
const userInfo = getUserInfo();
|
|
1058
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1059
|
+
const isFreeTier = !loggedIn || tier === "free";
|
|
1060
|
+
const isSoloOrBelow = isFreeTier || tier === "solo";
|
|
1061
|
+
if (isFreeTier) {
|
|
1062
|
+
const checksRun = mergedIssues.length > 0 ? loggedIn ? 100 : 50 : 0;
|
|
1063
|
+
showUpsell("checks_limit", {
|
|
1064
|
+
tier: loggedIn ? "free" : "free_anon",
|
|
1065
|
+
current: checksRun
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (options.geo && isFreeTier) {
|
|
1069
|
+
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1070
|
+
}
|
|
852
1071
|
if (loggedIn) {
|
|
853
1072
|
const syncSpinner = (0, import_ora.default)("Syncing to cloud...").start();
|
|
854
|
-
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis);
|
|
1073
|
+
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
855
1074
|
if (syncResult) {
|
|
856
1075
|
syncSpinner.succeed("Results synced to cloud");
|
|
857
|
-
console.log(
|
|
1076
|
+
console.log(import_chalk2.default.cyan(`
|
|
858
1077
|
View and manage issues in the web dashboard:`));
|
|
859
|
-
console.log(
|
|
1078
|
+
console.log(import_chalk2.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
860
1079
|
`));
|
|
861
1080
|
} else {
|
|
862
1081
|
syncSpinner.warn("Could not sync to cloud");
|
|
863
1082
|
}
|
|
864
1083
|
} else {
|
|
865
|
-
|
|
866
|
-
console.log(import_chalk.default.dim(" rankcli login\n"));
|
|
1084
|
+
showUpsell("free_audit_complete");
|
|
867
1085
|
}
|
|
868
1086
|
}
|
|
869
1087
|
} catch (error) {
|
|
@@ -882,19 +1100,19 @@ var SEVERITY_ICON = {
|
|
|
882
1100
|
warning: "\u26A0\uFE0F",
|
|
883
1101
|
info: "\u2139\uFE0F"
|
|
884
1102
|
};
|
|
885
|
-
function printFix(
|
|
886
|
-
const icon = SEVERITY_ICON[
|
|
1103
|
+
function printFix(fix2, index) {
|
|
1104
|
+
const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
|
|
887
1105
|
console.log(`
|
|
888
|
-
${index + 1}. ${icon} ${
|
|
889
|
-
console.log(` File: ${
|
|
890
|
-
if (
|
|
891
|
-
console.log(` Status: SKIPPED - ${
|
|
1106
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1107
|
+
console.log(` File: ${fix2.file}`);
|
|
1108
|
+
if (fix2.skipped) {
|
|
1109
|
+
console.log(` Status: SKIPPED - ${fix2.skipReason}`);
|
|
892
1110
|
} else {
|
|
893
|
-
console.log(` Explanation: ${
|
|
894
|
-
if (
|
|
895
|
-
console.log(` Before: ${truncate(
|
|
1111
|
+
console.log(` Explanation: ${fix2.explanation}`);
|
|
1112
|
+
if (fix2.before) {
|
|
1113
|
+
console.log(` Before: ${truncate(fix2.before, 60)}`);
|
|
896
1114
|
}
|
|
897
|
-
console.log(` After: ${truncate(
|
|
1115
|
+
console.log(` After: ${truncate(fix2.after, 100)}`);
|
|
898
1116
|
}
|
|
899
1117
|
}
|
|
900
1118
|
function truncate(str, maxLen) {
|
|
@@ -930,40 +1148,37 @@ async function apply(options) {
|
|
|
930
1148
|
}
|
|
931
1149
|
console.log("\n\u{1F680} SEO Autopilot - Apply Fixes");
|
|
932
1150
|
console.log("\u2550".repeat(50));
|
|
1151
|
+
const loggedIn = isLoggedIn();
|
|
1152
|
+
const userInfo = getUserInfo();
|
|
1153
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1154
|
+
const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
|
|
1155
|
+
if (!hasAutoFix) {
|
|
1156
|
+
showUpsell("auto_fix_blocked");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
933
1159
|
console.log("\n\u{1F50D} Detecting framework...");
|
|
934
1160
|
const frameworkResult = await (0, import_agent_runtime2.detectFramework)({ cwd });
|
|
935
1161
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
936
1162
|
console.log(` Framework: ${framework.name}`);
|
|
937
|
-
const isPaidUser = isLoggedIn();
|
|
938
1163
|
console.log(`
|
|
939
1164
|
\u{1F50D} Analyzing ${url}...`);
|
|
940
|
-
|
|
941
|
-
console.log(" Running full audit (200+ checks)...");
|
|
942
|
-
} else {
|
|
943
|
-
console.log(" Running basic analysis (login for full 200+ checks)");
|
|
944
|
-
}
|
|
1165
|
+
console.log(" Running full audit (200+ checks)...");
|
|
945
1166
|
let issues;
|
|
946
1167
|
let score;
|
|
947
1168
|
let auditResult = null;
|
|
948
1169
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
score = auditResult.score;
|
|
962
|
-
} else {
|
|
963
|
-
const analysis = await (0, import_agent_runtime2.runDirectAnalysis)(url);
|
|
964
|
-
issues = analysis.issues;
|
|
965
|
-
score = analysis.score;
|
|
966
|
-
}
|
|
1170
|
+
auditResult = await (0, import_agent_runtime2.runAuditWithFixes)({
|
|
1171
|
+
url,
|
|
1172
|
+
generateFixes: true
|
|
1173
|
+
});
|
|
1174
|
+
issues = auditResult.report.issues.map((issue) => ({
|
|
1175
|
+
severity: issue.severity,
|
|
1176
|
+
category: issue.category,
|
|
1177
|
+
code: issue.code,
|
|
1178
|
+
message: issue.title,
|
|
1179
|
+
impact: issue.description || void 0
|
|
1180
|
+
}));
|
|
1181
|
+
score = auditResult.score;
|
|
967
1182
|
} catch (error) {
|
|
968
1183
|
console.log(`
|
|
969
1184
|
\u274C Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}
|
|
@@ -1023,15 +1238,15 @@ async function apply(options) {
|
|
|
1023
1238
|
if (applied.length > 0) {
|
|
1024
1239
|
console.log(`
|
|
1025
1240
|
\u2705 Applied ${applied.length} fix(es):`);
|
|
1026
|
-
for (const
|
|
1027
|
-
console.log(` - ${
|
|
1241
|
+
for (const fix2 of applied) {
|
|
1242
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1028
1243
|
}
|
|
1029
1244
|
}
|
|
1030
1245
|
if (skipped.length > 0) {
|
|
1031
1246
|
console.log(`
|
|
1032
1247
|
\u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
|
|
1033
|
-
for (const
|
|
1034
|
-
console.log(` - ${
|
|
1248
|
+
for (const fix2 of skipped) {
|
|
1249
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1035
1250
|
}
|
|
1036
1251
|
}
|
|
1037
1252
|
if (config2 && !config2.url) {
|
|
@@ -1051,10 +1266,788 @@ async function apply(options) {
|
|
|
1051
1266
|
console.log("");
|
|
1052
1267
|
}
|
|
1053
1268
|
|
|
1054
|
-
// src/commands/
|
|
1269
|
+
// src/commands/fix.ts
|
|
1270
|
+
var import_child_process = require("child_process");
|
|
1271
|
+
var import_util = require("util");
|
|
1055
1272
|
var import_agent_runtime3 = require("@rankcli/agent-runtime");
|
|
1056
1273
|
var import_ora2 = __toESM(require("ora"));
|
|
1057
|
-
var
|
|
1274
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
1275
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
1276
|
+
var SEVERITY_ICON2 = {
|
|
1277
|
+
critical: "\u274C",
|
|
1278
|
+
error: "\u274C",
|
|
1279
|
+
warning: "\u26A0\uFE0F",
|
|
1280
|
+
notice: "\u2139\uFE0F",
|
|
1281
|
+
info: "\u2139\uFE0F"
|
|
1282
|
+
};
|
|
1283
|
+
function generateCommitMessage(fixes, url) {
|
|
1284
|
+
const applied = fixes.filter((f) => f.applied && !f.skipped);
|
|
1285
|
+
if (applied.length === 0) {
|
|
1286
|
+
return "";
|
|
1287
|
+
}
|
|
1288
|
+
const isAddingNew = applied.some(
|
|
1289
|
+
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1290
|
+
);
|
|
1291
|
+
const type = isAddingNew ? "feat" : "fix";
|
|
1292
|
+
const categories = new Set(applied.map((f) => f.issue.category || "seo"));
|
|
1293
|
+
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1294
|
+
const issueCount = applied.length;
|
|
1295
|
+
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1296
|
+
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
1297
|
+
const body = `Fixed:
|
|
1298
|
+
${fixList}
|
|
1299
|
+
|
|
1300
|
+
URL: ${url}
|
|
1301
|
+
Generated by: rankcli fix`;
|
|
1302
|
+
return `${title}
|
|
1303
|
+
|
|
1304
|
+
${body}`;
|
|
1305
|
+
}
|
|
1306
|
+
async function executeGitCommit(cwd, commitMessage, push) {
|
|
1307
|
+
try {
|
|
1308
|
+
await execAsync("git add -A", { cwd });
|
|
1309
|
+
const escapedMessage = commitMessage.replace(/"/g, '\\"');
|
|
1310
|
+
await execAsync(`git commit -m "${escapedMessage}"`, { cwd });
|
|
1311
|
+
let pushed = false;
|
|
1312
|
+
if (push) {
|
|
1313
|
+
try {
|
|
1314
|
+
await execAsync("git push", { cwd });
|
|
1315
|
+
pushed = true;
|
|
1316
|
+
} catch (pushErr) {
|
|
1317
|
+
return {
|
|
1318
|
+
committed: true,
|
|
1319
|
+
pushed: false,
|
|
1320
|
+
error: `Push failed: ${pushErr instanceof Error ? pushErr.message : "Unknown error"}`
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return { committed: true, pushed };
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
return {
|
|
1327
|
+
committed: false,
|
|
1328
|
+
pushed: false,
|
|
1329
|
+
error: err instanceof Error ? err.message : "Git operation failed"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function truncate2(str, maxLen) {
|
|
1334
|
+
const oneLine = str.replace(/\n/g, " ").trim();
|
|
1335
|
+
if (oneLine.length <= maxLen) return oneLine;
|
|
1336
|
+
return oneLine.substring(0, maxLen - 3) + "...";
|
|
1337
|
+
}
|
|
1338
|
+
function printFix2(fix2, index) {
|
|
1339
|
+
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1340
|
+
console.log(`
|
|
1341
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1342
|
+
console.log(` Code: ${import_chalk3.default.dim(fix2.issue.code)}`);
|
|
1343
|
+
console.log(` File: ${import_chalk3.default.cyan(fix2.file)}`);
|
|
1344
|
+
if (fix2.skipped) {
|
|
1345
|
+
console.log(` Status: ${import_chalk3.default.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1346
|
+
} else if (fix2.applied) {
|
|
1347
|
+
console.log(` Status: ${import_chalk3.default.green("APPLIED")}`);
|
|
1348
|
+
console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1349
|
+
} else {
|
|
1350
|
+
console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1351
|
+
if (fix2.before) {
|
|
1352
|
+
console.log(` ${import_chalk3.default.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1353
|
+
}
|
|
1354
|
+
console.log(` ${import_chalk3.default.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
async function fix(options) {
|
|
1358
|
+
const startTime = Date.now();
|
|
1359
|
+
const cwd = options.cwd || process.cwd();
|
|
1360
|
+
const url = options.url;
|
|
1361
|
+
if (!url) {
|
|
1362
|
+
const error = "Please provide a URL to fix:\n rankcli fix --url https://your-site.com";
|
|
1363
|
+
if (options.output !== "json") {
|
|
1364
|
+
console.log(`
|
|
1365
|
+
\u26A0\uFE0F ${error}
|
|
1366
|
+
`);
|
|
1367
|
+
}
|
|
1368
|
+
return { success: false, error };
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
new URL(url);
|
|
1372
|
+
} catch {
|
|
1373
|
+
const error = "Invalid URL format. Please provide a valid URL.";
|
|
1374
|
+
if (options.output !== "json") {
|
|
1375
|
+
console.log(`
|
|
1376
|
+
\u274C ${error}
|
|
1377
|
+
`);
|
|
1378
|
+
}
|
|
1379
|
+
return { success: false, error };
|
|
1380
|
+
}
|
|
1381
|
+
const loggedIn = isLoggedIn();
|
|
1382
|
+
const userInfo = getUserInfo();
|
|
1383
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1384
|
+
const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
|
|
1385
|
+
if (!options.dryRun && !hasAutoFix) {
|
|
1386
|
+
if (options.output !== "json") {
|
|
1387
|
+
showUpsell("auto_fix_blocked");
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
if (options.output !== "json") {
|
|
1395
|
+
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1396
|
+
console.log("\u2550".repeat(50));
|
|
1397
|
+
}
|
|
1398
|
+
const frameworkSpinner = options.output !== "json" ? (0, import_ora2.default)("Detecting framework...").start() : null;
|
|
1399
|
+
const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
|
|
1400
|
+
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1401
|
+
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1402
|
+
const analyzeSpinner = options.output !== "json" ? (0, import_ora2.default)(`Analyzing ${url}...`).start() : null;
|
|
1403
|
+
let issues;
|
|
1404
|
+
let score;
|
|
1405
|
+
try {
|
|
1406
|
+
const analysis = await (0, import_agent_runtime3.runDirectAnalysis)({ url });
|
|
1407
|
+
issues = analysis.issues;
|
|
1408
|
+
score = analysis.score;
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1411
|
+
analyzeSpinner?.fail(`Analysis failed: ${errorMsg}`);
|
|
1412
|
+
return { success: false, error: `Analysis failed: ${errorMsg}` };
|
|
1413
|
+
}
|
|
1414
|
+
analyzeSpinner?.succeed(`Score: ${score}/100, Issues: ${issues.length}`);
|
|
1415
|
+
if (options.only && options.only.length > 0) {
|
|
1416
|
+
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1417
|
+
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1418
|
+
if (options.output !== "json") {
|
|
1419
|
+
console.log(import_chalk3.default.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (issues.length === 0) {
|
|
1423
|
+
if (options.output !== "json") {
|
|
1424
|
+
console.log("\n\u2705 No issues found! Your site looks great.\n");
|
|
1425
|
+
}
|
|
1426
|
+
return {
|
|
1427
|
+
success: true,
|
|
1428
|
+
url,
|
|
1429
|
+
dryRun: options.dryRun,
|
|
1430
|
+
fixes: [],
|
|
1431
|
+
appliedCount: 0,
|
|
1432
|
+
skippedCount: 0,
|
|
1433
|
+
durationMs: Date.now() - startTime
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
const fixSpinner = options.output !== "json" ? (0, import_ora2.default)("Generating fixes...").start() : null;
|
|
1437
|
+
const fixes = await (0, import_agent_runtime3.generateFixes)(issues, {
|
|
1438
|
+
cwd,
|
|
1439
|
+
url,
|
|
1440
|
+
framework
|
|
1441
|
+
});
|
|
1442
|
+
const actionableFixes = fixes.filter((f) => !f.skipped);
|
|
1443
|
+
const skippedFixes = fixes.filter((f) => f.skipped);
|
|
1444
|
+
fixSpinner?.succeed(`Generated ${actionableFixes.length} fix(es), ${skippedFixes.length} skipped`);
|
|
1445
|
+
if (options.dryRun) {
|
|
1446
|
+
if (options.output !== "json") {
|
|
1447
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1448
|
+
console.log(import_chalk3.default.yellow("DRY RUN - No changes will be made"));
|
|
1449
|
+
console.log("\u2500".repeat(50));
|
|
1450
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1451
|
+
printFix2(fixes[i], i);
|
|
1452
|
+
}
|
|
1453
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1454
|
+
console.log(`\u{1F4CB} ${import_chalk3.default.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1455
|
+
console.log(` Remove --dry-run to apply these fixes.
|
|
1456
|
+
`);
|
|
1457
|
+
}
|
|
1458
|
+
const result2 = {
|
|
1459
|
+
success: true,
|
|
1460
|
+
url,
|
|
1461
|
+
dryRun: true,
|
|
1462
|
+
fixes,
|
|
1463
|
+
appliedCount: 0,
|
|
1464
|
+
skippedCount: skippedFixes.length,
|
|
1465
|
+
durationMs: Date.now() - startTime
|
|
1466
|
+
};
|
|
1467
|
+
if (options.output === "json") {
|
|
1468
|
+
result2.json = JSON.stringify({
|
|
1469
|
+
url,
|
|
1470
|
+
dryRun: true,
|
|
1471
|
+
fixes: fixes.map((f) => ({
|
|
1472
|
+
code: f.issue.code,
|
|
1473
|
+
message: f.issue.message,
|
|
1474
|
+
file: f.file,
|
|
1475
|
+
skipped: f.skipped,
|
|
1476
|
+
skipReason: f.skipReason
|
|
1477
|
+
})),
|
|
1478
|
+
appliedCount: 0,
|
|
1479
|
+
skippedCount: skippedFixes.length,
|
|
1480
|
+
durationMs: result2.durationMs
|
|
1481
|
+
}, null, 2);
|
|
1482
|
+
}
|
|
1483
|
+
return result2;
|
|
1484
|
+
}
|
|
1485
|
+
if (actionableFixes.length === 0) {
|
|
1486
|
+
if (options.output !== "json") {
|
|
1487
|
+
console.log("\n\u26A0\uFE0F No automatic fixes available. Please address issues manually.\n");
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
success: true,
|
|
1491
|
+
url,
|
|
1492
|
+
fixes,
|
|
1493
|
+
appliedCount: 0,
|
|
1494
|
+
skippedCount: skippedFixes.length,
|
|
1495
|
+
durationMs: Date.now() - startTime
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
if (!options.auto) {
|
|
1499
|
+
if (options.output !== "json") {
|
|
1500
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1501
|
+
console.log("PROPOSED FIXES:");
|
|
1502
|
+
console.log("\u2500".repeat(50));
|
|
1503
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1504
|
+
printFix2(fixes[i], i);
|
|
1505
|
+
}
|
|
1506
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1507
|
+
console.log(`Ready to apply ${actionableFixes.length} fix(es).`);
|
|
1508
|
+
console.log("Run with --auto to apply without confirmation.");
|
|
1509
|
+
console.log("Run with --dry-run to preview changes.\n");
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
success: true,
|
|
1513
|
+
url,
|
|
1514
|
+
fixes,
|
|
1515
|
+
appliedCount: 0,
|
|
1516
|
+
skippedCount: skippedFixes.length,
|
|
1517
|
+
durationMs: Date.now() - startTime
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const applySpinner = options.output !== "json" ? (0, import_ora2.default)("Applying fixes...").start() : null;
|
|
1521
|
+
const { applied, skipped } = await (0, import_agent_runtime3.applyFixes)(fixes, { cwd, dryRun: false });
|
|
1522
|
+
for (const fix2 of applied) {
|
|
1523
|
+
fix2.applied = true;
|
|
1524
|
+
}
|
|
1525
|
+
applySpinner?.succeed(`Applied ${applied.length} fix(es)`);
|
|
1526
|
+
const commitMessage = options.commit ? generateCommitMessage(fixes, url) : void 0;
|
|
1527
|
+
let committed = false;
|
|
1528
|
+
let pushed = false;
|
|
1529
|
+
let commitError;
|
|
1530
|
+
let pushError;
|
|
1531
|
+
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1532
|
+
const gitSpinner = options.output !== "json" ? (0, import_ora2.default)("Committing changes...").start() : null;
|
|
1533
|
+
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1534
|
+
committed = gitResult.committed;
|
|
1535
|
+
pushed = gitResult.pushed;
|
|
1536
|
+
if (gitResult.error) {
|
|
1537
|
+
if (gitResult.error.includes("Push")) {
|
|
1538
|
+
pushError = gitResult.error;
|
|
1539
|
+
gitSpinner?.warn("Committed but push failed");
|
|
1540
|
+
} else {
|
|
1541
|
+
commitError = gitResult.error;
|
|
1542
|
+
gitSpinner?.warn(`Commit failed: ${gitResult.error}`);
|
|
1543
|
+
}
|
|
1544
|
+
} else {
|
|
1545
|
+
if (pushed) {
|
|
1546
|
+
gitSpinner?.succeed("Committed and pushed");
|
|
1547
|
+
} else {
|
|
1548
|
+
gitSpinner?.succeed("Committed");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (options.output !== "json") {
|
|
1553
|
+
console.log("\n" + "\u2550".repeat(50));
|
|
1554
|
+
console.log("RESULTS");
|
|
1555
|
+
console.log("\u2550".repeat(50));
|
|
1556
|
+
if (applied.length > 0) {
|
|
1557
|
+
console.log(`
|
|
1558
|
+
${import_chalk3.default.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1559
|
+
for (const fix2 of applied) {
|
|
1560
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (skipped.length > 0) {
|
|
1564
|
+
console.log(`
|
|
1565
|
+
${import_chalk3.default.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1566
|
+
for (const fix2 of skipped) {
|
|
1567
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (committed) {
|
|
1571
|
+
console.log(`
|
|
1572
|
+
${import_chalk3.default.green("\u2705")} Changes committed`);
|
|
1573
|
+
if (pushed) {
|
|
1574
|
+
console.log(`${import_chalk3.default.green("\u2705")} Changes pushed`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (!committed && applied.length > 0) {
|
|
1578
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1579
|
+
console.log("NEXT STEPS:");
|
|
1580
|
+
console.log("\u2500".repeat(50));
|
|
1581
|
+
console.log("1. Review the changes:");
|
|
1582
|
+
console.log(" git diff");
|
|
1583
|
+
console.log("\n2. Commit the SEO improvements:");
|
|
1584
|
+
console.log(` git add -A && git commit -m "feat(seo): fix ${applied.length} SEO issues"`);
|
|
1585
|
+
console.log("\n3. Re-run the audit to verify:");
|
|
1586
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1587
|
+
}
|
|
1588
|
+
console.log("");
|
|
1589
|
+
}
|
|
1590
|
+
const result = {
|
|
1591
|
+
success: true,
|
|
1592
|
+
url,
|
|
1593
|
+
dryRun: false,
|
|
1594
|
+
fixes,
|
|
1595
|
+
appliedCount: applied.length,
|
|
1596
|
+
skippedCount: skipped.length,
|
|
1597
|
+
durationMs: Date.now() - startTime,
|
|
1598
|
+
commitMessage,
|
|
1599
|
+
committed,
|
|
1600
|
+
pushed,
|
|
1601
|
+
commitError,
|
|
1602
|
+
pushError
|
|
1603
|
+
};
|
|
1604
|
+
if (options.output === "json") {
|
|
1605
|
+
result.json = JSON.stringify({
|
|
1606
|
+
url,
|
|
1607
|
+
fixes: fixes.map((f) => ({
|
|
1608
|
+
code: f.issue.code,
|
|
1609
|
+
message: f.issue.message,
|
|
1610
|
+
file: f.file,
|
|
1611
|
+
applied: f.applied,
|
|
1612
|
+
skipped: f.skipped,
|
|
1613
|
+
skipReason: f.skipReason
|
|
1614
|
+
})),
|
|
1615
|
+
appliedCount: applied.length,
|
|
1616
|
+
skippedCount: skipped.length,
|
|
1617
|
+
durationMs: result.durationMs,
|
|
1618
|
+
committed,
|
|
1619
|
+
pushed,
|
|
1620
|
+
commitMessage
|
|
1621
|
+
}, null, 2);
|
|
1622
|
+
}
|
|
1623
|
+
return result;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/commands/track.ts
|
|
1627
|
+
var import_ora3 = __toESM(require("ora"));
|
|
1628
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1629
|
+
var import_table = require("table");
|
|
1630
|
+
var import_agent_runtime4 = require("@rankcli/agent-runtime");
|
|
1631
|
+
async function getProjectForUrl(url) {
|
|
1632
|
+
const supabase = await getAuthenticatedClient();
|
|
1633
|
+
if (!supabase) return null;
|
|
1634
|
+
const { userId } = getUserInfo();
|
|
1635
|
+
if (!userId) return null;
|
|
1636
|
+
const parsedUrl = new URL(url);
|
|
1637
|
+
const domain = parsedUrl.hostname.replace(/^www\./, "");
|
|
1638
|
+
const { data, error } = await supabase.from("projects").select("id, domain, last_rank_check_at").eq("user_id", userId).eq("domain", domain).single();
|
|
1639
|
+
if (error || !data) {
|
|
1640
|
+
const { data: newProject, error: createError } = await supabase.from("projects").insert({
|
|
1641
|
+
user_id: userId,
|
|
1642
|
+
name: domain,
|
|
1643
|
+
url,
|
|
1644
|
+
domain
|
|
1645
|
+
}).select("id, domain, last_rank_check_at").single();
|
|
1646
|
+
if (createError || !newProject) return null;
|
|
1647
|
+
return {
|
|
1648
|
+
id: newProject.id,
|
|
1649
|
+
domain: newProject.domain,
|
|
1650
|
+
lastRankCheckAt: newProject.last_rank_check_at ? new Date(newProject.last_rank_check_at) : null
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
id: data.id,
|
|
1655
|
+
domain: data.domain,
|
|
1656
|
+
lastRankCheckAt: data.last_rank_check_at ? new Date(data.last_rank_check_at) : null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function getTierLimits(tier) {
|
|
1660
|
+
const limits = import_agent_runtime4.TIER_LIMITS[tier] || import_agent_runtime4.TIER_LIMITS.free;
|
|
1661
|
+
return {
|
|
1662
|
+
maxKeywords: limits.maxKeywords,
|
|
1663
|
+
checksPerDay: limits.checksPerDay
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function formatPosition(pos) {
|
|
1667
|
+
if (pos === null) return import_chalk4.default.dim("N/A");
|
|
1668
|
+
if (pos <= 3) return import_chalk4.default.green.bold(`#${pos}`);
|
|
1669
|
+
if (pos <= 10) return import_chalk4.default.green(`#${pos}`);
|
|
1670
|
+
if (pos <= 20) return import_chalk4.default.yellow(`#${pos}`);
|
|
1671
|
+
if (pos <= 50) return import_chalk4.default.red(`#${pos}`);
|
|
1672
|
+
return import_chalk4.default.red.dim(`#${pos}`);
|
|
1673
|
+
}
|
|
1674
|
+
function formatChange(change) {
|
|
1675
|
+
if (change === 0) return import_chalk4.default.dim("\u2192");
|
|
1676
|
+
if (change > 0) return import_chalk4.default.green(`\u2191${change}`);
|
|
1677
|
+
return import_chalk4.default.red(`\u2193${Math.abs(change)}`);
|
|
1678
|
+
}
|
|
1679
|
+
async function trackAdd(options) {
|
|
1680
|
+
if (!options.keywords || options.keywords.length === 0) {
|
|
1681
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1682
|
+
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1683
|
+
return { success: false, error: "No keywords provided" };
|
|
1684
|
+
}
|
|
1685
|
+
if (!options.url) {
|
|
1686
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1687
|
+
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1688
|
+
return { success: false, error: "No URL provided" };
|
|
1689
|
+
}
|
|
1690
|
+
if (!isLoggedIn()) {
|
|
1691
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1692
|
+
console.log(" rankcli login\n");
|
|
1693
|
+
return { success: false, error: "Not logged in" };
|
|
1694
|
+
}
|
|
1695
|
+
const spinner = (0, import_ora3.default)("Adding keywords to track...").start();
|
|
1696
|
+
try {
|
|
1697
|
+
const project = await getProjectForUrl(options.url);
|
|
1698
|
+
if (!project) {
|
|
1699
|
+
spinner.fail("Could not find or create project");
|
|
1700
|
+
return { success: false, error: "Project not found" };
|
|
1701
|
+
}
|
|
1702
|
+
const { planName } = getUserInfo();
|
|
1703
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1704
|
+
const { maxKeywords } = getTierLimits(tier);
|
|
1705
|
+
const supabase = await getAuthenticatedClient();
|
|
1706
|
+
if (!supabase) {
|
|
1707
|
+
spinner.fail("Could not connect to database");
|
|
1708
|
+
return { success: false, error: "Database connection failed" };
|
|
1709
|
+
}
|
|
1710
|
+
const { count } = await supabase.from("keywords").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("is_active", true);
|
|
1711
|
+
const existingCount = count || 0;
|
|
1712
|
+
const newCount = options.keywords.length;
|
|
1713
|
+
if (existingCount + newCount > maxKeywords) {
|
|
1714
|
+
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1715
|
+
showUpsell("keyword_limit", { tier, current: existingCount, limit: maxKeywords });
|
|
1716
|
+
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1717
|
+
}
|
|
1718
|
+
const keywordRecords = options.keywords.map((keyword) => ({
|
|
1719
|
+
project_id: project.id,
|
|
1720
|
+
keyword: keyword.toLowerCase().trim(),
|
|
1721
|
+
search_engine: options.searchEngine || "google",
|
|
1722
|
+
country: options.country || "US",
|
|
1723
|
+
language: options.language || "en",
|
|
1724
|
+
is_active: true
|
|
1725
|
+
}));
|
|
1726
|
+
const { data, error } = await supabase.from("keywords").upsert(keywordRecords, {
|
|
1727
|
+
onConflict: "project_id,keyword",
|
|
1728
|
+
ignoreDuplicates: false
|
|
1729
|
+
}).select();
|
|
1730
|
+
if (error) {
|
|
1731
|
+
spinner.fail(`Failed to add keywords: ${error.message}`);
|
|
1732
|
+
return { success: false, error: error.message };
|
|
1733
|
+
}
|
|
1734
|
+
const addedCount = data?.length || 0;
|
|
1735
|
+
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1736
|
+
console.log(import_chalk4.default.dim(`
|
|
1737
|
+
Tracking: ${options.keywords.join(", ")}`));
|
|
1738
|
+
console.log(import_chalk4.default.dim(`For: ${project.domain}`));
|
|
1739
|
+
console.log(import_chalk4.default.cyan(`
|
|
1740
|
+
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1741
|
+
`));
|
|
1742
|
+
return { success: true, addedCount };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
spinner.fail("Failed to add keywords");
|
|
1745
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function trackStatus(options) {
|
|
1749
|
+
if (!options.url) {
|
|
1750
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1751
|
+
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1752
|
+
return { success: false, error: "No URL provided" };
|
|
1753
|
+
}
|
|
1754
|
+
if (!isLoggedIn()) {
|
|
1755
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1756
|
+
console.log(" rankcli login\n");
|
|
1757
|
+
return { success: false, error: "Not logged in" };
|
|
1758
|
+
}
|
|
1759
|
+
const spinner = (0, import_ora3.default)("Loading keyword rankings...").start();
|
|
1760
|
+
try {
|
|
1761
|
+
const project = await getProjectForUrl(options.url);
|
|
1762
|
+
if (!project) {
|
|
1763
|
+
spinner.fail("Project not found");
|
|
1764
|
+
return { success: false, error: "Project not found" };
|
|
1765
|
+
}
|
|
1766
|
+
const supabase = await getAuthenticatedClient();
|
|
1767
|
+
if (!supabase) {
|
|
1768
|
+
spinner.fail("Could not connect to database");
|
|
1769
|
+
return { success: false, error: "Database connection failed" };
|
|
1770
|
+
}
|
|
1771
|
+
const { data, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true).order("current_position", { ascending: true, nullsFirst: false });
|
|
1772
|
+
if (error) {
|
|
1773
|
+
spinner.fail(`Failed to load keywords: ${error.message}`);
|
|
1774
|
+
return { success: false, error: error.message };
|
|
1775
|
+
}
|
|
1776
|
+
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1777
|
+
if (!data || data.length === 0) {
|
|
1778
|
+
console.log(import_chalk4.default.dim("\nNo keywords tracked yet."));
|
|
1779
|
+
console.log(import_chalk4.default.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
|
|
1780
|
+
`));
|
|
1781
|
+
return { success: true, keywords: [] };
|
|
1782
|
+
}
|
|
1783
|
+
const keywords2 = data.map((row) => ({
|
|
1784
|
+
keyword: row.keyword,
|
|
1785
|
+
currentPosition: row.current_position,
|
|
1786
|
+
previousPosition: row.previous_position,
|
|
1787
|
+
bestPosition: row.best_position,
|
|
1788
|
+
change: (row.previous_position || 0) - (row.current_position || 0),
|
|
1789
|
+
lastChecked: row.last_checked ? new Date(row.last_checked) : null
|
|
1790
|
+
}));
|
|
1791
|
+
if (options.output === "json") {
|
|
1792
|
+
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1793
|
+
} else {
|
|
1794
|
+
console.log("\n" + import_chalk4.default.bold("\u{1F4CA} Keyword Rankings"));
|
|
1795
|
+
console.log("\u2550".repeat(60));
|
|
1796
|
+
const tableData = [
|
|
1797
|
+
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
1798
|
+
...keywords2.map((kw) => [
|
|
1799
|
+
kw.keyword,
|
|
1800
|
+
formatPosition(kw.currentPosition),
|
|
1801
|
+
formatPosition(kw.bestPosition),
|
|
1802
|
+
formatChange(kw.change),
|
|
1803
|
+
kw.lastChecked ? kw.lastChecked.toLocaleDateString() : "Never"
|
|
1804
|
+
])
|
|
1805
|
+
];
|
|
1806
|
+
console.log((0, import_table.table)(tableData, {
|
|
1807
|
+
border: {
|
|
1808
|
+
topBody: "\u2500",
|
|
1809
|
+
topJoin: "\u252C",
|
|
1810
|
+
topLeft: "\u250C",
|
|
1811
|
+
topRight: "\u2510",
|
|
1812
|
+
bottomBody: "\u2500",
|
|
1813
|
+
bottomJoin: "\u2534",
|
|
1814
|
+
bottomLeft: "\u2514",
|
|
1815
|
+
bottomRight: "\u2518",
|
|
1816
|
+
bodyLeft: "\u2502",
|
|
1817
|
+
bodyRight: "\u2502",
|
|
1818
|
+
bodyJoin: "\u2502",
|
|
1819
|
+
joinBody: "\u2500",
|
|
1820
|
+
joinLeft: "\u251C",
|
|
1821
|
+
joinRight: "\u2524",
|
|
1822
|
+
joinJoin: "\u253C"
|
|
1823
|
+
}
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
return { success: true, keywords: keywords2 };
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
spinner.fail("Failed to load rankings");
|
|
1829
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
async function trackHistory(options) {
|
|
1833
|
+
if (!options.url) {
|
|
1834
|
+
return { success: false, error: "No URL provided" };
|
|
1835
|
+
}
|
|
1836
|
+
if (!options.keyword) {
|
|
1837
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
1838
|
+
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
1839
|
+
return { success: false, error: "No keyword provided" };
|
|
1840
|
+
}
|
|
1841
|
+
if (!isLoggedIn()) {
|
|
1842
|
+
return { success: false, error: "Not logged in" };
|
|
1843
|
+
}
|
|
1844
|
+
const spinner = (0, import_ora3.default)(`Loading history for "${options.keyword}"...`).start();
|
|
1845
|
+
try {
|
|
1846
|
+
const project = await getProjectForUrl(options.url);
|
|
1847
|
+
if (!project) {
|
|
1848
|
+
spinner.fail("Project not found");
|
|
1849
|
+
return { success: false, error: "Project not found" };
|
|
1850
|
+
}
|
|
1851
|
+
const supabase = await getAuthenticatedClient();
|
|
1852
|
+
if (!supabase) {
|
|
1853
|
+
spinner.fail("Could not connect to database");
|
|
1854
|
+
return { success: false, error: "Database connection failed" };
|
|
1855
|
+
}
|
|
1856
|
+
const { data: keywordData } = await supabase.from("keywords").select("id, keyword").eq("project_id", project.id).eq("keyword", options.keyword.toLowerCase().trim()).single();
|
|
1857
|
+
if (!keywordData) {
|
|
1858
|
+
spinner.fail("Keyword not found");
|
|
1859
|
+
return { success: false, error: "Keyword not found" };
|
|
1860
|
+
}
|
|
1861
|
+
const days = options.days || 30;
|
|
1862
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
1863
|
+
const { data: historyData, error } = await supabase.from("rankings").select("position, url, recorded_at").eq("keyword_id", keywordData.id).gte("recorded_at", cutoff.toISOString()).order("recorded_at", { ascending: false });
|
|
1864
|
+
if (error) {
|
|
1865
|
+
spinner.fail(`Failed to load history: ${error.message}`);
|
|
1866
|
+
return { success: false, error: error.message };
|
|
1867
|
+
}
|
|
1868
|
+
spinner.succeed(`Loaded ${historyData?.length || 0} data points`);
|
|
1869
|
+
const history = (historyData || []).map((row) => ({
|
|
1870
|
+
position: row.position,
|
|
1871
|
+
url: row.url,
|
|
1872
|
+
recordedAt: new Date(row.recorded_at)
|
|
1873
|
+
}));
|
|
1874
|
+
if (options.output === "json") {
|
|
1875
|
+
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
1876
|
+
} else {
|
|
1877
|
+
console.log("\n" + import_chalk4.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
1878
|
+
console.log("\u2550".repeat(50));
|
|
1879
|
+
if (history.length === 0) {
|
|
1880
|
+
console.log(import_chalk4.default.dim("\nNo history available yet.\n"));
|
|
1881
|
+
} else {
|
|
1882
|
+
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
1883
|
+
const sparkline = positions.map((p) => {
|
|
1884
|
+
if (p <= 10) return import_chalk4.default.green("\u2588");
|
|
1885
|
+
if (p <= 20) return import_chalk4.default.yellow("\u2593");
|
|
1886
|
+
if (p <= 50) return import_chalk4.default.red("\u2592");
|
|
1887
|
+
return import_chalk4.default.dim("\u2591");
|
|
1888
|
+
}).join("");
|
|
1889
|
+
console.log(`
|
|
1890
|
+
${sparkline} (last ${positions.length} checks)`);
|
|
1891
|
+
console.log(import_chalk4.default.dim(" \u2190 newer older \u2192\n"));
|
|
1892
|
+
const recent = history.slice(0, 10);
|
|
1893
|
+
for (const entry of recent) {
|
|
1894
|
+
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return { success: true, history };
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
spinner.fail("Failed to load history");
|
|
1901
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async function trackExport(options) {
|
|
1905
|
+
if (!options.url) {
|
|
1906
|
+
return { success: false, error: "No URL provided" };
|
|
1907
|
+
}
|
|
1908
|
+
if (!isLoggedIn()) {
|
|
1909
|
+
return { success: false, error: "Not logged in" };
|
|
1910
|
+
}
|
|
1911
|
+
const spinner = (0, import_ora3.default)("Exporting data...").start();
|
|
1912
|
+
try {
|
|
1913
|
+
const project = await getProjectForUrl(options.url);
|
|
1914
|
+
if (!project) {
|
|
1915
|
+
spinner.fail("Project not found");
|
|
1916
|
+
return { success: false, error: "Project not found" };
|
|
1917
|
+
}
|
|
1918
|
+
const supabase = await getAuthenticatedClient();
|
|
1919
|
+
if (!supabase) {
|
|
1920
|
+
spinner.fail("Could not connect to database");
|
|
1921
|
+
return { success: false, error: "Database connection failed" };
|
|
1922
|
+
}
|
|
1923
|
+
const { data: keywords2, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
1924
|
+
if (error) {
|
|
1925
|
+
spinner.fail(`Failed to export: ${error.message}`);
|
|
1926
|
+
return { success: false, error: error.message };
|
|
1927
|
+
}
|
|
1928
|
+
const format = options.format || "csv";
|
|
1929
|
+
if (format === "json") {
|
|
1930
|
+
const data = JSON.stringify({
|
|
1931
|
+
domain: project.domain,
|
|
1932
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1933
|
+
keywords: keywords2?.map((kw) => ({
|
|
1934
|
+
keyword: kw.keyword,
|
|
1935
|
+
currentPosition: kw.current_position,
|
|
1936
|
+
bestPosition: kw.best_position,
|
|
1937
|
+
lastChecked: kw.last_checked
|
|
1938
|
+
}))
|
|
1939
|
+
}, null, 2);
|
|
1940
|
+
spinner.succeed("Exported as JSON");
|
|
1941
|
+
console.log(data);
|
|
1942
|
+
return { success: true, data };
|
|
1943
|
+
} else {
|
|
1944
|
+
const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
|
|
1945
|
+
for (const kw of keywords2 || []) {
|
|
1946
|
+
const trend = (kw.previous_position || 0) - (kw.current_position || 0);
|
|
1947
|
+
const trendStr = trend > 0 ? "\u2191" : trend < 0 ? "\u2193" : "\u2192";
|
|
1948
|
+
rows.push([
|
|
1949
|
+
`"${kw.keyword}"`,
|
|
1950
|
+
kw.current_position?.toString() || "N/A",
|
|
1951
|
+
kw.best_position?.toString() || "N/A",
|
|
1952
|
+
kw.last_checked || "Never",
|
|
1953
|
+
trendStr
|
|
1954
|
+
].join(","));
|
|
1955
|
+
}
|
|
1956
|
+
const data = rows.join("\n");
|
|
1957
|
+
spinner.succeed("Exported as CSV");
|
|
1958
|
+
console.log(data);
|
|
1959
|
+
return { success: true, data };
|
|
1960
|
+
}
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
spinner.fail("Failed to export");
|
|
1963
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function trackCheck(options) {
|
|
1967
|
+
if (!options.url) {
|
|
1968
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1969
|
+
console.log(" rankcli track check --url https://your-site.com\n");
|
|
1970
|
+
return { success: false, error: "No URL provided" };
|
|
1971
|
+
}
|
|
1972
|
+
if (!isLoggedIn()) {
|
|
1973
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
1974
|
+
console.log(" rankcli login\n");
|
|
1975
|
+
return { success: false, error: "Not logged in" };
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
const project = await getProjectForUrl(options.url);
|
|
1979
|
+
if (!project) {
|
|
1980
|
+
return { success: false, error: "Project not found" };
|
|
1981
|
+
}
|
|
1982
|
+
const { planName } = getUserInfo();
|
|
1983
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1984
|
+
const { checksPerDay } = getTierLimits(tier);
|
|
1985
|
+
if (!options.force && project.lastRankCheckAt) {
|
|
1986
|
+
const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
|
|
1987
|
+
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
1988
|
+
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
1989
|
+
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
1990
|
+
console.log(import_chalk4.default.yellow(`
|
|
1991
|
+
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
1992
|
+
console.log(import_chalk4.default.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
1993
|
+
`));
|
|
1994
|
+
return { success: true, rateLimited: true };
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const spinner = (0, import_ora3.default)("Checking current rankings...").start();
|
|
1998
|
+
const supabase = await getAuthenticatedClient();
|
|
1999
|
+
if (!supabase) {
|
|
2000
|
+
spinner.fail("Could not connect to database");
|
|
2001
|
+
return { success: false, error: "Database connection failed" };
|
|
2002
|
+
}
|
|
2003
|
+
const { data: keywords2 } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
2004
|
+
if (!keywords2 || keywords2.length === 0) {
|
|
2005
|
+
spinner.fail("No keywords to check");
|
|
2006
|
+
return { success: false, error: "No keywords tracked" };
|
|
2007
|
+
}
|
|
2008
|
+
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2009
|
+
if (!serpApiKey) {
|
|
2010
|
+
spinner.warn("No SERP API key configured");
|
|
2011
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
|
|
2012
|
+
console.log(import_chalk4.default.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
|
|
2013
|
+
return { success: false, error: "No SERP API key" };
|
|
2014
|
+
}
|
|
2015
|
+
const tracker = new import_agent_runtime4.RankTracker({
|
|
2016
|
+
supabase,
|
|
2017
|
+
serpConfig: {
|
|
2018
|
+
provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
|
|
2019
|
+
apiKey: serpApiKey
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2023
|
+
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2024
|
+
console.log("\n" + import_chalk4.default.bold("\u{1F50D} Rank Check Results"));
|
|
2025
|
+
console.log("\u2550".repeat(50));
|
|
2026
|
+
for (const result of results) {
|
|
2027
|
+
const posStr = formatPosition(result.position);
|
|
2028
|
+
console.log(` ${result.keyword}: ${posStr}`);
|
|
2029
|
+
if (result.serpFeatures.length > 0) {
|
|
2030
|
+
console.log(import_chalk4.default.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
console.log("");
|
|
2034
|
+
return {
|
|
2035
|
+
success: true,
|
|
2036
|
+
checkResults: results.map((r) => ({
|
|
2037
|
+
keyword: r.keyword,
|
|
2038
|
+
position: r.position,
|
|
2039
|
+
url: r.url
|
|
2040
|
+
}))
|
|
2041
|
+
};
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/commands/geo.ts
|
|
2048
|
+
var import_agent_runtime5 = require("@rankcli/agent-runtime");
|
|
2049
|
+
var import_ora4 = __toESM(require("ora"));
|
|
2050
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1058
2051
|
async function fetchResources(url) {
|
|
1059
2052
|
const headers = {};
|
|
1060
2053
|
let html = null;
|
|
@@ -1100,7 +2093,7 @@ async function geo(options) {
|
|
|
1100
2093
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1101
2094
|
return;
|
|
1102
2095
|
}
|
|
1103
|
-
const spinner = (0,
|
|
2096
|
+
const spinner = (0, import_ora4.default)("Fetching page and robots.txt...").start();
|
|
1104
2097
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
1105
2098
|
if (!html) {
|
|
1106
2099
|
spinner.fail("Could not fetch page content");
|
|
@@ -1108,11 +2101,11 @@ async function geo(options) {
|
|
|
1108
2101
|
}
|
|
1109
2102
|
spinner.text = "Running GEO analysis...";
|
|
1110
2103
|
try {
|
|
1111
|
-
const geoResult = await
|
|
1112
|
-
const cwvResult =
|
|
1113
|
-
const securityResult =
|
|
1114
|
-
const schemaResult =
|
|
1115
|
-
const mobileResult =
|
|
2104
|
+
const geoResult = await import_agent_runtime5.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
|
|
2105
|
+
const cwvResult = import_agent_runtime5.analyzers.analyzeCoreWebVitals(html, url, headers);
|
|
2106
|
+
const securityResult = import_agent_runtime5.analyzers.analyzeSecurityHeaders(headers, url);
|
|
2107
|
+
const schemaResult = import_agent_runtime5.analyzers.analyzeStructuredData(html, url);
|
|
2108
|
+
const mobileResult = import_agent_runtime5.analyzers.analyzeMobileSEO(html, url);
|
|
1116
2109
|
spinner.succeed("GEO analysis complete");
|
|
1117
2110
|
if (options.output === "json") {
|
|
1118
2111
|
const jsonOutput = {
|
|
@@ -1126,55 +2119,55 @@ async function geo(options) {
|
|
|
1126
2119
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1127
2120
|
return;
|
|
1128
2121
|
}
|
|
1129
|
-
console.log("\n" +
|
|
1130
|
-
console.log(
|
|
1131
|
-
console.log(
|
|
2122
|
+
console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
|
|
2123
|
+
console.log(import_chalk5.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2124
|
+
console.log(import_chalk5.default.bold("\u2550".repeat(60)));
|
|
1132
2125
|
const geoScore = geoResult.score;
|
|
1133
|
-
const scoreColor = geoScore >= 80 ?
|
|
2126
|
+
const scoreColor = geoScore >= 80 ? import_chalk5.default.green : geoScore >= 60 ? import_chalk5.default.yellow : import_chalk5.default.red;
|
|
1134
2127
|
console.log(`
|
|
1135
|
-
${
|
|
1136
|
-
console.log("\n" +
|
|
2128
|
+
${import_chalk5.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2129
|
+
console.log("\n" + import_chalk5.default.bold(" AI Crawler Access:"));
|
|
1137
2130
|
const { aiCrawlerAccess } = geoResult;
|
|
1138
2131
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
1139
|
-
console.log(
|
|
2132
|
+
console.log(import_chalk5.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
1140
2133
|
}
|
|
1141
2134
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
1142
2135
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
1143
|
-
console.log(
|
|
2136
|
+
console.log(import_chalk5.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
1144
2137
|
}
|
|
1145
|
-
console.log("\n" +
|
|
2138
|
+
console.log("\n" + import_chalk5.default.bold(" Rendering:"));
|
|
1146
2139
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
1147
|
-
console.log(
|
|
2140
|
+
console.log(import_chalk5.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
1148
2141
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
1149
|
-
console.log(
|
|
2142
|
+
console.log(import_chalk5.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
1150
2143
|
}
|
|
1151
|
-
console.log("\n" +
|
|
2144
|
+
console.log("\n" + import_chalk5.default.bold(" LLM Friendliness:"));
|
|
1152
2145
|
const { llmSignals } = geoResult;
|
|
1153
2146
|
const bar = (score) => {
|
|
1154
2147
|
const filled = Math.round(score / 5);
|
|
1155
2148
|
const empty = 20 - filled;
|
|
1156
|
-
const color = score >= 70 ?
|
|
1157
|
-
return color("\u2588".repeat(filled)) +
|
|
2149
|
+
const color = score >= 70 ? import_chalk5.default.green : score >= 40 ? import_chalk5.default.yellow : import_chalk5.default.red;
|
|
2150
|
+
return color("\u2588".repeat(filled)) + import_chalk5.default.gray("\u2591".repeat(empty));
|
|
1158
2151
|
};
|
|
1159
2152
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
1160
2153
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
1161
2154
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
1162
2155
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
1163
|
-
console.log("\n" +
|
|
2156
|
+
console.log("\n" + import_chalk5.default.bold(" Content Structure:"));
|
|
1164
2157
|
const { contentStructure } = geoResult;
|
|
1165
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
1166
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
1167
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
1168
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
1169
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
1170
|
-
console.log("\n" +
|
|
2158
|
+
console.log(` ${contentStructure.hasStructuredData ? import_chalk5.default.green("\u2705") : import_chalk5.default.red("\u274C")} JSON-LD Structured Data`);
|
|
2159
|
+
console.log(` ${contentStructure.hasFAQSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} FAQ Schema`);
|
|
2160
|
+
console.log(` ${contentStructure.hasArticleSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Article Schema`);
|
|
2161
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Breadcrumbs`);
|
|
2162
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk5.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk5.default.yellow("Needs Work") : import_chalk5.default.red("Poor")}`);
|
|
2163
|
+
console.log("\n" + import_chalk5.default.bold(" Citation Readiness:"));
|
|
1171
2164
|
const { citationReadiness } = geoResult;
|
|
1172
2165
|
if (citationReadiness.trustSignals.length > 0) {
|
|
1173
|
-
console.log(
|
|
2166
|
+
console.log(import_chalk5.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
1174
2167
|
} else {
|
|
1175
|
-
console.log(
|
|
2168
|
+
console.log(import_chalk5.default.yellow(" \u25CB No trust signals detected"));
|
|
1176
2169
|
}
|
|
1177
|
-
console.log("\n" +
|
|
2170
|
+
console.log("\n" + import_chalk5.default.bold(" Other Scores:"));
|
|
1178
2171
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
1179
2172
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
1180
2173
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -1188,18 +2181,18 @@ async function geo(options) {
|
|
|
1188
2181
|
];
|
|
1189
2182
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
1190
2183
|
if (criticalIssues.length > 0) {
|
|
1191
|
-
console.log("\n" +
|
|
2184
|
+
console.log("\n" + import_chalk5.default.bold.red(" Critical Issues:"));
|
|
1192
2185
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
1193
|
-
console.log(
|
|
2186
|
+
console.log(import_chalk5.default.red(` ${i + 1}. ${issue.title}`));
|
|
1194
2187
|
});
|
|
1195
2188
|
}
|
|
1196
2189
|
if (geoResult.recommendations.length > 0) {
|
|
1197
|
-
console.log("\n" +
|
|
2190
|
+
console.log("\n" + import_chalk5.default.bold(" Recommendations:"));
|
|
1198
2191
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
1199
2192
|
console.log(` ${i + 1}. ${rec}`);
|
|
1200
2193
|
});
|
|
1201
2194
|
}
|
|
1202
|
-
console.log("\n" +
|
|
2195
|
+
console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
|
|
1203
2196
|
console.log("");
|
|
1204
2197
|
} catch (error) {
|
|
1205
2198
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1208,7 +2201,7 @@ async function geo(options) {
|
|
|
1208
2201
|
|
|
1209
2202
|
// src/commands/keywords.ts
|
|
1210
2203
|
var readline = __toESM(require("readline"));
|
|
1211
|
-
var
|
|
2204
|
+
var import_agent_runtime6 = require("@rankcli/agent-runtime");
|
|
1212
2205
|
async function keywords(options) {
|
|
1213
2206
|
console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
|
|
1214
2207
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -1217,7 +2210,7 @@ async function keywords(options) {
|
|
|
1217
2210
|
input: process.stdin,
|
|
1218
2211
|
output: process.stdout
|
|
1219
2212
|
});
|
|
1220
|
-
const
|
|
2213
|
+
const ask2 = (question) => {
|
|
1221
2214
|
return new Promise((resolve) => {
|
|
1222
2215
|
rl.question(question, (answer) => {
|
|
1223
2216
|
resolve(answer.trim());
|
|
@@ -1232,7 +2225,7 @@ ${question}`);
|
|
|
1232
2225
|
console.log(` ${choice.description}`);
|
|
1233
2226
|
});
|
|
1234
2227
|
while (true) {
|
|
1235
|
-
const answer = await
|
|
2228
|
+
const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
|
|
1236
2229
|
const num = parseInt(answer, 10);
|
|
1237
2230
|
if (num >= 1 && num <= choices.length) {
|
|
1238
2231
|
return choices[num - 1].value;
|
|
@@ -1243,7 +2236,7 @@ ${question}`);
|
|
|
1243
2236
|
try {
|
|
1244
2237
|
let url = options.url;
|
|
1245
2238
|
if (!url) {
|
|
1246
|
-
url = await
|
|
2239
|
+
url = await ask2("Enter your website URL: ");
|
|
1247
2240
|
}
|
|
1248
2241
|
try {
|
|
1249
2242
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
@@ -1259,7 +2252,7 @@ ${question}`);
|
|
|
1259
2252
|
seedKeywords = options.seed.split(",").map((s) => s.trim());
|
|
1260
2253
|
} else if (options.auto) {
|
|
1261
2254
|
console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
|
|
1262
|
-
seedKeywords = await (0,
|
|
2255
|
+
seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
|
|
1263
2256
|
if (seedKeywords.length > 0) {
|
|
1264
2257
|
console.log(`Found: ${seedKeywords.join(", ")}`);
|
|
1265
2258
|
}
|
|
@@ -1267,7 +2260,7 @@ ${question}`);
|
|
|
1267
2260
|
if (seedKeywords.length === 0) {
|
|
1268
2261
|
console.log("\n\u{1F4DD} What is your website/product about?");
|
|
1269
2262
|
console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
|
|
1270
|
-
const seedInput = await
|
|
2263
|
+
const seedInput = await ask2(" > ");
|
|
1271
2264
|
seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1272
2265
|
if (seedKeywords.length === 0) {
|
|
1273
2266
|
console.log("\n\u274C Please provide at least one seed keyword.\n");
|
|
@@ -1284,7 +2277,7 @@ ${question}`);
|
|
|
1284
2277
|
contentCapacity: "medium",
|
|
1285
2278
|
targetGeo: "us"
|
|
1286
2279
|
};
|
|
1287
|
-
for (const q of
|
|
2280
|
+
for (const q of import_agent_runtime6.SITE_PROFILE_QUESTIONS) {
|
|
1288
2281
|
const answer = await askChoice(q.question, q.options);
|
|
1289
2282
|
siteProfile[q.id] = answer;
|
|
1290
2283
|
}
|
|
@@ -1303,13 +2296,13 @@ ${question}`);
|
|
|
1303
2296
|
url,
|
|
1304
2297
|
maxKeywords: 100
|
|
1305
2298
|
};
|
|
1306
|
-
const result = await (0,
|
|
1307
|
-
console.log((0,
|
|
2299
|
+
const result = await (0, import_agent_runtime6.runKeywordResearch)(researchOptions);
|
|
2300
|
+
console.log((0, import_agent_runtime6.formatKeywordReport)(result));
|
|
1308
2301
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
1309
2302
|
if (allKeywords.length > 5) {
|
|
1310
2303
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
1311
|
-
const topicResult = (0,
|
|
1312
|
-
console.log((0,
|
|
2304
|
+
const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
|
|
2305
|
+
console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
|
|
1313
2306
|
}
|
|
1314
2307
|
if (result.quickWins.length > 0) {
|
|
1315
2308
|
console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
|
|
@@ -1334,7 +2327,7 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
1334
2327
|
`);
|
|
1335
2328
|
} else {
|
|
1336
2329
|
console.log("\u{1F50D} Extracting keywords from your page...");
|
|
1337
|
-
seedKeywords = await (0,
|
|
2330
|
+
seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
|
|
1338
2331
|
if (seedKeywords.length === 0) {
|
|
1339
2332
|
console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
|
|
1340
2333
|
return;
|
|
@@ -1350,18 +2343,18 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
1350
2343
|
contentCapacity: "medium",
|
|
1351
2344
|
targetGeo: "us"
|
|
1352
2345
|
};
|
|
1353
|
-
const result = await (0,
|
|
2346
|
+
const result = await (0, import_agent_runtime6.runKeywordResearch)({
|
|
1354
2347
|
seedKeywords,
|
|
1355
2348
|
siteProfile,
|
|
1356
2349
|
url,
|
|
1357
2350
|
maxKeywords: 50
|
|
1358
2351
|
});
|
|
1359
|
-
console.log((0,
|
|
2352
|
+
console.log((0, import_agent_runtime6.formatKeywordReport)(result));
|
|
1360
2353
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
1361
2354
|
if (allKeywords.length > 5) {
|
|
1362
2355
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
1363
|
-
const topicResult = (0,
|
|
1364
|
-
console.log((0,
|
|
2356
|
+
const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
|
|
2357
|
+
console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
|
|
1365
2358
|
}
|
|
1366
2359
|
} catch (error) {
|
|
1367
2360
|
console.log(`
|
|
@@ -1424,7 +2417,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
1424
2417
|
return;
|
|
1425
2418
|
}
|
|
1426
2419
|
console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
|
|
1427
|
-
const result = await (0,
|
|
2420
|
+
const result = await (0, import_agent_runtime6.runAIKeywordResearch)({
|
|
1428
2421
|
url,
|
|
1429
2422
|
tier: "solo",
|
|
1430
2423
|
openaiApiKey,
|
|
@@ -1543,12 +2536,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
1543
2536
|
}
|
|
1544
2537
|
console.log("");
|
|
1545
2538
|
console.log("Analyzing competitor keywords (this may take a moment)...\n");
|
|
1546
|
-
const result = await (0,
|
|
2539
|
+
const result = await (0, import_agent_runtime6.discoverCompetitorKeywords)(
|
|
1547
2540
|
yourDomain,
|
|
1548
2541
|
seedKeywords,
|
|
1549
2542
|
competitors
|
|
1550
2543
|
);
|
|
1551
|
-
console.log((0,
|
|
2544
|
+
console.log((0, import_agent_runtime6.formatCompetitorReport)(result));
|
|
1552
2545
|
if (result.missingKeywords.length > 0) {
|
|
1553
2546
|
console.log("\n\u{1F3AF} ACTION ITEMS:");
|
|
1554
2547
|
console.log("\u2500".repeat(60));
|
|
@@ -1567,7 +2560,7 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
1567
2560
|
}
|
|
1568
2561
|
|
|
1569
2562
|
// src/commands/content.ts
|
|
1570
|
-
var
|
|
2563
|
+
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
1571
2564
|
async function analyzeContent(options) {
|
|
1572
2565
|
console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
|
|
1573
2566
|
const mode = options.mode || "full";
|
|
@@ -1577,12 +2570,12 @@ async function analyzeContent(options) {
|
|
|
1577
2570
|
console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
|
|
1578
2571
|
return;
|
|
1579
2572
|
}
|
|
1580
|
-
const analysis = (0,
|
|
1581
|
-
console.log((0,
|
|
2573
|
+
const analysis = (0, import_agent_runtime7.analyzeHeadline)(headline);
|
|
2574
|
+
console.log((0, import_agent_runtime7.formatHeadlineReport)(analysis));
|
|
1582
2575
|
if (options.keyword) {
|
|
1583
2576
|
console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
|
|
1584
2577
|
console.log("\u2500".repeat(60));
|
|
1585
|
-
const variations = (0,
|
|
2578
|
+
const variations = (0, import_agent_runtime7.generateHeadlineVariations)(options.keyword, [options.keyword]);
|
|
1586
2579
|
for (const v of variations.slice(0, 5)) {
|
|
1587
2580
|
console.log(` \u2022 ${v}`);
|
|
1588
2581
|
}
|
|
@@ -1593,7 +2586,7 @@ async function analyzeContent(options) {
|
|
|
1593
2586
|
if (options.url) {
|
|
1594
2587
|
console.log(`Fetching content from ${options.url}...`);
|
|
1595
2588
|
try {
|
|
1596
|
-
const result = await (0,
|
|
2589
|
+
const result = await (0, import_agent_runtime7.crawlUrl)(options.url);
|
|
1597
2590
|
if (!result.html) {
|
|
1598
2591
|
console.log("\u274C Could not fetch content from URL");
|
|
1599
2592
|
return;
|
|
@@ -1607,15 +2600,15 @@ async function analyzeContent(options) {
|
|
|
1607
2600
|
Analyzing: "${title || options.url}"
|
|
1608
2601
|
`);
|
|
1609
2602
|
if (mode === "full" || mode === "readability") {
|
|
1610
|
-
const readability = (0,
|
|
1611
|
-
console.log((0,
|
|
2603
|
+
const readability = (0, import_agent_runtime7.analyzeReadability)(bodyText);
|
|
2604
|
+
console.log((0, import_agent_runtime7.formatReadabilityReport)(readability));
|
|
1612
2605
|
}
|
|
1613
2606
|
if (mode === "full" && title) {
|
|
1614
|
-
const headlineAnalysis = (0,
|
|
1615
|
-
console.log((0,
|
|
2607
|
+
const headlineAnalysis = (0, import_agent_runtime7.analyzeHeadline)(title);
|
|
2608
|
+
console.log((0, import_agent_runtime7.formatHeadlineReport)(headlineAnalysis));
|
|
1616
2609
|
}
|
|
1617
2610
|
if (options.keyword && (mode === "full" || mode === "density")) {
|
|
1618
|
-
const densityAnalysis = (0,
|
|
2611
|
+
const densityAnalysis = (0, import_agent_runtime7.analyzeKeywordDensity)(options.keyword, {
|
|
1619
2612
|
title,
|
|
1620
2613
|
h1,
|
|
1621
2614
|
headings,
|
|
@@ -1623,19 +2616,19 @@ Analyzing: "${title || options.url}"
|
|
|
1623
2616
|
firstParagraph: paragraphs[0],
|
|
1624
2617
|
lastParagraph: paragraphs[paragraphs.length - 1]
|
|
1625
2618
|
});
|
|
1626
|
-
console.log((0,
|
|
2619
|
+
console.log((0, import_agent_runtime7.formatKeywordDensityReport)(densityAnalysis));
|
|
1627
2620
|
}
|
|
1628
2621
|
if (options.keyword && (mode === "full" || mode === "snippet")) {
|
|
1629
|
-
const snippetAnalysis = (0,
|
|
2622
|
+
const snippetAnalysis = (0, import_agent_runtime7.analyzeFeaturedSnippetPotential)(
|
|
1630
2623
|
options.keyword,
|
|
1631
2624
|
bodyText,
|
|
1632
2625
|
headings
|
|
1633
2626
|
);
|
|
1634
|
-
console.log((0,
|
|
2627
|
+
console.log((0, import_agent_runtime7.formatFeaturedSnippetReport)(snippetAnalysis));
|
|
1635
2628
|
}
|
|
1636
2629
|
if (options.keyword) {
|
|
1637
|
-
const intentAnalysis = (0,
|
|
1638
|
-
console.log((0,
|
|
2630
|
+
const intentAnalysis = (0, import_agent_runtime7.classifyIntent)(options.keyword);
|
|
2631
|
+
console.log((0, import_agent_runtime7.formatIntentReport)(options.keyword, intentAnalysis));
|
|
1639
2632
|
}
|
|
1640
2633
|
console.log("\n\u{1F4CA} CONTENT SUMMARY");
|
|
1641
2634
|
console.log("\u2500".repeat(60));
|
|
@@ -1662,8 +2655,8 @@ Analyzing: "${title || options.url}"
|
|
|
1662
2655
|
}
|
|
1663
2656
|
async function analyzeIntent(keyword) {
|
|
1664
2657
|
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
1665
|
-
const analysis = (0,
|
|
1666
|
-
console.log((0,
|
|
2658
|
+
const analysis = (0, import_agent_runtime7.classifyIntent)(keyword);
|
|
2659
|
+
console.log((0, import_agent_runtime7.formatIntentReport)(keyword, analysis));
|
|
1667
2660
|
}
|
|
1668
2661
|
function extractBodyText(html) {
|
|
1669
2662
|
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
@@ -1682,7 +2675,7 @@ function extractBodyText(html) {
|
|
|
1682
2675
|
var readline2 = __toESM(require("readline"));
|
|
1683
2676
|
var fs = __toESM(require("fs"));
|
|
1684
2677
|
var path = __toESM(require("path"));
|
|
1685
|
-
var
|
|
2678
|
+
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
1686
2679
|
async function setup(options) {
|
|
1687
2680
|
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
1688
2681
|
const projectPath = process.cwd();
|
|
@@ -1708,7 +2701,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1708
2701
|
input: process.stdin,
|
|
1709
2702
|
output: process.stdout
|
|
1710
2703
|
});
|
|
1711
|
-
const
|
|
2704
|
+
const ask2 = (question) => {
|
|
1712
2705
|
return new Promise((resolve) => {
|
|
1713
2706
|
rl.question(question, (answer) => {
|
|
1714
2707
|
resolve(answer.trim());
|
|
@@ -1722,7 +2715,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1722
2715
|
console.log("GA4 tracks visitor behavior on your site.");
|
|
1723
2716
|
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
1724
2717
|
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
1725
|
-
const ga4Id = await
|
|
2718
|
+
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
1726
2719
|
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
1727
2720
|
await setupGA4(projectPath, ga4Id);
|
|
1728
2721
|
} else if (ga4Id) {
|
|
@@ -1735,7 +2728,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1735
2728
|
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
1736
2729
|
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
1737
2730
|
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
1738
|
-
const gscCode = await
|
|
2731
|
+
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
1739
2732
|
if (gscCode && gscCode.length > 10) {
|
|
1740
2733
|
await setupGSC(projectPath, gscCode);
|
|
1741
2734
|
} else if (gscCode) {
|
|
@@ -1747,20 +2740,20 @@ async function interactiveSetup(projectPath) {
|
|
|
1747
2740
|
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
1748
2741
|
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
1749
2742
|
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
1750
|
-
const setupAction = await
|
|
2743
|
+
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
1751
2744
|
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
1752
2745
|
console.log("\nHow often should the check run?");
|
|
1753
2746
|
console.log(" 1. Weekly (recommended for most sites)");
|
|
1754
2747
|
console.log(" 2. Daily (for high-traffic sites)");
|
|
1755
2748
|
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
1756
|
-
const scheduleChoice = await
|
|
2749
|
+
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
1757
2750
|
const scheduleMap = {
|
|
1758
2751
|
"1": "weekly",
|
|
1759
2752
|
"2": "daily",
|
|
1760
2753
|
"3": "monthly"
|
|
1761
2754
|
};
|
|
1762
2755
|
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
1763
|
-
const siteUrl = await
|
|
2756
|
+
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
1764
2757
|
if (siteUrl) {
|
|
1765
2758
|
await setupGitHubAction(projectPath, schedule, siteUrl);
|
|
1766
2759
|
}
|
|
@@ -1784,7 +2777,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1784
2777
|
}
|
|
1785
2778
|
async function setupGA4(projectPath, measurementId) {
|
|
1786
2779
|
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
1787
|
-
const result = await (0,
|
|
2780
|
+
const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
|
|
1788
2781
|
if (result.success) {
|
|
1789
2782
|
console.log(`\u2705 ${result.message}`);
|
|
1790
2783
|
if (result.file) {
|
|
@@ -1798,7 +2791,7 @@ async function setupGA4(projectPath, measurementId) {
|
|
|
1798
2791
|
}
|
|
1799
2792
|
}
|
|
1800
2793
|
const envExamplePath = path.join(projectPath, ".env.example");
|
|
1801
|
-
const envContent = (0,
|
|
2794
|
+
const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
|
|
1802
2795
|
if (fs.existsSync(envExamplePath)) {
|
|
1803
2796
|
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
1804
2797
|
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
@@ -1813,7 +2806,7 @@ async function setupGA4(projectPath, measurementId) {
|
|
|
1813
2806
|
}
|
|
1814
2807
|
async function setupGSC(projectPath, verificationCode) {
|
|
1815
2808
|
console.log("Setting up Google Search Console verification...");
|
|
1816
|
-
const result = await (0,
|
|
2809
|
+
const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
|
|
1817
2810
|
if (result.success) {
|
|
1818
2811
|
console.log(`\u2705 ${result.message}`);
|
|
1819
2812
|
if (result.file) {
|
|
@@ -1837,7 +2830,7 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
|
1837
2830
|
createPRs: true
|
|
1838
2831
|
}
|
|
1839
2832
|
};
|
|
1840
|
-
const { files, instructions } = (0,
|
|
2833
|
+
const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
|
|
1841
2834
|
console.log("\u2705 GitHub Action files created:");
|
|
1842
2835
|
for (const file of files) {
|
|
1843
2836
|
console.log(` - ${path.relative(projectPath, file)}`);
|
|
@@ -1845,34 +2838,199 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
|
1845
2838
|
console.log(instructions);
|
|
1846
2839
|
}
|
|
1847
2840
|
function showGSCInstructions() {
|
|
1848
|
-
console.log((0,
|
|
2841
|
+
console.log((0, import_agent_runtime8.getGSCSetupInstructions)());
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/commands/alerts.ts
|
|
2845
|
+
var readline3 = __toESM(require("readline"));
|
|
2846
|
+
var ALERT_TYPES = [
|
|
2847
|
+
{ id: "error", label: "Critical errors detected", default: true },
|
|
2848
|
+
{ id: "score_drop", label: "Score drops (configurable threshold)", default: true },
|
|
2849
|
+
{ id: "weekly_report", label: "Weekly summary reports", default: false },
|
|
2850
|
+
{ id: "pr_created", label: "Fix PRs created", default: true }
|
|
2851
|
+
];
|
|
2852
|
+
async function ask(rl, question) {
|
|
2853
|
+
return new Promise((resolve) => {
|
|
2854
|
+
rl.question(question, (answer) => {
|
|
2855
|
+
resolve(answer.trim());
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
2860
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
2861
|
+
const answer = await ask(rl, `${question} ${suffix}: `);
|
|
2862
|
+
if (answer === "") return defaultYes;
|
|
2863
|
+
return answer.toLowerCase().startsWith("y");
|
|
2864
|
+
}
|
|
2865
|
+
async function alerts(options) {
|
|
2866
|
+
console.log("\n\u{1F514} SEO Autopilot - Alert Configuration\n");
|
|
2867
|
+
const rl = readline3.createInterface({
|
|
2868
|
+
input: process.stdin,
|
|
2869
|
+
output: process.stdout
|
|
2870
|
+
});
|
|
2871
|
+
try {
|
|
2872
|
+
if (options.list) {
|
|
2873
|
+
console.log("\u{1F4CB} Current alert configurations:\n");
|
|
2874
|
+
console.log(" (Alert configs are stored in your RankCLI account)");
|
|
2875
|
+
console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
|
|
2876
|
+
rl.close();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (options.test && options.webhook) {
|
|
2880
|
+
await testWebhook(options.channel || "slack", options.webhook);
|
|
2881
|
+
rl.close();
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
console.log("Configure notifications for SEO audit events.\n");
|
|
2885
|
+
console.log("Supported channels:");
|
|
2886
|
+
console.log(" \u2022 Slack - Use an Incoming Webhook URL");
|
|
2887
|
+
console.log(" \u2022 Discord - Use a Channel Webhook URL\n");
|
|
2888
|
+
const channelChoice = await ask(rl, "Which channel? (slack/discord): ");
|
|
2889
|
+
const channel = channelChoice.toLowerCase();
|
|
2890
|
+
if (channel !== "slack" && channel !== "discord") {
|
|
2891
|
+
console.log('\n\u274C Invalid channel. Please choose "slack" or "discord".\n');
|
|
2892
|
+
rl.close();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
console.log("");
|
|
2896
|
+
if (channel === "slack") {
|
|
2897
|
+
console.log("To create a Slack webhook:");
|
|
2898
|
+
console.log("1. Go to https://api.slack.com/apps");
|
|
2899
|
+
console.log("2. Create an app (or use existing)");
|
|
2900
|
+
console.log('3. Enable "Incoming Webhooks"');
|
|
2901
|
+
console.log("4. Add a new webhook to your workspace");
|
|
2902
|
+
console.log("5. Copy the webhook URL\n");
|
|
2903
|
+
} else {
|
|
2904
|
+
console.log("To create a Discord webhook:");
|
|
2905
|
+
console.log("1. Open Discord channel settings");
|
|
2906
|
+
console.log("2. Go to Integrations \u2192 Webhooks");
|
|
2907
|
+
console.log("3. Create a new webhook");
|
|
2908
|
+
console.log("4. Copy the webhook URL\n");
|
|
2909
|
+
}
|
|
2910
|
+
const webhookUrl = await ask(rl, "Webhook URL: ");
|
|
2911
|
+
if (!webhookUrl.startsWith("https://")) {
|
|
2912
|
+
console.log("\n\u274C Invalid webhook URL. Must start with https://\n");
|
|
2913
|
+
rl.close();
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
console.log("\nWhich events should trigger alerts?\n");
|
|
2917
|
+
const selectedTypes = [];
|
|
2918
|
+
for (const alertType of ALERT_TYPES) {
|
|
2919
|
+
const enabled = await askYesNo(rl, ` ${alertType.label}`, alertType.default);
|
|
2920
|
+
if (enabled) {
|
|
2921
|
+
selectedTypes.push(alertType.id);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
let scoreDropThreshold = 10;
|
|
2925
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2926
|
+
const thresholdStr = await ask(rl, "\nMinimum score drop to alert (default: 10): ");
|
|
2927
|
+
if (thresholdStr) {
|
|
2928
|
+
const parsed = parseInt(thresholdStr, 10);
|
|
2929
|
+
if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
|
|
2930
|
+
scoreDropThreshold = parsed;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
console.log("\n\u{1F4E4} Testing webhook...");
|
|
2935
|
+
const testResult = await testWebhook(channel, webhookUrl);
|
|
2936
|
+
if (!testResult) {
|
|
2937
|
+
console.log("\u274C Webhook test failed. Please check the URL and try again.\n");
|
|
2938
|
+
rl.close();
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
console.log("\u2705 Webhook test successful!\n");
|
|
2942
|
+
console.log("\u{1F4DD} Alert configuration:");
|
|
2943
|
+
console.log(` Channel: ${channel}`);
|
|
2944
|
+
console.log(` Events: ${selectedTypes.join(", ")}`);
|
|
2945
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2946
|
+
console.log(` Score drop threshold: ${scoreDropThreshold} points`);
|
|
2947
|
+
}
|
|
2948
|
+
console.log("");
|
|
2949
|
+
console.log("To save this configuration:");
|
|
2950
|
+
console.log("1. Log in with `rankcli auth login`");
|
|
2951
|
+
console.log("2. Add alert config at https://rankcli.dev/account/alerts");
|
|
2952
|
+
console.log("");
|
|
2953
|
+
console.log("Or use the API:");
|
|
2954
|
+
console.log(` POST /api/alerts with webhook_url and settings`);
|
|
2955
|
+
console.log("");
|
|
2956
|
+
rl.close();
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
console.error("Error:", error);
|
|
2959
|
+
rl.close();
|
|
2960
|
+
process.exit(1);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
async function testWebhook(channel, webhookUrl) {
|
|
2964
|
+
const testPayload = channel === "slack" ? {
|
|
2965
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2966
|
+
blocks: [
|
|
2967
|
+
{
|
|
2968
|
+
type: "header",
|
|
2969
|
+
text: {
|
|
2970
|
+
type: "plain_text",
|
|
2971
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2972
|
+
emoji: true
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
type: "section",
|
|
2977
|
+
text: {
|
|
2978
|
+
type: "mrkdwn",
|
|
2979
|
+
text: "This is a test message from RankCLI. If you see this, your webhook is working correctly!"
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
]
|
|
2983
|
+
} : {
|
|
2984
|
+
embeds: [
|
|
2985
|
+
{
|
|
2986
|
+
title: "\u{1F514} RankCLI Alert Test",
|
|
2987
|
+
description: "This is a test message from RankCLI. If you see this, your webhook is working correctly!",
|
|
2988
|
+
color: 2450411,
|
|
2989
|
+
footer: {
|
|
2990
|
+
text: "\u{1F916} Sent by RankCLI"
|
|
2991
|
+
},
|
|
2992
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2993
|
+
}
|
|
2994
|
+
]
|
|
2995
|
+
};
|
|
2996
|
+
try {
|
|
2997
|
+
const response = await fetch(webhookUrl, {
|
|
2998
|
+
method: "POST",
|
|
2999
|
+
headers: { "Content-Type": "application/json" },
|
|
3000
|
+
body: JSON.stringify(testPayload)
|
|
3001
|
+
});
|
|
3002
|
+
return response.ok;
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
console.error("Webhook test error:", error);
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
1849
3007
|
}
|
|
1850
3008
|
|
|
1851
3009
|
// src/commands/auth.ts
|
|
1852
|
-
var
|
|
1853
|
-
var
|
|
3010
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
3011
|
+
var import_ora5 = __toESM(require("ora"));
|
|
1854
3012
|
var import_inquirer = __toESM(require("inquirer"));
|
|
1855
3013
|
var import_open = __toESM(require("open"));
|
|
1856
3014
|
async function login(options) {
|
|
1857
3015
|
if (isLoggedIn()) {
|
|
1858
3016
|
const { email } = getUserInfo();
|
|
1859
|
-
console.log(
|
|
1860
|
-
console.log(
|
|
3017
|
+
console.log(import_chalk6.default.yellow(`Already logged in as ${email}`));
|
|
3018
|
+
console.log(import_chalk6.default.dim('Run "rankcli logout" to sign out first.'));
|
|
1861
3019
|
return;
|
|
1862
3020
|
}
|
|
1863
|
-
const spinner = (0,
|
|
3021
|
+
const spinner = (0, import_ora5.default)();
|
|
1864
3022
|
try {
|
|
1865
3023
|
if (options.token) {
|
|
1866
3024
|
const apiKey = options.token;
|
|
1867
3025
|
if (!apiKey.startsWith("rankcli_")) {
|
|
1868
|
-
console.log(
|
|
3026
|
+
console.log(import_chalk6.default.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
1869
3027
|
return;
|
|
1870
3028
|
}
|
|
1871
3029
|
spinner.start("Validating API key...");
|
|
1872
3030
|
const result = await validateApiKey(apiKey);
|
|
1873
3031
|
if (!result.valid) {
|
|
1874
3032
|
spinner.fail("Invalid API key");
|
|
1875
|
-
console.log(
|
|
3033
|
+
console.log(import_chalk6.default.red(`
|
|
1876
3034
|
${result.error}`));
|
|
1877
3035
|
return;
|
|
1878
3036
|
}
|
|
@@ -1885,14 +3043,14 @@ ${result.error}`));
|
|
|
1885
3043
|
});
|
|
1886
3044
|
}
|
|
1887
3045
|
spinner.succeed(`Authenticated via API key`);
|
|
1888
|
-
console.log(
|
|
3046
|
+
console.log(import_chalk6.default.dim(`
|
|
1889
3047
|
Email: ${result.user?.email}`));
|
|
1890
|
-
console.log(
|
|
3048
|
+
console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
1891
3049
|
return;
|
|
1892
3050
|
}
|
|
1893
3051
|
if (options.browser) {
|
|
1894
|
-
console.log(
|
|
1895
|
-
console.log(
|
|
3052
|
+
console.log(import_chalk6.default.cyan("\nOpening browser for login..."));
|
|
3053
|
+
console.log(import_chalk6.default.dim("Complete the login in your browser, then return here.\n"));
|
|
1896
3054
|
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
1897
3055
|
await (0, import_open.default)(loginUrl);
|
|
1898
3056
|
const { token } = await import_inquirer.default.prompt([
|
|
@@ -1908,7 +3066,7 @@ Email: ${result.user?.email}`));
|
|
|
1908
3066
|
const { data, error } = await supabase.auth.getUser(token);
|
|
1909
3067
|
if (error || !data.user) {
|
|
1910
3068
|
spinner.fail("Invalid token");
|
|
1911
|
-
console.log(
|
|
3069
|
+
console.log(import_chalk6.default.red("The token is invalid or expired. Please try again."));
|
|
1912
3070
|
return;
|
|
1913
3071
|
}
|
|
1914
3072
|
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
@@ -1922,7 +3080,7 @@ Email: ${result.user?.email}`));
|
|
|
1922
3080
|
}
|
|
1923
3081
|
saveSession(sessionData.session);
|
|
1924
3082
|
await fetchAndSaveSubscription();
|
|
1925
|
-
spinner.succeed(`Logged in as ${
|
|
3083
|
+
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
|
|
1926
3084
|
} else {
|
|
1927
3085
|
let email = options.email;
|
|
1928
3086
|
if (!email) {
|
|
@@ -1955,11 +3113,11 @@ Email: ${result.user?.email}`));
|
|
|
1955
3113
|
});
|
|
1956
3114
|
if (error) {
|
|
1957
3115
|
spinner.fail("Login failed");
|
|
1958
|
-
console.log(
|
|
3116
|
+
console.log(import_chalk6.default.red(`
|
|
1959
3117
|
${error.message}`));
|
|
1960
3118
|
if (error.message.includes("Invalid login")) {
|
|
1961
|
-
console.log(
|
|
1962
|
-
console.log(
|
|
3119
|
+
console.log(import_chalk6.default.dim("\nDon't have an account? Sign up at:"));
|
|
3120
|
+
console.log(import_chalk6.default.cyan(` ${WEB_APP_URL}/signup`));
|
|
1963
3121
|
}
|
|
1964
3122
|
return;
|
|
1965
3123
|
}
|
|
@@ -1969,41 +3127,41 @@ ${error.message}`));
|
|
|
1969
3127
|
}
|
|
1970
3128
|
saveSession(data.session);
|
|
1971
3129
|
await fetchAndSaveSubscription();
|
|
1972
|
-
spinner.succeed(`Logged in as ${
|
|
3130
|
+
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
|
|
1973
3131
|
const { planName, sitesLimit } = getUserInfo();
|
|
1974
|
-
console.log(
|
|
3132
|
+
console.log(import_chalk6.default.dim(`
|
|
1975
3133
|
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
1976
3134
|
}
|
|
1977
3135
|
} catch (err) {
|
|
1978
3136
|
spinner.fail("Login failed");
|
|
1979
|
-
console.error(
|
|
3137
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
1980
3138
|
}
|
|
1981
3139
|
}
|
|
1982
3140
|
async function logout() {
|
|
1983
3141
|
if (!isLoggedIn()) {
|
|
1984
|
-
console.log(
|
|
3142
|
+
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
1985
3143
|
return;
|
|
1986
3144
|
}
|
|
1987
3145
|
const { email } = getUserInfo();
|
|
1988
|
-
const spinner = (0,
|
|
3146
|
+
const spinner = (0, import_ora5.default)("Signing out...").start();
|
|
1989
3147
|
try {
|
|
1990
3148
|
const supabase = getSupabaseClient();
|
|
1991
3149
|
await supabase.auth.signOut();
|
|
1992
3150
|
clearCredentials();
|
|
1993
|
-
spinner.succeed(`Logged out from ${
|
|
3151
|
+
spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
|
|
1994
3152
|
} catch (err) {
|
|
1995
3153
|
spinner.fail("Logout failed");
|
|
1996
|
-
console.error(
|
|
3154
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
1997
3155
|
}
|
|
1998
3156
|
}
|
|
1999
3157
|
async function whoami() {
|
|
2000
3158
|
if (!isLoggedIn()) {
|
|
2001
|
-
console.log(
|
|
2002
|
-
console.log(
|
|
2003
|
-
console.log(
|
|
3159
|
+
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3160
|
+
console.log(import_chalk6.default.dim('\nRun "rankcli login" to sign in.'));
|
|
3161
|
+
console.log(import_chalk6.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
2004
3162
|
return;
|
|
2005
3163
|
}
|
|
2006
|
-
const spinner = (0,
|
|
3164
|
+
const spinner = (0, import_ora5.default)("Checking account...").start();
|
|
2007
3165
|
try {
|
|
2008
3166
|
if (isApiKeyAuth()) {
|
|
2009
3167
|
const apiKey = getApiKey();
|
|
@@ -2023,18 +3181,18 @@ async function whoami() {
|
|
|
2023
3181
|
const { email, planName, sitesLimit } = getUserInfo();
|
|
2024
3182
|
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
2025
3183
|
spinner.stop();
|
|
2026
|
-
console.log(
|
|
3184
|
+
console.log(import_chalk6.default.bold("\nRankCLI Account"));
|
|
2027
3185
|
console.log("\u2500".repeat(40));
|
|
2028
|
-
console.log(` Email: ${
|
|
2029
|
-
console.log(` Plan: ${
|
|
3186
|
+
console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
|
|
3187
|
+
console.log(` Plan: ${import_chalk6.default.green(planName)}`);
|
|
2030
3188
|
console.log(` Sites: ${sitesLimit} allowed`);
|
|
2031
|
-
console.log(` Auth: ${
|
|
3189
|
+
console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
|
|
2032
3190
|
console.log("\u2500".repeat(40));
|
|
2033
|
-
console.log(
|
|
3191
|
+
console.log(import_chalk6.default.dim(`
|
|
2034
3192
|
Manage at: ${WEB_APP_URL}/account`));
|
|
2035
3193
|
} catch (err) {
|
|
2036
3194
|
spinner.fail("Failed to fetch account info");
|
|
2037
|
-
console.error(
|
|
3195
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2038
3196
|
}
|
|
2039
3197
|
}
|
|
2040
3198
|
async function fetchAndSaveSubscription() {
|
|
@@ -2066,7 +3224,7 @@ async function fetchAndSaveSubscription() {
|
|
|
2066
3224
|
}
|
|
2067
3225
|
|
|
2068
3226
|
// src/version.ts
|
|
2069
|
-
var VERSION = "0.0.
|
|
3227
|
+
var VERSION = "0.0.20";
|
|
2070
3228
|
|
|
2071
3229
|
// src/index.ts
|
|
2072
3230
|
var program = new import_commander.Command();
|
|
@@ -2078,6 +3236,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
|
|
|
2078
3236
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
2079
3237
|
program.command("audit").description("Run a comprehensive SEO audit (200+ checks, Ahrefs-level)").option("-u, --url <url>", "URL to audit").option("-o, --output <format>", "Output format (json, console)", "console").option("--check-links", "Check for broken internal/external links (slower)").option("--max-pages <n>", "Max pages to crawl (default: 5)").option("--ai", "Enable AI analysis").option("--ai-provider <provider>", "AI provider: openai or anthropic").option("--ai-key <key>", "API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)").action(audit);
|
|
2080
3238
|
program.command("apply").description("Analyze a live URL and apply SEO fixes to your codebase").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--dry-run", "Preview changes without applying").action(apply);
|
|
3239
|
+
program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and push").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--commit", "Commit changes with conventional commit message").option("--push", "Push changes after committing (requires --commit)").option("--dry-run", "Preview changes without applying").option("--only <codes>", "Fix only specific issue codes (comma-separated)", (val) => val.split(",")).option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3240
|
+
const result = await fix({
|
|
3241
|
+
url: options.url,
|
|
3242
|
+
auto: options.auto,
|
|
3243
|
+
commit: options.commit,
|
|
3244
|
+
push: options.push,
|
|
3245
|
+
dryRun: options.dryRun,
|
|
3246
|
+
only: options.only,
|
|
3247
|
+
output: options.output
|
|
3248
|
+
});
|
|
3249
|
+
if (options.output === "json" && result.json) {
|
|
3250
|
+
console.log(result.json);
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
2081
3253
|
program.command("geo").description("GEO analysis - Check AI search visibility (ChatGPT, Perplexity, Claude)").option("-u, --url <url>", "URL to analyze").option("-o, --output <format>", "Output format (json, console)", "console").action(geo);
|
|
2082
3254
|
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "Use GPT-4 for enhanced analysis with free tool ideas").option("--local", "Force local processing (skip cloud worker)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
|
|
2083
3255
|
if (options.ai && options.url) {
|
|
@@ -2100,8 +3272,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
|
|
|
2100
3272
|
program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
|
|
2101
3273
|
console.log("Competitor analysis coming soon...");
|
|
2102
3274
|
});
|
|
2103
|
-
program.command("
|
|
2104
|
-
|
|
3275
|
+
var trackCmd = program.command("track").description("Keyword rank tracking");
|
|
3276
|
+
trackCmd.command("add <keywords...>").description("Add keywords to track").option("-u, --url <url>", "URL to track rankings for").option("-e, --engine <engine>", "Search engine (google, bing)", "google").option("-c, --country <country>", "Country code (US, UK, DE, etc.)", "US").option("-l, --language <lang>", "Language code (en, de, fr, etc.)", "en").action(async (keywords2, options) => {
|
|
3277
|
+
await trackAdd({
|
|
3278
|
+
keywords: keywords2,
|
|
3279
|
+
url: options.url,
|
|
3280
|
+
searchEngine: options.engine,
|
|
3281
|
+
country: options.country,
|
|
3282
|
+
language: options.language
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
trackCmd.command("status").description("Show current keyword rankings").option("-u, --url <url>", "URL to show rankings for").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3286
|
+
await trackStatus({
|
|
3287
|
+
url: options.url,
|
|
3288
|
+
output: options.output
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
trackCmd.command("history").description("Show keyword position history").option("-u, --url <url>", "URL to show history for").option("-k, --keyword <keyword>", "Keyword to show history for").option("-d, --days <days>", "Number of days of history", "30").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3292
|
+
await trackHistory({
|
|
3293
|
+
url: options.url,
|
|
3294
|
+
keyword: options.keyword,
|
|
3295
|
+
days: parseInt(options.days, 10),
|
|
3296
|
+
output: options.output
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
trackCmd.command("check").description("Check current rankings (calls SERP API)").option("-u, --url <url>", "URL to check rankings for").option("-f, --force", "Force check even if rate limited").action(async (options) => {
|
|
3300
|
+
await trackCheck({
|
|
3301
|
+
url: options.url,
|
|
3302
|
+
force: options.force
|
|
3303
|
+
});
|
|
3304
|
+
});
|
|
3305
|
+
trackCmd.command("export").description("Export tracking data").option("-u, --url <url>", "URL to export data for").option("-f, --format <format>", "Export format (csv, json)", "csv").option("-d, --days <days>", "Number of days of history", "30").action(async (options) => {
|
|
3306
|
+
await trackExport({
|
|
3307
|
+
url: options.url,
|
|
3308
|
+
format: options.format,
|
|
3309
|
+
days: parseInt(options.days, 10)
|
|
3310
|
+
});
|
|
2105
3311
|
});
|
|
2106
3312
|
program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
|
|
2107
3313
|
console.log("Report generation coming soon...");
|
|
@@ -2111,4 +3317,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
|
|
|
2111
3317
|
});
|
|
2112
3318
|
program.command("setup").description("Set up SEO tracking (GA4, Search Console, GitHub Actions)").option("--ga4 <id>", "Google Analytics 4 Measurement ID (G-XXXXXXXXXX)").option("--gsc <code>", "Google Search Console verification code").option("--github-action", "Set up automated GitHub Action").option("--schedule <freq>", "GitHub Action schedule (daily, weekly, monthly)", "weekly").option("--all", "Interactive setup wizard").action(setup);
|
|
2113
3319
|
program.command("gsc-setup").description("Show Google Search Console API setup instructions").action(showGSCInstructions);
|
|
3320
|
+
program.command("alerts").description("Configure Slack/Discord notifications for SEO events").option("-c, --channel <channel>", "Alert channel (slack, discord)").option("-w, --webhook <url>", "Webhook URL").option("--test", "Test the webhook").option("--list", "List current alert configurations").option("--remove <id>", "Remove an alert configuration").action(alerts);
|
|
2114
3321
|
program.parse();
|