@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.mjs
CHANGED
|
@@ -262,7 +262,7 @@ async function analyze(options) {
|
|
|
262
262
|
// src/commands/audit.ts
|
|
263
263
|
import { runFullAudit, discoverPagesFromLinks, frameworks } from "@rankcli/agent-runtime";
|
|
264
264
|
import ora from "ora";
|
|
265
|
-
import
|
|
265
|
+
import chalk2 from "chalk";
|
|
266
266
|
|
|
267
267
|
// src/lib/config.ts
|
|
268
268
|
import Conf from "conf";
|
|
@@ -457,6 +457,191 @@ async function getAuthenticatedClient() {
|
|
|
457
457
|
return supabase;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// src/lib/upsell.ts
|
|
461
|
+
import chalk from "chalk";
|
|
462
|
+
var WEB_APP_URL2 = "https://rankcli.dev";
|
|
463
|
+
var TIER_LIMITS = {
|
|
464
|
+
free_anon: {
|
|
465
|
+
checks: 50,
|
|
466
|
+
sites: 0,
|
|
467
|
+
domains_per_month: 1,
|
|
468
|
+
features: { geo: false, auto_fix: false, github: false, ai: false }
|
|
469
|
+
},
|
|
470
|
+
free: {
|
|
471
|
+
checks: 100,
|
|
472
|
+
sites: 1,
|
|
473
|
+
domains_per_month: 2,
|
|
474
|
+
features: { geo: "basic", auto_fix: false, github: false, ai: false }
|
|
475
|
+
},
|
|
476
|
+
solo: {
|
|
477
|
+
checks: 280,
|
|
478
|
+
sites: 3,
|
|
479
|
+
domains_per_month: 5,
|
|
480
|
+
features: { geo: true, auto_fix: true, github: false, ai: true }
|
|
481
|
+
},
|
|
482
|
+
pro: {
|
|
483
|
+
checks: 280,
|
|
484
|
+
sites: 10,
|
|
485
|
+
domains_per_month: 15,
|
|
486
|
+
features: { geo: true, auto_fix: true, github: true, ai: true }
|
|
487
|
+
},
|
|
488
|
+
agency: {
|
|
489
|
+
checks: 280,
|
|
490
|
+
sites: 50,
|
|
491
|
+
domains_per_month: null,
|
|
492
|
+
// unlimited
|
|
493
|
+
features: { geo: true, auto_fix: true, github: true, ai: true }
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var UPSELL_MESSAGES = {
|
|
497
|
+
// After free audit completes
|
|
498
|
+
free_audit_complete: () => [
|
|
499
|
+
"",
|
|
500
|
+
chalk.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"),
|
|
501
|
+
chalk.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
|
|
502
|
+
chalk.dim(" Create a free account to unlock 100 checks and save your results."),
|
|
503
|
+
"",
|
|
504
|
+
chalk.cyan(` \u2192 rankcli login`),
|
|
505
|
+
chalk.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"),
|
|
506
|
+
""
|
|
507
|
+
],
|
|
508
|
+
// When checks limit is reached
|
|
509
|
+
checks_limit: (ctx) => [
|
|
510
|
+
"",
|
|
511
|
+
chalk.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
|
|
512
|
+
chalk.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
|
|
513
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
|
|
514
|
+
""
|
|
515
|
+
],
|
|
516
|
+
// When domain limit is reached
|
|
517
|
+
domain_limit: (ctx) => [
|
|
518
|
+
"",
|
|
519
|
+
chalk.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"),
|
|
520
|
+
chalk.red.bold(" \u2717 Domain limit reached"),
|
|
521
|
+
chalk.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
|
|
522
|
+
"",
|
|
523
|
+
chalk.dim(" Upgrade to audit more sites:"),
|
|
524
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
|
|
525
|
+
chalk.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"),
|
|
526
|
+
""
|
|
527
|
+
],
|
|
528
|
+
// When GEO features are limited
|
|
529
|
+
geo_limited: (ctx) => [
|
|
530
|
+
"",
|
|
531
|
+
chalk.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"),
|
|
532
|
+
chalk.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
|
|
533
|
+
chalk.dim(" You're seeing basic AI crawler access checks."),
|
|
534
|
+
chalk.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
|
|
535
|
+
"",
|
|
536
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
|
|
537
|
+
chalk.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"),
|
|
538
|
+
""
|
|
539
|
+
],
|
|
540
|
+
// When auto-fix is attempted without access
|
|
541
|
+
auto_fix_blocked: () => [
|
|
542
|
+
"",
|
|
543
|
+
chalk.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"),
|
|
544
|
+
chalk.red.bold(" \u2717 Auto-fix requires Solo plan"),
|
|
545
|
+
chalk.dim(" Apply framework-specific SEO fixes directly to your codebase."),
|
|
546
|
+
"",
|
|
547
|
+
chalk.dim(" Solo ($9/mo) includes:"),
|
|
548
|
+
chalk.dim(" \u2022 Local auto-fix via CLI"),
|
|
549
|
+
chalk.dim(" \u2022 25+ framework support"),
|
|
550
|
+
chalk.dim(" \u2022 Full 280+ checks"),
|
|
551
|
+
"",
|
|
552
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
|
|
553
|
+
chalk.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"),
|
|
554
|
+
""
|
|
555
|
+
],
|
|
556
|
+
// When GitHub integration is attempted without access
|
|
557
|
+
github_blocked: () => [
|
|
558
|
+
"",
|
|
559
|
+
chalk.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"),
|
|
560
|
+
chalk.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
|
|
561
|
+
chalk.dim(" Automatically create fix PRs when SEO issues are detected."),
|
|
562
|
+
"",
|
|
563
|
+
chalk.dim(" Pro ($29/mo) includes:"),
|
|
564
|
+
chalk.dim(" \u2022 GitHub App integration"),
|
|
565
|
+
chalk.dim(" \u2022 Auto-fix PRs"),
|
|
566
|
+
chalk.dim(" \u2022 Daily scheduled audits"),
|
|
567
|
+
chalk.dim(" \u2022 Slack/Discord alerts"),
|
|
568
|
+
"",
|
|
569
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
|
|
570
|
+
chalk.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"),
|
|
571
|
+
""
|
|
572
|
+
],
|
|
573
|
+
// Keyword tracking limit reached
|
|
574
|
+
keyword_limit: (ctx) => [
|
|
575
|
+
"",
|
|
576
|
+
chalk.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"),
|
|
577
|
+
chalk.red.bold(" \u2717 Keyword tracking limit reached"),
|
|
578
|
+
chalk.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
|
|
579
|
+
chalk.dim(` Currently tracking: ${ctx.current}`),
|
|
580
|
+
"",
|
|
581
|
+
chalk.dim(" Upgrade for more keywords:"),
|
|
582
|
+
chalk.dim(" \u2022 Solo ($9/mo): 100 keywords"),
|
|
583
|
+
chalk.dim(" \u2022 Pro ($29/mo): 500 keywords"),
|
|
584
|
+
chalk.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
|
|
585
|
+
"",
|
|
586
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
|
|
587
|
+
chalk.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"),
|
|
588
|
+
""
|
|
589
|
+
],
|
|
590
|
+
// Rate limit exceeded
|
|
591
|
+
rate_limit: (ctx) => [
|
|
592
|
+
"",
|
|
593
|
+
chalk.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"),
|
|
594
|
+
chalk.red.bold(" \u2717 API rate limit exceeded"),
|
|
595
|
+
chalk.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
|
|
596
|
+
"",
|
|
597
|
+
chalk.dim(" Need more? Upgrade for higher limits:"),
|
|
598
|
+
chalk.dim(" \u2022 Solo: 100 req/day"),
|
|
599
|
+
chalk.dim(" \u2022 Pro: 1,000 req/day"),
|
|
600
|
+
chalk.dim(" \u2022 Agency: 10,000 req/day"),
|
|
601
|
+
"",
|
|
602
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
|
|
603
|
+
chalk.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"),
|
|
604
|
+
""
|
|
605
|
+
],
|
|
606
|
+
// Schedule exceeded (for GitHub Action)
|
|
607
|
+
schedule_exceeded: (ctx) => [
|
|
608
|
+
"",
|
|
609
|
+
chalk.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"),
|
|
610
|
+
chalk.yellow.bold(" \u23F0 Scheduled audit skipped"),
|
|
611
|
+
chalk.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
|
|
612
|
+
"",
|
|
613
|
+
chalk.dim(" Upgrade for more frequent audits:"),
|
|
614
|
+
chalk.dim(" \u2022 Solo: Weekly"),
|
|
615
|
+
chalk.dim(" \u2022 Pro: Daily"),
|
|
616
|
+
chalk.dim(" \u2022 Agency: Hourly"),
|
|
617
|
+
"",
|
|
618
|
+
chalk.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
|
|
619
|
+
chalk.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"),
|
|
620
|
+
""
|
|
621
|
+
],
|
|
622
|
+
// Login prompt (soft)
|
|
623
|
+
login_prompt: () => [
|
|
624
|
+
"",
|
|
625
|
+
chalk.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
|
|
626
|
+
chalk.dim(" rankcli login"),
|
|
627
|
+
""
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
function showUpsell(messageKey, context) {
|
|
631
|
+
const messageFn = UPSELL_MESSAGES[messageKey];
|
|
632
|
+
if (!messageFn) return;
|
|
633
|
+
const ctx = {
|
|
634
|
+
tier: "free",
|
|
635
|
+
...context
|
|
636
|
+
};
|
|
637
|
+
const lines = messageFn(ctx);
|
|
638
|
+
lines.forEach((line) => console.log(line));
|
|
639
|
+
}
|
|
640
|
+
function hasFeature(tier, feature) {
|
|
641
|
+
const limits = TIER_LIMITS[tier];
|
|
642
|
+
return limits?.features?.[feature] ?? false;
|
|
643
|
+
}
|
|
644
|
+
|
|
460
645
|
// src/commands/audit.ts
|
|
461
646
|
var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = frameworks;
|
|
462
647
|
async function fetchHtml(url, timeout = 1e4) {
|
|
@@ -524,7 +709,7 @@ Return JSON with:
|
|
|
524
709
|
}
|
|
525
710
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
526
711
|
if (!response.ok) {
|
|
527
|
-
console.error(
|
|
712
|
+
console.error(chalk2.yellow(`AI analysis failed: ${response.status}`));
|
|
528
713
|
return null;
|
|
529
714
|
}
|
|
530
715
|
const data = await response.json();
|
|
@@ -534,7 +719,7 @@ Return JSON with:
|
|
|
534
719
|
if (!jsonMatch) return null;
|
|
535
720
|
return JSON.parse(jsonMatch[0]);
|
|
536
721
|
} catch (error) {
|
|
537
|
-
console.error(
|
|
722
|
+
console.error(chalk2.yellow(`AI analysis error: ${error}`));
|
|
538
723
|
return null;
|
|
539
724
|
}
|
|
540
725
|
}
|
|
@@ -553,7 +738,7 @@ function mergeIssues(issueArrays) {
|
|
|
553
738
|
}
|
|
554
739
|
return Array.from(issueMap.values());
|
|
555
740
|
}
|
|
556
|
-
async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis) {
|
|
741
|
+
async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis, durationMs, checksRun) {
|
|
557
742
|
const supabase = await getAuthenticatedClient();
|
|
558
743
|
if (!supabase) return null;
|
|
559
744
|
const { userId, sitesLimit } = getUserInfo();
|
|
@@ -569,9 +754,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
569
754
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
570
755
|
const limit = sitesLimit ?? 1;
|
|
571
756
|
if (count !== null && count >= limit) {
|
|
572
|
-
console.log(
|
|
757
|
+
console.log(chalk2.yellow(`
|
|
573
758
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
574
|
-
console.log(
|
|
759
|
+
console.log(chalk2.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
575
760
|
return null;
|
|
576
761
|
}
|
|
577
762
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -581,7 +766,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
581
766
|
domain
|
|
582
767
|
}).select("id").single();
|
|
583
768
|
if (projectError || !newProject) {
|
|
584
|
-
console.error(
|
|
769
|
+
console.error(chalk2.dim("Failed to create project"));
|
|
585
770
|
return null;
|
|
586
771
|
}
|
|
587
772
|
projectId = newProject.id;
|
|
@@ -593,8 +778,11 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
593
778
|
onPage: report.healthScore?.onPage || 0,
|
|
594
779
|
content: report.healthScore?.content || 0,
|
|
595
780
|
performance: report.healthScore?.performance || 0,
|
|
596
|
-
security: report.healthScore?.security || 0
|
|
781
|
+
security: report.healthScore?.security || 0,
|
|
782
|
+
localSeo: report.healthScore?.localSeo || 0,
|
|
783
|
+
aiReadiness: report.healthScore?.aiReadiness || 0
|
|
597
784
|
};
|
|
785
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
598
786
|
const { data: audit2, error: auditError } = await supabase.from("audits").insert({
|
|
599
787
|
project_id: projectId,
|
|
600
788
|
triggered_by: "cli",
|
|
@@ -603,21 +791,23 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
603
791
|
score,
|
|
604
792
|
pages_crawled: pagesAudited,
|
|
605
793
|
issues_found: mergedIssues.length,
|
|
606
|
-
checks_run:
|
|
607
|
-
// Approximate
|
|
794
|
+
checks_run: checksRun,
|
|
608
795
|
health_scores: healthScores,
|
|
609
|
-
|
|
796
|
+
started_at: new Date(Date.now() - durationMs).toISOString(),
|
|
797
|
+
completed_at: now,
|
|
798
|
+
duration_ms: durationMs,
|
|
610
799
|
summary: {
|
|
611
800
|
errors: mergedIssues.filter((i) => i.severity === "error").length,
|
|
612
801
|
warnings: mergedIssues.filter((i) => i.severity === "warning").length,
|
|
613
802
|
notices: mergedIssues.filter((i) => i.severity === "notice").length,
|
|
614
803
|
pagesAudited,
|
|
804
|
+
checksRun,
|
|
615
805
|
source: "cli"
|
|
616
806
|
},
|
|
617
807
|
ai_analysis: aiAnalysis
|
|
618
808
|
}).select("id").single();
|
|
619
809
|
if (auditError || !audit2) {
|
|
620
|
-
console.error(
|
|
810
|
+
console.error(chalk2.dim("Failed to save audit"));
|
|
621
811
|
return null;
|
|
622
812
|
}
|
|
623
813
|
if (mergedIssues.length > 0) {
|
|
@@ -638,12 +828,14 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis)
|
|
|
638
828
|
await supabase.from("audit_issues").insert(issueRecords);
|
|
639
829
|
}
|
|
640
830
|
await supabase.from("projects").update({
|
|
641
|
-
last_audit_at:
|
|
642
|
-
last_audit_score: score
|
|
831
|
+
last_audit_at: now,
|
|
832
|
+
last_audit_score: score,
|
|
833
|
+
last_scheduled_audit_at: now,
|
|
834
|
+
scheduled_audit_source: "cli"
|
|
643
835
|
}).eq("id", projectId);
|
|
644
836
|
return { projectId, auditId: audit2.id };
|
|
645
837
|
} catch (error) {
|
|
646
|
-
console.error(
|
|
838
|
+
console.error(chalk2.dim("Cloud sync failed"));
|
|
647
839
|
return null;
|
|
648
840
|
}
|
|
649
841
|
}
|
|
@@ -652,7 +844,7 @@ async function audit(options) {
|
|
|
652
844
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
653
845
|
const enableAI = options.ai || false;
|
|
654
846
|
const aiProvider = options.aiProvider || "openai";
|
|
655
|
-
const aiKey = options.aiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || "";
|
|
847
|
+
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) || "";
|
|
656
848
|
const loggedIn = isLoggedIn();
|
|
657
849
|
if (!url) {
|
|
658
850
|
console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
|
|
@@ -664,8 +856,8 @@ async function audit(options) {
|
|
|
664
856
|
console.log(" --ai-key <key> API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)");
|
|
665
857
|
console.log(" --output json Output as JSON");
|
|
666
858
|
if (!loggedIn) {
|
|
667
|
-
console.log(
|
|
668
|
-
console.log(
|
|
859
|
+
console.log(chalk2.dim("\nTip: Log in to sync results to the web dashboard:"));
|
|
860
|
+
console.log(chalk2.dim(" rankcli login"));
|
|
669
861
|
}
|
|
670
862
|
console.log("");
|
|
671
863
|
return;
|
|
@@ -749,12 +941,17 @@ async function audit(options) {
|
|
|
749
941
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
750
942
|
}
|
|
751
943
|
} else if (enableAI && !aiKey) {
|
|
752
|
-
console.log(
|
|
753
|
-
console.log(
|
|
944
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
|
|
945
|
+
console.log(chalk2.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
|
|
754
946
|
}
|
|
755
947
|
const totalTime = Date.now() - startTime;
|
|
756
948
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
949
|
+
const actualChecksRun = (primaryReport.summary?.passed || 0) + (primaryReport.summary?.errors || 0) + (primaryReport.summary?.warnings || 0) || 200;
|
|
757
950
|
if (options.output === "json") {
|
|
951
|
+
let syncResult = null;
|
|
952
|
+
if (loggedIn) {
|
|
953
|
+
syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
954
|
+
}
|
|
758
955
|
const jsonOutput = {
|
|
759
956
|
url,
|
|
760
957
|
score: primaryReport.healthScore?.overall || 0,
|
|
@@ -763,85 +960,106 @@ async function audit(options) {
|
|
|
763
960
|
framework: frameworkName,
|
|
764
961
|
issues: mergedIssues,
|
|
765
962
|
healthScores: primaryReport.healthScore,
|
|
766
|
-
aiAnalysis
|
|
963
|
+
aiAnalysis,
|
|
964
|
+
// Include cloud sync info if synced
|
|
965
|
+
...syncResult && {
|
|
966
|
+
projectId: syncResult.projectId,
|
|
967
|
+
auditId: syncResult.auditId,
|
|
968
|
+
dashboardUrl: `${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}`
|
|
969
|
+
}
|
|
767
970
|
};
|
|
768
971
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
769
972
|
} else {
|
|
770
|
-
console.log("\n" +
|
|
771
|
-
console.log(
|
|
772
|
-
console.log(
|
|
973
|
+
console.log("\n" + chalk2.bold("\u2550".repeat(60)));
|
|
974
|
+
console.log(chalk2.bold.cyan(` SEO AUDIT REPORT: ${url}`));
|
|
975
|
+
console.log(chalk2.bold("\u2550".repeat(60)));
|
|
773
976
|
const score = primaryReport.healthScore?.overall || 0;
|
|
774
|
-
const scoreColor = score >= 80 ?
|
|
977
|
+
const scoreColor = score >= 80 ? chalk2.green : score >= 60 ? chalk2.yellow : chalk2.red;
|
|
775
978
|
console.log(`
|
|
776
|
-
${
|
|
777
|
-
console.log(` ${
|
|
778
|
-
console.log(` ${
|
|
779
|
-
console.log(` ${
|
|
979
|
+
${chalk2.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
|
|
980
|
+
console.log(` ${chalk2.bold("Pages audited:")} ${successCount}`);
|
|
981
|
+
console.log(` ${chalk2.bold("Issues found:")} ${mergedIssues.length}`);
|
|
982
|
+
console.log(` ${chalk2.bold("Time:")} ${totalTime}ms`);
|
|
780
983
|
if (frameworkName) {
|
|
781
|
-
console.log(` ${
|
|
984
|
+
console.log(` ${chalk2.bold("Framework:")} ${chalk2.cyan(frameworkName)}`);
|
|
782
985
|
}
|
|
783
986
|
if (primaryReport.healthScore) {
|
|
784
|
-
console.log("\n" +
|
|
987
|
+
console.log("\n" + chalk2.bold(" Health Scores:"));
|
|
785
988
|
const hs = primaryReport.healthScore;
|
|
786
989
|
const bar = (score2) => {
|
|
787
990
|
const filled = Math.round(score2 / 5);
|
|
788
991
|
const empty = 20 - filled;
|
|
789
|
-
const color = score2 >= 80 ?
|
|
790
|
-
return color("\u2588".repeat(filled)) +
|
|
992
|
+
const color = score2 >= 80 ? chalk2.green : score2 >= 60 ? chalk2.yellow : chalk2.red;
|
|
993
|
+
return color("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(empty));
|
|
791
994
|
};
|
|
792
995
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
793
996
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
794
997
|
console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
|
|
795
998
|
console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
|
|
796
999
|
console.log(` Security: ${bar(hs.security || 0)} ${hs.security || 0}%`);
|
|
1000
|
+
console.log(` Local SEO: ${bar(hs.localSeo || 0)} ${hs.localSeo || 0}%`);
|
|
1001
|
+
console.log(` AI Readiness: ${bar(hs.aiReadiness || 0)} ${hs.aiReadiness || 0}%`);
|
|
797
1002
|
}
|
|
798
1003
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
799
1004
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
800
1005
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
801
|
-
console.log("\n" +
|
|
802
|
-
console.log(` ${
|
|
803
|
-
console.log(` ${
|
|
804
|
-
console.log(` ${
|
|
805
|
-
console.log("\n" +
|
|
1006
|
+
console.log("\n" + chalk2.bold(" Issues by Severity:"));
|
|
1007
|
+
console.log(` ${chalk2.red("\u25CF")} Errors: ${errors.length}`);
|
|
1008
|
+
console.log(` ${chalk2.yellow("\u25CF")} Warnings: ${warnings.length}`);
|
|
1009
|
+
console.log(` ${chalk2.blue("\u25CF")} Notices: ${notices.length}`);
|
|
1010
|
+
console.log("\n" + chalk2.bold(" Top Issues:"));
|
|
806
1011
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
807
1012
|
topIssues.forEach((issue, i) => {
|
|
808
|
-
const icon = issue.severity === "error" ?
|
|
1013
|
+
const icon = issue.severity === "error" ? chalk2.red("\u2717") : issue.severity === "warning" ? chalk2.yellow("!") : chalk2.blue("i");
|
|
809
1014
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
810
1015
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
811
|
-
console.log(
|
|
1016
|
+
console.log(chalk2.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
812
1017
|
}
|
|
813
1018
|
});
|
|
814
1019
|
if (aiAnalysis) {
|
|
815
|
-
console.log("\n" +
|
|
1020
|
+
console.log("\n" + chalk2.bold(" AI Analysis:"));
|
|
816
1021
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
817
|
-
console.log("\n " +
|
|
1022
|
+
console.log("\n " + chalk2.bold("Suggestions:"));
|
|
818
1023
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
819
1024
|
console.log(` ${i + 1}. ${s}`);
|
|
820
1025
|
});
|
|
821
1026
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
822
|
-
console.log("\n " +
|
|
823
|
-
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((
|
|
824
|
-
console.log(` ${i + 1}. [P${
|
|
825
|
-
console.log(
|
|
1027
|
+
console.log("\n " + chalk2.bold("Priority Fixes:"));
|
|
1028
|
+
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1029
|
+
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1030
|
+
console.log(chalk2.dim(` ${fix2.rationale}`));
|
|
826
1031
|
});
|
|
827
1032
|
}
|
|
828
1033
|
}
|
|
829
|
-
console.log("\n" +
|
|
1034
|
+
console.log("\n" + chalk2.bold("\u2550".repeat(60)));
|
|
1035
|
+
const userInfo = getUserInfo();
|
|
1036
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1037
|
+
const isFreeTier = !loggedIn || tier === "free";
|
|
1038
|
+
const isSoloOrBelow = isFreeTier || tier === "solo";
|
|
1039
|
+
if (isFreeTier) {
|
|
1040
|
+
const checksRun = mergedIssues.length > 0 ? loggedIn ? 100 : 50 : 0;
|
|
1041
|
+
showUpsell("checks_limit", {
|
|
1042
|
+
tier: loggedIn ? "free" : "free_anon",
|
|
1043
|
+
current: checksRun
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (options.geo && isFreeTier) {
|
|
1047
|
+
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1048
|
+
}
|
|
830
1049
|
if (loggedIn) {
|
|
831
1050
|
const syncSpinner = ora("Syncing to cloud...").start();
|
|
832
|
-
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis);
|
|
1051
|
+
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
833
1052
|
if (syncResult) {
|
|
834
1053
|
syncSpinner.succeed("Results synced to cloud");
|
|
835
|
-
console.log(
|
|
1054
|
+
console.log(chalk2.cyan(`
|
|
836
1055
|
View and manage issues in the web dashboard:`));
|
|
837
|
-
console.log(
|
|
1056
|
+
console.log(chalk2.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
838
1057
|
`));
|
|
839
1058
|
} else {
|
|
840
1059
|
syncSpinner.warn("Could not sync to cloud");
|
|
841
1060
|
}
|
|
842
1061
|
} else {
|
|
843
|
-
|
|
844
|
-
console.log(chalk.dim(" rankcli login\n"));
|
|
1062
|
+
showUpsell("free_audit_complete");
|
|
845
1063
|
}
|
|
846
1064
|
}
|
|
847
1065
|
} catch (error) {
|
|
@@ -855,7 +1073,6 @@ async function audit(options) {
|
|
|
855
1073
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
856
1074
|
import { join as join3 } from "path";
|
|
857
1075
|
import {
|
|
858
|
-
runDirectAnalysis,
|
|
859
1076
|
runAuditWithFixes,
|
|
860
1077
|
generateFixes,
|
|
861
1078
|
applyFixes,
|
|
@@ -866,19 +1083,19 @@ var SEVERITY_ICON = {
|
|
|
866
1083
|
warning: "\u26A0\uFE0F",
|
|
867
1084
|
info: "\u2139\uFE0F"
|
|
868
1085
|
};
|
|
869
|
-
function printFix(
|
|
870
|
-
const icon = SEVERITY_ICON[
|
|
1086
|
+
function printFix(fix2, index) {
|
|
1087
|
+
const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
|
|
871
1088
|
console.log(`
|
|
872
|
-
${index + 1}. ${icon} ${
|
|
873
|
-
console.log(` File: ${
|
|
874
|
-
if (
|
|
875
|
-
console.log(` Status: SKIPPED - ${
|
|
1089
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1090
|
+
console.log(` File: ${fix2.file}`);
|
|
1091
|
+
if (fix2.skipped) {
|
|
1092
|
+
console.log(` Status: SKIPPED - ${fix2.skipReason}`);
|
|
876
1093
|
} else {
|
|
877
|
-
console.log(` Explanation: ${
|
|
878
|
-
if (
|
|
879
|
-
console.log(` Before: ${truncate(
|
|
1094
|
+
console.log(` Explanation: ${fix2.explanation}`);
|
|
1095
|
+
if (fix2.before) {
|
|
1096
|
+
console.log(` Before: ${truncate(fix2.before, 60)}`);
|
|
880
1097
|
}
|
|
881
|
-
console.log(` After: ${truncate(
|
|
1098
|
+
console.log(` After: ${truncate(fix2.after, 100)}`);
|
|
882
1099
|
}
|
|
883
1100
|
}
|
|
884
1101
|
function truncate(str, maxLen) {
|
|
@@ -914,40 +1131,37 @@ async function apply(options) {
|
|
|
914
1131
|
}
|
|
915
1132
|
console.log("\n\u{1F680} SEO Autopilot - Apply Fixes");
|
|
916
1133
|
console.log("\u2550".repeat(50));
|
|
1134
|
+
const loggedIn = isLoggedIn();
|
|
1135
|
+
const userInfo = getUserInfo();
|
|
1136
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1137
|
+
const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
|
|
1138
|
+
if (!hasAutoFix) {
|
|
1139
|
+
showUpsell("auto_fix_blocked");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
917
1142
|
console.log("\n\u{1F50D} Detecting framework...");
|
|
918
1143
|
const frameworkResult = await detectFramework3({ cwd });
|
|
919
1144
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
920
1145
|
console.log(` Framework: ${framework.name}`);
|
|
921
|
-
const isPaidUser = isLoggedIn();
|
|
922
1146
|
console.log(`
|
|
923
1147
|
\u{1F50D} Analyzing ${url}...`);
|
|
924
|
-
|
|
925
|
-
console.log(" Running full audit (200+ checks)...");
|
|
926
|
-
} else {
|
|
927
|
-
console.log(" Running basic analysis (login for full 200+ checks)");
|
|
928
|
-
}
|
|
1148
|
+
console.log(" Running full audit (200+ checks)...");
|
|
929
1149
|
let issues;
|
|
930
1150
|
let score;
|
|
931
1151
|
let auditResult = null;
|
|
932
1152
|
try {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
score = auditResult.score;
|
|
946
|
-
} else {
|
|
947
|
-
const analysis = await runDirectAnalysis(url);
|
|
948
|
-
issues = analysis.issues;
|
|
949
|
-
score = analysis.score;
|
|
950
|
-
}
|
|
1153
|
+
auditResult = await runAuditWithFixes({
|
|
1154
|
+
url,
|
|
1155
|
+
generateFixes: true
|
|
1156
|
+
});
|
|
1157
|
+
issues = auditResult.report.issues.map((issue) => ({
|
|
1158
|
+
severity: issue.severity,
|
|
1159
|
+
category: issue.category,
|
|
1160
|
+
code: issue.code,
|
|
1161
|
+
message: issue.title,
|
|
1162
|
+
impact: issue.description || void 0
|
|
1163
|
+
}));
|
|
1164
|
+
score = auditResult.score;
|
|
951
1165
|
} catch (error) {
|
|
952
1166
|
console.log(`
|
|
953
1167
|
\u274C Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}
|
|
@@ -1007,15 +1221,15 @@ async function apply(options) {
|
|
|
1007
1221
|
if (applied.length > 0) {
|
|
1008
1222
|
console.log(`
|
|
1009
1223
|
\u2705 Applied ${applied.length} fix(es):`);
|
|
1010
|
-
for (const
|
|
1011
|
-
console.log(` - ${
|
|
1224
|
+
for (const fix2 of applied) {
|
|
1225
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1012
1226
|
}
|
|
1013
1227
|
}
|
|
1014
1228
|
if (skipped.length > 0) {
|
|
1015
1229
|
console.log(`
|
|
1016
1230
|
\u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
|
|
1017
|
-
for (const
|
|
1018
|
-
console.log(` - ${
|
|
1231
|
+
for (const fix2 of skipped) {
|
|
1232
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1019
1233
|
}
|
|
1020
1234
|
}
|
|
1021
1235
|
if (config2 && !config2.url) {
|
|
@@ -1035,10 +1249,793 @@ async function apply(options) {
|
|
|
1035
1249
|
console.log("");
|
|
1036
1250
|
}
|
|
1037
1251
|
|
|
1252
|
+
// src/commands/fix.ts
|
|
1253
|
+
import { exec } from "child_process";
|
|
1254
|
+
import { promisify } from "util";
|
|
1255
|
+
import {
|
|
1256
|
+
runDirectAnalysis as runDirectAnalysis2,
|
|
1257
|
+
generateFixes as generateFixes2,
|
|
1258
|
+
applyFixes as applyFixes2,
|
|
1259
|
+
detectFramework as detectFramework4
|
|
1260
|
+
} from "@rankcli/agent-runtime";
|
|
1261
|
+
import ora2 from "ora";
|
|
1262
|
+
import chalk3 from "chalk";
|
|
1263
|
+
var execAsync = promisify(exec);
|
|
1264
|
+
var SEVERITY_ICON2 = {
|
|
1265
|
+
critical: "\u274C",
|
|
1266
|
+
error: "\u274C",
|
|
1267
|
+
warning: "\u26A0\uFE0F",
|
|
1268
|
+
notice: "\u2139\uFE0F",
|
|
1269
|
+
info: "\u2139\uFE0F"
|
|
1270
|
+
};
|
|
1271
|
+
function generateCommitMessage(fixes, url) {
|
|
1272
|
+
const applied = fixes.filter((f) => f.applied && !f.skipped);
|
|
1273
|
+
if (applied.length === 0) {
|
|
1274
|
+
return "";
|
|
1275
|
+
}
|
|
1276
|
+
const isAddingNew = applied.some(
|
|
1277
|
+
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1278
|
+
);
|
|
1279
|
+
const type = isAddingNew ? "feat" : "fix";
|
|
1280
|
+
const categories = new Set(applied.map((f) => f.issue.category || "seo"));
|
|
1281
|
+
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1282
|
+
const issueCount = applied.length;
|
|
1283
|
+
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1284
|
+
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
1285
|
+
const body = `Fixed:
|
|
1286
|
+
${fixList}
|
|
1287
|
+
|
|
1288
|
+
URL: ${url}
|
|
1289
|
+
Generated by: rankcli fix`;
|
|
1290
|
+
return `${title}
|
|
1291
|
+
|
|
1292
|
+
${body}`;
|
|
1293
|
+
}
|
|
1294
|
+
async function executeGitCommit(cwd, commitMessage, push) {
|
|
1295
|
+
try {
|
|
1296
|
+
await execAsync("git add -A", { cwd });
|
|
1297
|
+
const escapedMessage = commitMessage.replace(/"/g, '\\"');
|
|
1298
|
+
await execAsync(`git commit -m "${escapedMessage}"`, { cwd });
|
|
1299
|
+
let pushed = false;
|
|
1300
|
+
if (push) {
|
|
1301
|
+
try {
|
|
1302
|
+
await execAsync("git push", { cwd });
|
|
1303
|
+
pushed = true;
|
|
1304
|
+
} catch (pushErr) {
|
|
1305
|
+
return {
|
|
1306
|
+
committed: true,
|
|
1307
|
+
pushed: false,
|
|
1308
|
+
error: `Push failed: ${pushErr instanceof Error ? pushErr.message : "Unknown error"}`
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return { committed: true, pushed };
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
return {
|
|
1315
|
+
committed: false,
|
|
1316
|
+
pushed: false,
|
|
1317
|
+
error: err instanceof Error ? err.message : "Git operation failed"
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function truncate2(str, maxLen) {
|
|
1322
|
+
const oneLine = str.replace(/\n/g, " ").trim();
|
|
1323
|
+
if (oneLine.length <= maxLen) return oneLine;
|
|
1324
|
+
return oneLine.substring(0, maxLen - 3) + "...";
|
|
1325
|
+
}
|
|
1326
|
+
function printFix2(fix2, index) {
|
|
1327
|
+
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1328
|
+
console.log(`
|
|
1329
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1330
|
+
console.log(` Code: ${chalk3.dim(fix2.issue.code)}`);
|
|
1331
|
+
console.log(` File: ${chalk3.cyan(fix2.file)}`);
|
|
1332
|
+
if (fix2.skipped) {
|
|
1333
|
+
console.log(` Status: ${chalk3.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1334
|
+
} else if (fix2.applied) {
|
|
1335
|
+
console.log(` Status: ${chalk3.green("APPLIED")}`);
|
|
1336
|
+
console.log(` ${chalk3.dim("Explanation:")} ${fix2.explanation}`);
|
|
1337
|
+
} else {
|
|
1338
|
+
console.log(` ${chalk3.dim("Explanation:")} ${fix2.explanation}`);
|
|
1339
|
+
if (fix2.before) {
|
|
1340
|
+
console.log(` ${chalk3.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1341
|
+
}
|
|
1342
|
+
console.log(` ${chalk3.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
async function fix(options) {
|
|
1346
|
+
const startTime = Date.now();
|
|
1347
|
+
const cwd = options.cwd || process.cwd();
|
|
1348
|
+
const url = options.url;
|
|
1349
|
+
if (!url) {
|
|
1350
|
+
const error = "Please provide a URL to fix:\n rankcli fix --url https://your-site.com";
|
|
1351
|
+
if (options.output !== "json") {
|
|
1352
|
+
console.log(`
|
|
1353
|
+
\u26A0\uFE0F ${error}
|
|
1354
|
+
`);
|
|
1355
|
+
}
|
|
1356
|
+
return { success: false, error };
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
new URL(url);
|
|
1360
|
+
} catch {
|
|
1361
|
+
const error = "Invalid URL format. Please provide a valid URL.";
|
|
1362
|
+
if (options.output !== "json") {
|
|
1363
|
+
console.log(`
|
|
1364
|
+
\u274C ${error}
|
|
1365
|
+
`);
|
|
1366
|
+
}
|
|
1367
|
+
return { success: false, error };
|
|
1368
|
+
}
|
|
1369
|
+
const loggedIn = isLoggedIn();
|
|
1370
|
+
const userInfo = getUserInfo();
|
|
1371
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1372
|
+
const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
|
|
1373
|
+
if (!options.dryRun && !hasAutoFix) {
|
|
1374
|
+
if (options.output !== "json") {
|
|
1375
|
+
showUpsell("auto_fix_blocked");
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
success: false,
|
|
1379
|
+
error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (options.output !== "json") {
|
|
1383
|
+
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1384
|
+
console.log("\u2550".repeat(50));
|
|
1385
|
+
}
|
|
1386
|
+
const frameworkSpinner = options.output !== "json" ? ora2("Detecting framework...").start() : null;
|
|
1387
|
+
const frameworkResult = await detectFramework4({ cwd });
|
|
1388
|
+
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1389
|
+
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1390
|
+
const analyzeSpinner = options.output !== "json" ? ora2(`Analyzing ${url}...`).start() : null;
|
|
1391
|
+
let issues;
|
|
1392
|
+
let score;
|
|
1393
|
+
try {
|
|
1394
|
+
const analysis = await runDirectAnalysis2({ url });
|
|
1395
|
+
issues = analysis.issues;
|
|
1396
|
+
score = analysis.score;
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1399
|
+
analyzeSpinner?.fail(`Analysis failed: ${errorMsg}`);
|
|
1400
|
+
return { success: false, error: `Analysis failed: ${errorMsg}` };
|
|
1401
|
+
}
|
|
1402
|
+
analyzeSpinner?.succeed(`Score: ${score}/100, Issues: ${issues.length}`);
|
|
1403
|
+
if (options.only && options.only.length > 0) {
|
|
1404
|
+
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1405
|
+
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1406
|
+
if (options.output !== "json") {
|
|
1407
|
+
console.log(chalk3.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (issues.length === 0) {
|
|
1411
|
+
if (options.output !== "json") {
|
|
1412
|
+
console.log("\n\u2705 No issues found! Your site looks great.\n");
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
success: true,
|
|
1416
|
+
url,
|
|
1417
|
+
dryRun: options.dryRun,
|
|
1418
|
+
fixes: [],
|
|
1419
|
+
appliedCount: 0,
|
|
1420
|
+
skippedCount: 0,
|
|
1421
|
+
durationMs: Date.now() - startTime
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
const fixSpinner = options.output !== "json" ? ora2("Generating fixes...").start() : null;
|
|
1425
|
+
const fixes = await generateFixes2(issues, {
|
|
1426
|
+
cwd,
|
|
1427
|
+
url,
|
|
1428
|
+
framework
|
|
1429
|
+
});
|
|
1430
|
+
const actionableFixes = fixes.filter((f) => !f.skipped);
|
|
1431
|
+
const skippedFixes = fixes.filter((f) => f.skipped);
|
|
1432
|
+
fixSpinner?.succeed(`Generated ${actionableFixes.length} fix(es), ${skippedFixes.length} skipped`);
|
|
1433
|
+
if (options.dryRun) {
|
|
1434
|
+
if (options.output !== "json") {
|
|
1435
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1436
|
+
console.log(chalk3.yellow("DRY RUN - No changes will be made"));
|
|
1437
|
+
console.log("\u2500".repeat(50));
|
|
1438
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1439
|
+
printFix2(fixes[i], i);
|
|
1440
|
+
}
|
|
1441
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1442
|
+
console.log(`\u{1F4CB} ${chalk3.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1443
|
+
console.log(` Remove --dry-run to apply these fixes.
|
|
1444
|
+
`);
|
|
1445
|
+
}
|
|
1446
|
+
const result2 = {
|
|
1447
|
+
success: true,
|
|
1448
|
+
url,
|
|
1449
|
+
dryRun: true,
|
|
1450
|
+
fixes,
|
|
1451
|
+
appliedCount: 0,
|
|
1452
|
+
skippedCount: skippedFixes.length,
|
|
1453
|
+
durationMs: Date.now() - startTime
|
|
1454
|
+
};
|
|
1455
|
+
if (options.output === "json") {
|
|
1456
|
+
result2.json = JSON.stringify({
|
|
1457
|
+
url,
|
|
1458
|
+
dryRun: true,
|
|
1459
|
+
fixes: fixes.map((f) => ({
|
|
1460
|
+
code: f.issue.code,
|
|
1461
|
+
message: f.issue.message,
|
|
1462
|
+
file: f.file,
|
|
1463
|
+
skipped: f.skipped,
|
|
1464
|
+
skipReason: f.skipReason
|
|
1465
|
+
})),
|
|
1466
|
+
appliedCount: 0,
|
|
1467
|
+
skippedCount: skippedFixes.length,
|
|
1468
|
+
durationMs: result2.durationMs
|
|
1469
|
+
}, null, 2);
|
|
1470
|
+
}
|
|
1471
|
+
return result2;
|
|
1472
|
+
}
|
|
1473
|
+
if (actionableFixes.length === 0) {
|
|
1474
|
+
if (options.output !== "json") {
|
|
1475
|
+
console.log("\n\u26A0\uFE0F No automatic fixes available. Please address issues manually.\n");
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
success: true,
|
|
1479
|
+
url,
|
|
1480
|
+
fixes,
|
|
1481
|
+
appliedCount: 0,
|
|
1482
|
+
skippedCount: skippedFixes.length,
|
|
1483
|
+
durationMs: Date.now() - startTime
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
if (!options.auto) {
|
|
1487
|
+
if (options.output !== "json") {
|
|
1488
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1489
|
+
console.log("PROPOSED FIXES:");
|
|
1490
|
+
console.log("\u2500".repeat(50));
|
|
1491
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1492
|
+
printFix2(fixes[i], i);
|
|
1493
|
+
}
|
|
1494
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1495
|
+
console.log(`Ready to apply ${actionableFixes.length} fix(es).`);
|
|
1496
|
+
console.log("Run with --auto to apply without confirmation.");
|
|
1497
|
+
console.log("Run with --dry-run to preview changes.\n");
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
success: true,
|
|
1501
|
+
url,
|
|
1502
|
+
fixes,
|
|
1503
|
+
appliedCount: 0,
|
|
1504
|
+
skippedCount: skippedFixes.length,
|
|
1505
|
+
durationMs: Date.now() - startTime
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const applySpinner = options.output !== "json" ? ora2("Applying fixes...").start() : null;
|
|
1509
|
+
const { applied, skipped } = await applyFixes2(fixes, { cwd, dryRun: false });
|
|
1510
|
+
for (const fix2 of applied) {
|
|
1511
|
+
fix2.applied = true;
|
|
1512
|
+
}
|
|
1513
|
+
applySpinner?.succeed(`Applied ${applied.length} fix(es)`);
|
|
1514
|
+
const commitMessage = options.commit ? generateCommitMessage(fixes, url) : void 0;
|
|
1515
|
+
let committed = false;
|
|
1516
|
+
let pushed = false;
|
|
1517
|
+
let commitError;
|
|
1518
|
+
let pushError;
|
|
1519
|
+
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1520
|
+
const gitSpinner = options.output !== "json" ? ora2("Committing changes...").start() : null;
|
|
1521
|
+
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1522
|
+
committed = gitResult.committed;
|
|
1523
|
+
pushed = gitResult.pushed;
|
|
1524
|
+
if (gitResult.error) {
|
|
1525
|
+
if (gitResult.error.includes("Push")) {
|
|
1526
|
+
pushError = gitResult.error;
|
|
1527
|
+
gitSpinner?.warn("Committed but push failed");
|
|
1528
|
+
} else {
|
|
1529
|
+
commitError = gitResult.error;
|
|
1530
|
+
gitSpinner?.warn(`Commit failed: ${gitResult.error}`);
|
|
1531
|
+
}
|
|
1532
|
+
} else {
|
|
1533
|
+
if (pushed) {
|
|
1534
|
+
gitSpinner?.succeed("Committed and pushed");
|
|
1535
|
+
} else {
|
|
1536
|
+
gitSpinner?.succeed("Committed");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (options.output !== "json") {
|
|
1541
|
+
console.log("\n" + "\u2550".repeat(50));
|
|
1542
|
+
console.log("RESULTS");
|
|
1543
|
+
console.log("\u2550".repeat(50));
|
|
1544
|
+
if (applied.length > 0) {
|
|
1545
|
+
console.log(`
|
|
1546
|
+
${chalk3.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1547
|
+
for (const fix2 of applied) {
|
|
1548
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (skipped.length > 0) {
|
|
1552
|
+
console.log(`
|
|
1553
|
+
${chalk3.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1554
|
+
for (const fix2 of skipped) {
|
|
1555
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (committed) {
|
|
1559
|
+
console.log(`
|
|
1560
|
+
${chalk3.green("\u2705")} Changes committed`);
|
|
1561
|
+
if (pushed) {
|
|
1562
|
+
console.log(`${chalk3.green("\u2705")} Changes pushed`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (!committed && applied.length > 0) {
|
|
1566
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1567
|
+
console.log("NEXT STEPS:");
|
|
1568
|
+
console.log("\u2500".repeat(50));
|
|
1569
|
+
console.log("1. Review the changes:");
|
|
1570
|
+
console.log(" git diff");
|
|
1571
|
+
console.log("\n2. Commit the SEO improvements:");
|
|
1572
|
+
console.log(` git add -A && git commit -m "feat(seo): fix ${applied.length} SEO issues"`);
|
|
1573
|
+
console.log("\n3. Re-run the audit to verify:");
|
|
1574
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1575
|
+
}
|
|
1576
|
+
console.log("");
|
|
1577
|
+
}
|
|
1578
|
+
const result = {
|
|
1579
|
+
success: true,
|
|
1580
|
+
url,
|
|
1581
|
+
dryRun: false,
|
|
1582
|
+
fixes,
|
|
1583
|
+
appliedCount: applied.length,
|
|
1584
|
+
skippedCount: skipped.length,
|
|
1585
|
+
durationMs: Date.now() - startTime,
|
|
1586
|
+
commitMessage,
|
|
1587
|
+
committed,
|
|
1588
|
+
pushed,
|
|
1589
|
+
commitError,
|
|
1590
|
+
pushError
|
|
1591
|
+
};
|
|
1592
|
+
if (options.output === "json") {
|
|
1593
|
+
result.json = JSON.stringify({
|
|
1594
|
+
url,
|
|
1595
|
+
fixes: fixes.map((f) => ({
|
|
1596
|
+
code: f.issue.code,
|
|
1597
|
+
message: f.issue.message,
|
|
1598
|
+
file: f.file,
|
|
1599
|
+
applied: f.applied,
|
|
1600
|
+
skipped: f.skipped,
|
|
1601
|
+
skipReason: f.skipReason
|
|
1602
|
+
})),
|
|
1603
|
+
appliedCount: applied.length,
|
|
1604
|
+
skippedCount: skipped.length,
|
|
1605
|
+
durationMs: result.durationMs,
|
|
1606
|
+
committed,
|
|
1607
|
+
pushed,
|
|
1608
|
+
commitMessage
|
|
1609
|
+
}, null, 2);
|
|
1610
|
+
}
|
|
1611
|
+
return result;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/commands/track.ts
|
|
1615
|
+
import ora3 from "ora";
|
|
1616
|
+
import chalk4 from "chalk";
|
|
1617
|
+
import { table } from "table";
|
|
1618
|
+
import { RankTracker, TIER_LIMITS as TIER_LIMITS3 } from "@rankcli/agent-runtime";
|
|
1619
|
+
async function getProjectForUrl(url) {
|
|
1620
|
+
const supabase = await getAuthenticatedClient();
|
|
1621
|
+
if (!supabase) return null;
|
|
1622
|
+
const { userId } = getUserInfo();
|
|
1623
|
+
if (!userId) return null;
|
|
1624
|
+
const parsedUrl = new URL(url);
|
|
1625
|
+
const domain = parsedUrl.hostname.replace(/^www\./, "");
|
|
1626
|
+
const { data, error } = await supabase.from("projects").select("id, domain, last_rank_check_at").eq("user_id", userId).eq("domain", domain).single();
|
|
1627
|
+
if (error || !data) {
|
|
1628
|
+
const { data: newProject, error: createError } = await supabase.from("projects").insert({
|
|
1629
|
+
user_id: userId,
|
|
1630
|
+
name: domain,
|
|
1631
|
+
url,
|
|
1632
|
+
domain
|
|
1633
|
+
}).select("id, domain, last_rank_check_at").single();
|
|
1634
|
+
if (createError || !newProject) return null;
|
|
1635
|
+
return {
|
|
1636
|
+
id: newProject.id,
|
|
1637
|
+
domain: newProject.domain,
|
|
1638
|
+
lastRankCheckAt: newProject.last_rank_check_at ? new Date(newProject.last_rank_check_at) : null
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
id: data.id,
|
|
1643
|
+
domain: data.domain,
|
|
1644
|
+
lastRankCheckAt: data.last_rank_check_at ? new Date(data.last_rank_check_at) : null
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function getTierLimits(tier) {
|
|
1648
|
+
const limits = TIER_LIMITS3[tier] || TIER_LIMITS3.free;
|
|
1649
|
+
return {
|
|
1650
|
+
maxKeywords: limits.maxKeywords,
|
|
1651
|
+
checksPerDay: limits.checksPerDay
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function formatPosition(pos) {
|
|
1655
|
+
if (pos === null) return chalk4.dim("N/A");
|
|
1656
|
+
if (pos <= 3) return chalk4.green.bold(`#${pos}`);
|
|
1657
|
+
if (pos <= 10) return chalk4.green(`#${pos}`);
|
|
1658
|
+
if (pos <= 20) return chalk4.yellow(`#${pos}`);
|
|
1659
|
+
if (pos <= 50) return chalk4.red(`#${pos}`);
|
|
1660
|
+
return chalk4.red.dim(`#${pos}`);
|
|
1661
|
+
}
|
|
1662
|
+
function formatChange(change) {
|
|
1663
|
+
if (change === 0) return chalk4.dim("\u2192");
|
|
1664
|
+
if (change > 0) return chalk4.green(`\u2191${change}`);
|
|
1665
|
+
return chalk4.red(`\u2193${Math.abs(change)}`);
|
|
1666
|
+
}
|
|
1667
|
+
async function trackAdd(options) {
|
|
1668
|
+
if (!options.keywords || options.keywords.length === 0) {
|
|
1669
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1670
|
+
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1671
|
+
return { success: false, error: "No keywords provided" };
|
|
1672
|
+
}
|
|
1673
|
+
if (!options.url) {
|
|
1674
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1675
|
+
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1676
|
+
return { success: false, error: "No URL provided" };
|
|
1677
|
+
}
|
|
1678
|
+
if (!isLoggedIn()) {
|
|
1679
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1680
|
+
console.log(" rankcli login\n");
|
|
1681
|
+
return { success: false, error: "Not logged in" };
|
|
1682
|
+
}
|
|
1683
|
+
const spinner = ora3("Adding keywords to track...").start();
|
|
1684
|
+
try {
|
|
1685
|
+
const project = await getProjectForUrl(options.url);
|
|
1686
|
+
if (!project) {
|
|
1687
|
+
spinner.fail("Could not find or create project");
|
|
1688
|
+
return { success: false, error: "Project not found" };
|
|
1689
|
+
}
|
|
1690
|
+
const { planName } = getUserInfo();
|
|
1691
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1692
|
+
const { maxKeywords } = getTierLimits(tier);
|
|
1693
|
+
const supabase = await getAuthenticatedClient();
|
|
1694
|
+
if (!supabase) {
|
|
1695
|
+
spinner.fail("Could not connect to database");
|
|
1696
|
+
return { success: false, error: "Database connection failed" };
|
|
1697
|
+
}
|
|
1698
|
+
const { count } = await supabase.from("keywords").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("is_active", true);
|
|
1699
|
+
const existingCount = count || 0;
|
|
1700
|
+
const newCount = options.keywords.length;
|
|
1701
|
+
if (existingCount + newCount > maxKeywords) {
|
|
1702
|
+
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1703
|
+
showUpsell("keyword_limit", { tier, current: existingCount, limit: maxKeywords });
|
|
1704
|
+
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1705
|
+
}
|
|
1706
|
+
const keywordRecords = options.keywords.map((keyword) => ({
|
|
1707
|
+
project_id: project.id,
|
|
1708
|
+
keyword: keyword.toLowerCase().trim(),
|
|
1709
|
+
search_engine: options.searchEngine || "google",
|
|
1710
|
+
country: options.country || "US",
|
|
1711
|
+
language: options.language || "en",
|
|
1712
|
+
is_active: true
|
|
1713
|
+
}));
|
|
1714
|
+
const { data, error } = await supabase.from("keywords").upsert(keywordRecords, {
|
|
1715
|
+
onConflict: "project_id,keyword",
|
|
1716
|
+
ignoreDuplicates: false
|
|
1717
|
+
}).select();
|
|
1718
|
+
if (error) {
|
|
1719
|
+
spinner.fail(`Failed to add keywords: ${error.message}`);
|
|
1720
|
+
return { success: false, error: error.message };
|
|
1721
|
+
}
|
|
1722
|
+
const addedCount = data?.length || 0;
|
|
1723
|
+
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1724
|
+
console.log(chalk4.dim(`
|
|
1725
|
+
Tracking: ${options.keywords.join(", ")}`));
|
|
1726
|
+
console.log(chalk4.dim(`For: ${project.domain}`));
|
|
1727
|
+
console.log(chalk4.cyan(`
|
|
1728
|
+
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1729
|
+
`));
|
|
1730
|
+
return { success: true, addedCount };
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
spinner.fail("Failed to add keywords");
|
|
1733
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
async function trackStatus(options) {
|
|
1737
|
+
if (!options.url) {
|
|
1738
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1739
|
+
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1740
|
+
return { success: false, error: "No URL provided" };
|
|
1741
|
+
}
|
|
1742
|
+
if (!isLoggedIn()) {
|
|
1743
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1744
|
+
console.log(" rankcli login\n");
|
|
1745
|
+
return { success: false, error: "Not logged in" };
|
|
1746
|
+
}
|
|
1747
|
+
const spinner = ora3("Loading keyword rankings...").start();
|
|
1748
|
+
try {
|
|
1749
|
+
const project = await getProjectForUrl(options.url);
|
|
1750
|
+
if (!project) {
|
|
1751
|
+
spinner.fail("Project not found");
|
|
1752
|
+
return { success: false, error: "Project not found" };
|
|
1753
|
+
}
|
|
1754
|
+
const supabase = await getAuthenticatedClient();
|
|
1755
|
+
if (!supabase) {
|
|
1756
|
+
spinner.fail("Could not connect to database");
|
|
1757
|
+
return { success: false, error: "Database connection failed" };
|
|
1758
|
+
}
|
|
1759
|
+
const { data, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true).order("current_position", { ascending: true, nullsFirst: false });
|
|
1760
|
+
if (error) {
|
|
1761
|
+
spinner.fail(`Failed to load keywords: ${error.message}`);
|
|
1762
|
+
return { success: false, error: error.message };
|
|
1763
|
+
}
|
|
1764
|
+
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1765
|
+
if (!data || data.length === 0) {
|
|
1766
|
+
console.log(chalk4.dim("\nNo keywords tracked yet."));
|
|
1767
|
+
console.log(chalk4.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
|
|
1768
|
+
`));
|
|
1769
|
+
return { success: true, keywords: [] };
|
|
1770
|
+
}
|
|
1771
|
+
const keywords2 = data.map((row) => ({
|
|
1772
|
+
keyword: row.keyword,
|
|
1773
|
+
currentPosition: row.current_position,
|
|
1774
|
+
previousPosition: row.previous_position,
|
|
1775
|
+
bestPosition: row.best_position,
|
|
1776
|
+
change: (row.previous_position || 0) - (row.current_position || 0),
|
|
1777
|
+
lastChecked: row.last_checked ? new Date(row.last_checked) : null
|
|
1778
|
+
}));
|
|
1779
|
+
if (options.output === "json") {
|
|
1780
|
+
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1781
|
+
} else {
|
|
1782
|
+
console.log("\n" + chalk4.bold("\u{1F4CA} Keyword Rankings"));
|
|
1783
|
+
console.log("\u2550".repeat(60));
|
|
1784
|
+
const tableData = [
|
|
1785
|
+
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
1786
|
+
...keywords2.map((kw) => [
|
|
1787
|
+
kw.keyword,
|
|
1788
|
+
formatPosition(kw.currentPosition),
|
|
1789
|
+
formatPosition(kw.bestPosition),
|
|
1790
|
+
formatChange(kw.change),
|
|
1791
|
+
kw.lastChecked ? kw.lastChecked.toLocaleDateString() : "Never"
|
|
1792
|
+
])
|
|
1793
|
+
];
|
|
1794
|
+
console.log(table(tableData, {
|
|
1795
|
+
border: {
|
|
1796
|
+
topBody: "\u2500",
|
|
1797
|
+
topJoin: "\u252C",
|
|
1798
|
+
topLeft: "\u250C",
|
|
1799
|
+
topRight: "\u2510",
|
|
1800
|
+
bottomBody: "\u2500",
|
|
1801
|
+
bottomJoin: "\u2534",
|
|
1802
|
+
bottomLeft: "\u2514",
|
|
1803
|
+
bottomRight: "\u2518",
|
|
1804
|
+
bodyLeft: "\u2502",
|
|
1805
|
+
bodyRight: "\u2502",
|
|
1806
|
+
bodyJoin: "\u2502",
|
|
1807
|
+
joinBody: "\u2500",
|
|
1808
|
+
joinLeft: "\u251C",
|
|
1809
|
+
joinRight: "\u2524",
|
|
1810
|
+
joinJoin: "\u253C"
|
|
1811
|
+
}
|
|
1812
|
+
}));
|
|
1813
|
+
}
|
|
1814
|
+
return { success: true, keywords: keywords2 };
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
spinner.fail("Failed to load rankings");
|
|
1817
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
async function trackHistory(options) {
|
|
1821
|
+
if (!options.url) {
|
|
1822
|
+
return { success: false, error: "No URL provided" };
|
|
1823
|
+
}
|
|
1824
|
+
if (!options.keyword) {
|
|
1825
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
1826
|
+
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
1827
|
+
return { success: false, error: "No keyword provided" };
|
|
1828
|
+
}
|
|
1829
|
+
if (!isLoggedIn()) {
|
|
1830
|
+
return { success: false, error: "Not logged in" };
|
|
1831
|
+
}
|
|
1832
|
+
const spinner = ora3(`Loading history for "${options.keyword}"...`).start();
|
|
1833
|
+
try {
|
|
1834
|
+
const project = await getProjectForUrl(options.url);
|
|
1835
|
+
if (!project) {
|
|
1836
|
+
spinner.fail("Project not found");
|
|
1837
|
+
return { success: false, error: "Project not found" };
|
|
1838
|
+
}
|
|
1839
|
+
const supabase = await getAuthenticatedClient();
|
|
1840
|
+
if (!supabase) {
|
|
1841
|
+
spinner.fail("Could not connect to database");
|
|
1842
|
+
return { success: false, error: "Database connection failed" };
|
|
1843
|
+
}
|
|
1844
|
+
const { data: keywordData } = await supabase.from("keywords").select("id, keyword").eq("project_id", project.id).eq("keyword", options.keyword.toLowerCase().trim()).single();
|
|
1845
|
+
if (!keywordData) {
|
|
1846
|
+
spinner.fail("Keyword not found");
|
|
1847
|
+
return { success: false, error: "Keyword not found" };
|
|
1848
|
+
}
|
|
1849
|
+
const days = options.days || 30;
|
|
1850
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
1851
|
+
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 });
|
|
1852
|
+
if (error) {
|
|
1853
|
+
spinner.fail(`Failed to load history: ${error.message}`);
|
|
1854
|
+
return { success: false, error: error.message };
|
|
1855
|
+
}
|
|
1856
|
+
spinner.succeed(`Loaded ${historyData?.length || 0} data points`);
|
|
1857
|
+
const history = (historyData || []).map((row) => ({
|
|
1858
|
+
position: row.position,
|
|
1859
|
+
url: row.url,
|
|
1860
|
+
recordedAt: new Date(row.recorded_at)
|
|
1861
|
+
}));
|
|
1862
|
+
if (options.output === "json") {
|
|
1863
|
+
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
1864
|
+
} else {
|
|
1865
|
+
console.log("\n" + chalk4.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
1866
|
+
console.log("\u2550".repeat(50));
|
|
1867
|
+
if (history.length === 0) {
|
|
1868
|
+
console.log(chalk4.dim("\nNo history available yet.\n"));
|
|
1869
|
+
} else {
|
|
1870
|
+
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
1871
|
+
const sparkline = positions.map((p) => {
|
|
1872
|
+
if (p <= 10) return chalk4.green("\u2588");
|
|
1873
|
+
if (p <= 20) return chalk4.yellow("\u2593");
|
|
1874
|
+
if (p <= 50) return chalk4.red("\u2592");
|
|
1875
|
+
return chalk4.dim("\u2591");
|
|
1876
|
+
}).join("");
|
|
1877
|
+
console.log(`
|
|
1878
|
+
${sparkline} (last ${positions.length} checks)`);
|
|
1879
|
+
console.log(chalk4.dim(" \u2190 newer older \u2192\n"));
|
|
1880
|
+
const recent = history.slice(0, 10);
|
|
1881
|
+
for (const entry of recent) {
|
|
1882
|
+
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return { success: true, history };
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
spinner.fail("Failed to load history");
|
|
1889
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async function trackExport(options) {
|
|
1893
|
+
if (!options.url) {
|
|
1894
|
+
return { success: false, error: "No URL provided" };
|
|
1895
|
+
}
|
|
1896
|
+
if (!isLoggedIn()) {
|
|
1897
|
+
return { success: false, error: "Not logged in" };
|
|
1898
|
+
}
|
|
1899
|
+
const spinner = ora3("Exporting data...").start();
|
|
1900
|
+
try {
|
|
1901
|
+
const project = await getProjectForUrl(options.url);
|
|
1902
|
+
if (!project) {
|
|
1903
|
+
spinner.fail("Project not found");
|
|
1904
|
+
return { success: false, error: "Project not found" };
|
|
1905
|
+
}
|
|
1906
|
+
const supabase = await getAuthenticatedClient();
|
|
1907
|
+
if (!supabase) {
|
|
1908
|
+
spinner.fail("Could not connect to database");
|
|
1909
|
+
return { success: false, error: "Database connection failed" };
|
|
1910
|
+
}
|
|
1911
|
+
const { data: keywords2, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
1912
|
+
if (error) {
|
|
1913
|
+
spinner.fail(`Failed to export: ${error.message}`);
|
|
1914
|
+
return { success: false, error: error.message };
|
|
1915
|
+
}
|
|
1916
|
+
const format = options.format || "csv";
|
|
1917
|
+
if (format === "json") {
|
|
1918
|
+
const data = JSON.stringify({
|
|
1919
|
+
domain: project.domain,
|
|
1920
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1921
|
+
keywords: keywords2?.map((kw) => ({
|
|
1922
|
+
keyword: kw.keyword,
|
|
1923
|
+
currentPosition: kw.current_position,
|
|
1924
|
+
bestPosition: kw.best_position,
|
|
1925
|
+
lastChecked: kw.last_checked
|
|
1926
|
+
}))
|
|
1927
|
+
}, null, 2);
|
|
1928
|
+
spinner.succeed("Exported as JSON");
|
|
1929
|
+
console.log(data);
|
|
1930
|
+
return { success: true, data };
|
|
1931
|
+
} else {
|
|
1932
|
+
const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
|
|
1933
|
+
for (const kw of keywords2 || []) {
|
|
1934
|
+
const trend = (kw.previous_position || 0) - (kw.current_position || 0);
|
|
1935
|
+
const trendStr = trend > 0 ? "\u2191" : trend < 0 ? "\u2193" : "\u2192";
|
|
1936
|
+
rows.push([
|
|
1937
|
+
`"${kw.keyword}"`,
|
|
1938
|
+
kw.current_position?.toString() || "N/A",
|
|
1939
|
+
kw.best_position?.toString() || "N/A",
|
|
1940
|
+
kw.last_checked || "Never",
|
|
1941
|
+
trendStr
|
|
1942
|
+
].join(","));
|
|
1943
|
+
}
|
|
1944
|
+
const data = rows.join("\n");
|
|
1945
|
+
spinner.succeed("Exported as CSV");
|
|
1946
|
+
console.log(data);
|
|
1947
|
+
return { success: true, data };
|
|
1948
|
+
}
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
spinner.fail("Failed to export");
|
|
1951
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
async function trackCheck(options) {
|
|
1955
|
+
if (!options.url) {
|
|
1956
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1957
|
+
console.log(" rankcli track check --url https://your-site.com\n");
|
|
1958
|
+
return { success: false, error: "No URL provided" };
|
|
1959
|
+
}
|
|
1960
|
+
if (!isLoggedIn()) {
|
|
1961
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
1962
|
+
console.log(" rankcli login\n");
|
|
1963
|
+
return { success: false, error: "Not logged in" };
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
const project = await getProjectForUrl(options.url);
|
|
1967
|
+
if (!project) {
|
|
1968
|
+
return { success: false, error: "Project not found" };
|
|
1969
|
+
}
|
|
1970
|
+
const { planName } = getUserInfo();
|
|
1971
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1972
|
+
const { checksPerDay } = getTierLimits(tier);
|
|
1973
|
+
if (!options.force && project.lastRankCheckAt) {
|
|
1974
|
+
const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
|
|
1975
|
+
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
1976
|
+
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
1977
|
+
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
1978
|
+
console.log(chalk4.yellow(`
|
|
1979
|
+
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
1980
|
+
console.log(chalk4.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
1981
|
+
`));
|
|
1982
|
+
return { success: true, rateLimited: true };
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const spinner = ora3("Checking current rankings...").start();
|
|
1986
|
+
const supabase = await getAuthenticatedClient();
|
|
1987
|
+
if (!supabase) {
|
|
1988
|
+
spinner.fail("Could not connect to database");
|
|
1989
|
+
return { success: false, error: "Database connection failed" };
|
|
1990
|
+
}
|
|
1991
|
+
const { data: keywords2 } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
1992
|
+
if (!keywords2 || keywords2.length === 0) {
|
|
1993
|
+
spinner.fail("No keywords to check");
|
|
1994
|
+
return { success: false, error: "No keywords tracked" };
|
|
1995
|
+
}
|
|
1996
|
+
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
1997
|
+
if (!serpApiKey) {
|
|
1998
|
+
spinner.warn("No SERP API key configured");
|
|
1999
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
|
|
2000
|
+
console.log(chalk4.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
|
|
2001
|
+
return { success: false, error: "No SERP API key" };
|
|
2002
|
+
}
|
|
2003
|
+
const tracker = new RankTracker({
|
|
2004
|
+
supabase,
|
|
2005
|
+
serpConfig: {
|
|
2006
|
+
provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
|
|
2007
|
+
apiKey: serpApiKey
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2011
|
+
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2012
|
+
console.log("\n" + chalk4.bold("\u{1F50D} Rank Check Results"));
|
|
2013
|
+
console.log("\u2550".repeat(50));
|
|
2014
|
+
for (const result of results) {
|
|
2015
|
+
const posStr = formatPosition(result.position);
|
|
2016
|
+
console.log(` ${result.keyword}: ${posStr}`);
|
|
2017
|
+
if (result.serpFeatures.length > 0) {
|
|
2018
|
+
console.log(chalk4.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
console.log("");
|
|
2022
|
+
return {
|
|
2023
|
+
success: true,
|
|
2024
|
+
checkResults: results.map((r) => ({
|
|
2025
|
+
keyword: r.keyword,
|
|
2026
|
+
position: r.position,
|
|
2027
|
+
url: r.url
|
|
2028
|
+
}))
|
|
2029
|
+
};
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1038
2035
|
// src/commands/geo.ts
|
|
1039
2036
|
import { analyzers } from "@rankcli/agent-runtime";
|
|
1040
|
-
import
|
|
1041
|
-
import
|
|
2037
|
+
import ora4 from "ora";
|
|
2038
|
+
import chalk5 from "chalk";
|
|
1042
2039
|
async function fetchResources(url) {
|
|
1043
2040
|
const headers = {};
|
|
1044
2041
|
let html = null;
|
|
@@ -1084,7 +2081,7 @@ async function geo(options) {
|
|
|
1084
2081
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1085
2082
|
return;
|
|
1086
2083
|
}
|
|
1087
|
-
const spinner =
|
|
2084
|
+
const spinner = ora4("Fetching page and robots.txt...").start();
|
|
1088
2085
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
1089
2086
|
if (!html) {
|
|
1090
2087
|
spinner.fail("Could not fetch page content");
|
|
@@ -1110,55 +2107,55 @@ async function geo(options) {
|
|
|
1110
2107
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1111
2108
|
return;
|
|
1112
2109
|
}
|
|
1113
|
-
console.log("\n" +
|
|
1114
|
-
console.log(
|
|
1115
|
-
console.log(
|
|
2110
|
+
console.log("\n" + chalk5.bold("\u2550".repeat(60)));
|
|
2111
|
+
console.log(chalk5.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2112
|
+
console.log(chalk5.bold("\u2550".repeat(60)));
|
|
1116
2113
|
const geoScore = geoResult.score;
|
|
1117
|
-
const scoreColor = geoScore >= 80 ?
|
|
2114
|
+
const scoreColor = geoScore >= 80 ? chalk5.green : geoScore >= 60 ? chalk5.yellow : chalk5.red;
|
|
1118
2115
|
console.log(`
|
|
1119
|
-
${
|
|
1120
|
-
console.log("\n" +
|
|
2116
|
+
${chalk5.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2117
|
+
console.log("\n" + chalk5.bold(" AI Crawler Access:"));
|
|
1121
2118
|
const { aiCrawlerAccess } = geoResult;
|
|
1122
2119
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
1123
|
-
console.log(
|
|
2120
|
+
console.log(chalk5.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
1124
2121
|
}
|
|
1125
2122
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
1126
2123
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
1127
|
-
console.log(
|
|
2124
|
+
console.log(chalk5.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
1128
2125
|
}
|
|
1129
|
-
console.log("\n" +
|
|
2126
|
+
console.log("\n" + chalk5.bold(" Rendering:"));
|
|
1130
2127
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
1131
|
-
console.log(
|
|
2128
|
+
console.log(chalk5.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
1132
2129
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
1133
|
-
console.log(
|
|
2130
|
+
console.log(chalk5.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
1134
2131
|
}
|
|
1135
|
-
console.log("\n" +
|
|
2132
|
+
console.log("\n" + chalk5.bold(" LLM Friendliness:"));
|
|
1136
2133
|
const { llmSignals } = geoResult;
|
|
1137
2134
|
const bar = (score) => {
|
|
1138
2135
|
const filled = Math.round(score / 5);
|
|
1139
2136
|
const empty = 20 - filled;
|
|
1140
|
-
const color = score >= 70 ?
|
|
1141
|
-
return color("\u2588".repeat(filled)) +
|
|
2137
|
+
const color = score >= 70 ? chalk5.green : score >= 40 ? chalk5.yellow : chalk5.red;
|
|
2138
|
+
return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
|
|
1142
2139
|
};
|
|
1143
2140
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
1144
2141
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
1145
2142
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
1146
2143
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
1147
|
-
console.log("\n" +
|
|
2144
|
+
console.log("\n" + chalk5.bold(" Content Structure:"));
|
|
1148
2145
|
const { contentStructure } = geoResult;
|
|
1149
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
1150
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
1151
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
1152
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
1153
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
1154
|
-
console.log("\n" +
|
|
2146
|
+
console.log(` ${contentStructure.hasStructuredData ? chalk5.green("\u2705") : chalk5.red("\u274C")} JSON-LD Structured Data`);
|
|
2147
|
+
console.log(` ${contentStructure.hasFAQSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} FAQ Schema`);
|
|
2148
|
+
console.log(` ${contentStructure.hasArticleSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Article Schema`);
|
|
2149
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Breadcrumbs`);
|
|
2150
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk5.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk5.yellow("Needs Work") : chalk5.red("Poor")}`);
|
|
2151
|
+
console.log("\n" + chalk5.bold(" Citation Readiness:"));
|
|
1155
2152
|
const { citationReadiness } = geoResult;
|
|
1156
2153
|
if (citationReadiness.trustSignals.length > 0) {
|
|
1157
|
-
console.log(
|
|
2154
|
+
console.log(chalk5.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
1158
2155
|
} else {
|
|
1159
|
-
console.log(
|
|
2156
|
+
console.log(chalk5.yellow(" \u25CB No trust signals detected"));
|
|
1160
2157
|
}
|
|
1161
|
-
console.log("\n" +
|
|
2158
|
+
console.log("\n" + chalk5.bold(" Other Scores:"));
|
|
1162
2159
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
1163
2160
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
1164
2161
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -1172,18 +2169,18 @@ async function geo(options) {
|
|
|
1172
2169
|
];
|
|
1173
2170
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
1174
2171
|
if (criticalIssues.length > 0) {
|
|
1175
|
-
console.log("\n" +
|
|
2172
|
+
console.log("\n" + chalk5.bold.red(" Critical Issues:"));
|
|
1176
2173
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
1177
|
-
console.log(
|
|
2174
|
+
console.log(chalk5.red(` ${i + 1}. ${issue.title}`));
|
|
1178
2175
|
});
|
|
1179
2176
|
}
|
|
1180
2177
|
if (geoResult.recommendations.length > 0) {
|
|
1181
|
-
console.log("\n" +
|
|
2178
|
+
console.log("\n" + chalk5.bold(" Recommendations:"));
|
|
1182
2179
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
1183
2180
|
console.log(` ${i + 1}. ${rec}`);
|
|
1184
2181
|
});
|
|
1185
2182
|
}
|
|
1186
|
-
console.log("\n" +
|
|
2183
|
+
console.log("\n" + chalk5.bold("\u2550".repeat(60)));
|
|
1187
2184
|
console.log("");
|
|
1188
2185
|
} catch (error) {
|
|
1189
2186
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1211,7 +2208,7 @@ async function keywords(options) {
|
|
|
1211
2208
|
input: process.stdin,
|
|
1212
2209
|
output: process.stdout
|
|
1213
2210
|
});
|
|
1214
|
-
const
|
|
2211
|
+
const ask2 = (question) => {
|
|
1215
2212
|
return new Promise((resolve) => {
|
|
1216
2213
|
rl.question(question, (answer) => {
|
|
1217
2214
|
resolve(answer.trim());
|
|
@@ -1226,7 +2223,7 @@ ${question}`);
|
|
|
1226
2223
|
console.log(` ${choice.description}`);
|
|
1227
2224
|
});
|
|
1228
2225
|
while (true) {
|
|
1229
|
-
const answer = await
|
|
2226
|
+
const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
|
|
1230
2227
|
const num = parseInt(answer, 10);
|
|
1231
2228
|
if (num >= 1 && num <= choices.length) {
|
|
1232
2229
|
return choices[num - 1].value;
|
|
@@ -1237,7 +2234,7 @@ ${question}`);
|
|
|
1237
2234
|
try {
|
|
1238
2235
|
let url = options.url;
|
|
1239
2236
|
if (!url) {
|
|
1240
|
-
url = await
|
|
2237
|
+
url = await ask2("Enter your website URL: ");
|
|
1241
2238
|
}
|
|
1242
2239
|
try {
|
|
1243
2240
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
@@ -1261,7 +2258,7 @@ ${question}`);
|
|
|
1261
2258
|
if (seedKeywords.length === 0) {
|
|
1262
2259
|
console.log("\n\u{1F4DD} What is your website/product about?");
|
|
1263
2260
|
console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
|
|
1264
|
-
const seedInput = await
|
|
2261
|
+
const seedInput = await ask2(" > ");
|
|
1265
2262
|
seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1266
2263
|
if (seedKeywords.length === 0) {
|
|
1267
2264
|
console.log("\n\u274C Please provide at least one seed keyword.\n");
|
|
@@ -1721,7 +2718,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1721
2718
|
input: process.stdin,
|
|
1722
2719
|
output: process.stdout
|
|
1723
2720
|
});
|
|
1724
|
-
const
|
|
2721
|
+
const ask2 = (question) => {
|
|
1725
2722
|
return new Promise((resolve) => {
|
|
1726
2723
|
rl.question(question, (answer) => {
|
|
1727
2724
|
resolve(answer.trim());
|
|
@@ -1735,7 +2732,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1735
2732
|
console.log("GA4 tracks visitor behavior on your site.");
|
|
1736
2733
|
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
1737
2734
|
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
1738
|
-
const ga4Id = await
|
|
2735
|
+
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
1739
2736
|
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
1740
2737
|
await setupGA4(projectPath, ga4Id);
|
|
1741
2738
|
} else if (ga4Id) {
|
|
@@ -1748,7 +2745,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1748
2745
|
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
1749
2746
|
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
1750
2747
|
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
1751
|
-
const gscCode = await
|
|
2748
|
+
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
1752
2749
|
if (gscCode && gscCode.length > 10) {
|
|
1753
2750
|
await setupGSC(projectPath, gscCode);
|
|
1754
2751
|
} else if (gscCode) {
|
|
@@ -1760,20 +2757,20 @@ async function interactiveSetup(projectPath) {
|
|
|
1760
2757
|
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
1761
2758
|
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
1762
2759
|
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
1763
|
-
const setupAction = await
|
|
2760
|
+
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
1764
2761
|
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
1765
2762
|
console.log("\nHow often should the check run?");
|
|
1766
2763
|
console.log(" 1. Weekly (recommended for most sites)");
|
|
1767
2764
|
console.log(" 2. Daily (for high-traffic sites)");
|
|
1768
2765
|
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
1769
|
-
const scheduleChoice = await
|
|
2766
|
+
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
1770
2767
|
const scheduleMap = {
|
|
1771
2768
|
"1": "weekly",
|
|
1772
2769
|
"2": "daily",
|
|
1773
2770
|
"3": "monthly"
|
|
1774
2771
|
};
|
|
1775
2772
|
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
1776
|
-
const siteUrl = await
|
|
2773
|
+
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
1777
2774
|
if (siteUrl) {
|
|
1778
2775
|
await setupGitHubAction(projectPath, schedule, siteUrl);
|
|
1779
2776
|
}
|
|
@@ -1861,31 +2858,196 @@ function showGSCInstructions() {
|
|
|
1861
2858
|
console.log(getGSCSetupInstructions());
|
|
1862
2859
|
}
|
|
1863
2860
|
|
|
2861
|
+
// src/commands/alerts.ts
|
|
2862
|
+
import * as readline3 from "readline";
|
|
2863
|
+
var ALERT_TYPES = [
|
|
2864
|
+
{ id: "error", label: "Critical errors detected", default: true },
|
|
2865
|
+
{ id: "score_drop", label: "Score drops (configurable threshold)", default: true },
|
|
2866
|
+
{ id: "weekly_report", label: "Weekly summary reports", default: false },
|
|
2867
|
+
{ id: "pr_created", label: "Fix PRs created", default: true }
|
|
2868
|
+
];
|
|
2869
|
+
async function ask(rl, question) {
|
|
2870
|
+
return new Promise((resolve) => {
|
|
2871
|
+
rl.question(question, (answer) => {
|
|
2872
|
+
resolve(answer.trim());
|
|
2873
|
+
});
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
2877
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
2878
|
+
const answer = await ask(rl, `${question} ${suffix}: `);
|
|
2879
|
+
if (answer === "") return defaultYes;
|
|
2880
|
+
return answer.toLowerCase().startsWith("y");
|
|
2881
|
+
}
|
|
2882
|
+
async function alerts(options) {
|
|
2883
|
+
console.log("\n\u{1F514} SEO Autopilot - Alert Configuration\n");
|
|
2884
|
+
const rl = readline3.createInterface({
|
|
2885
|
+
input: process.stdin,
|
|
2886
|
+
output: process.stdout
|
|
2887
|
+
});
|
|
2888
|
+
try {
|
|
2889
|
+
if (options.list) {
|
|
2890
|
+
console.log("\u{1F4CB} Current alert configurations:\n");
|
|
2891
|
+
console.log(" (Alert configs are stored in your RankCLI account)");
|
|
2892
|
+
console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
|
|
2893
|
+
rl.close();
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
if (options.test && options.webhook) {
|
|
2897
|
+
await testWebhook(options.channel || "slack", options.webhook);
|
|
2898
|
+
rl.close();
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
console.log("Configure notifications for SEO audit events.\n");
|
|
2902
|
+
console.log("Supported channels:");
|
|
2903
|
+
console.log(" \u2022 Slack - Use an Incoming Webhook URL");
|
|
2904
|
+
console.log(" \u2022 Discord - Use a Channel Webhook URL\n");
|
|
2905
|
+
const channelChoice = await ask(rl, "Which channel? (slack/discord): ");
|
|
2906
|
+
const channel = channelChoice.toLowerCase();
|
|
2907
|
+
if (channel !== "slack" && channel !== "discord") {
|
|
2908
|
+
console.log('\n\u274C Invalid channel. Please choose "slack" or "discord".\n');
|
|
2909
|
+
rl.close();
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
console.log("");
|
|
2913
|
+
if (channel === "slack") {
|
|
2914
|
+
console.log("To create a Slack webhook:");
|
|
2915
|
+
console.log("1. Go to https://api.slack.com/apps");
|
|
2916
|
+
console.log("2. Create an app (or use existing)");
|
|
2917
|
+
console.log('3. Enable "Incoming Webhooks"');
|
|
2918
|
+
console.log("4. Add a new webhook to your workspace");
|
|
2919
|
+
console.log("5. Copy the webhook URL\n");
|
|
2920
|
+
} else {
|
|
2921
|
+
console.log("To create a Discord webhook:");
|
|
2922
|
+
console.log("1. Open Discord channel settings");
|
|
2923
|
+
console.log("2. Go to Integrations \u2192 Webhooks");
|
|
2924
|
+
console.log("3. Create a new webhook");
|
|
2925
|
+
console.log("4. Copy the webhook URL\n");
|
|
2926
|
+
}
|
|
2927
|
+
const webhookUrl = await ask(rl, "Webhook URL: ");
|
|
2928
|
+
if (!webhookUrl.startsWith("https://")) {
|
|
2929
|
+
console.log("\n\u274C Invalid webhook URL. Must start with https://\n");
|
|
2930
|
+
rl.close();
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
console.log("\nWhich events should trigger alerts?\n");
|
|
2934
|
+
const selectedTypes = [];
|
|
2935
|
+
for (const alertType of ALERT_TYPES) {
|
|
2936
|
+
const enabled = await askYesNo(rl, ` ${alertType.label}`, alertType.default);
|
|
2937
|
+
if (enabled) {
|
|
2938
|
+
selectedTypes.push(alertType.id);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
let scoreDropThreshold = 10;
|
|
2942
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2943
|
+
const thresholdStr = await ask(rl, "\nMinimum score drop to alert (default: 10): ");
|
|
2944
|
+
if (thresholdStr) {
|
|
2945
|
+
const parsed = parseInt(thresholdStr, 10);
|
|
2946
|
+
if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
|
|
2947
|
+
scoreDropThreshold = parsed;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
console.log("\n\u{1F4E4} Testing webhook...");
|
|
2952
|
+
const testResult = await testWebhook(channel, webhookUrl);
|
|
2953
|
+
if (!testResult) {
|
|
2954
|
+
console.log("\u274C Webhook test failed. Please check the URL and try again.\n");
|
|
2955
|
+
rl.close();
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
console.log("\u2705 Webhook test successful!\n");
|
|
2959
|
+
console.log("\u{1F4DD} Alert configuration:");
|
|
2960
|
+
console.log(` Channel: ${channel}`);
|
|
2961
|
+
console.log(` Events: ${selectedTypes.join(", ")}`);
|
|
2962
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2963
|
+
console.log(` Score drop threshold: ${scoreDropThreshold} points`);
|
|
2964
|
+
}
|
|
2965
|
+
console.log("");
|
|
2966
|
+
console.log("To save this configuration:");
|
|
2967
|
+
console.log("1. Log in with `rankcli auth login`");
|
|
2968
|
+
console.log("2. Add alert config at https://rankcli.dev/account/alerts");
|
|
2969
|
+
console.log("");
|
|
2970
|
+
console.log("Or use the API:");
|
|
2971
|
+
console.log(` POST /api/alerts with webhook_url and settings`);
|
|
2972
|
+
console.log("");
|
|
2973
|
+
rl.close();
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
console.error("Error:", error);
|
|
2976
|
+
rl.close();
|
|
2977
|
+
process.exit(1);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
async function testWebhook(channel, webhookUrl) {
|
|
2981
|
+
const testPayload = channel === "slack" ? {
|
|
2982
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2983
|
+
blocks: [
|
|
2984
|
+
{
|
|
2985
|
+
type: "header",
|
|
2986
|
+
text: {
|
|
2987
|
+
type: "plain_text",
|
|
2988
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2989
|
+
emoji: true
|
|
2990
|
+
}
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
type: "section",
|
|
2994
|
+
text: {
|
|
2995
|
+
type: "mrkdwn",
|
|
2996
|
+
text: "This is a test message from RankCLI. If you see this, your webhook is working correctly!"
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
]
|
|
3000
|
+
} : {
|
|
3001
|
+
embeds: [
|
|
3002
|
+
{
|
|
3003
|
+
title: "\u{1F514} RankCLI Alert Test",
|
|
3004
|
+
description: "This is a test message from RankCLI. If you see this, your webhook is working correctly!",
|
|
3005
|
+
color: 2450411,
|
|
3006
|
+
footer: {
|
|
3007
|
+
text: "\u{1F916} Sent by RankCLI"
|
|
3008
|
+
},
|
|
3009
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3010
|
+
}
|
|
3011
|
+
]
|
|
3012
|
+
};
|
|
3013
|
+
try {
|
|
3014
|
+
const response = await fetch(webhookUrl, {
|
|
3015
|
+
method: "POST",
|
|
3016
|
+
headers: { "Content-Type": "application/json" },
|
|
3017
|
+
body: JSON.stringify(testPayload)
|
|
3018
|
+
});
|
|
3019
|
+
return response.ok;
|
|
3020
|
+
} catch (error) {
|
|
3021
|
+
console.error("Webhook test error:", error);
|
|
3022
|
+
return false;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
1864
3026
|
// src/commands/auth.ts
|
|
1865
|
-
import
|
|
1866
|
-
import
|
|
3027
|
+
import chalk6 from "chalk";
|
|
3028
|
+
import ora5 from "ora";
|
|
1867
3029
|
import inquirer from "inquirer";
|
|
1868
3030
|
import open from "open";
|
|
1869
3031
|
async function login(options) {
|
|
1870
3032
|
if (isLoggedIn()) {
|
|
1871
3033
|
const { email } = getUserInfo();
|
|
1872
|
-
console.log(
|
|
1873
|
-
console.log(
|
|
3034
|
+
console.log(chalk6.yellow(`Already logged in as ${email}`));
|
|
3035
|
+
console.log(chalk6.dim('Run "rankcli logout" to sign out first.'));
|
|
1874
3036
|
return;
|
|
1875
3037
|
}
|
|
1876
|
-
const spinner =
|
|
3038
|
+
const spinner = ora5();
|
|
1877
3039
|
try {
|
|
1878
3040
|
if (options.token) {
|
|
1879
3041
|
const apiKey = options.token;
|
|
1880
3042
|
if (!apiKey.startsWith("rankcli_")) {
|
|
1881
|
-
console.log(
|
|
3043
|
+
console.log(chalk6.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
1882
3044
|
return;
|
|
1883
3045
|
}
|
|
1884
3046
|
spinner.start("Validating API key...");
|
|
1885
3047
|
const result = await validateApiKey(apiKey);
|
|
1886
3048
|
if (!result.valid) {
|
|
1887
3049
|
spinner.fail("Invalid API key");
|
|
1888
|
-
console.log(
|
|
3050
|
+
console.log(chalk6.red(`
|
|
1889
3051
|
${result.error}`));
|
|
1890
3052
|
return;
|
|
1891
3053
|
}
|
|
@@ -1898,14 +3060,14 @@ ${result.error}`));
|
|
|
1898
3060
|
});
|
|
1899
3061
|
}
|
|
1900
3062
|
spinner.succeed(`Authenticated via API key`);
|
|
1901
|
-
console.log(
|
|
3063
|
+
console.log(chalk6.dim(`
|
|
1902
3064
|
Email: ${result.user?.email}`));
|
|
1903
|
-
console.log(
|
|
3065
|
+
console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
1904
3066
|
return;
|
|
1905
3067
|
}
|
|
1906
3068
|
if (options.browser) {
|
|
1907
|
-
console.log(
|
|
1908
|
-
console.log(
|
|
3069
|
+
console.log(chalk6.cyan("\nOpening browser for login..."));
|
|
3070
|
+
console.log(chalk6.dim("Complete the login in your browser, then return here.\n"));
|
|
1909
3071
|
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
1910
3072
|
await open(loginUrl);
|
|
1911
3073
|
const { token } = await inquirer.prompt([
|
|
@@ -1921,7 +3083,7 @@ Email: ${result.user?.email}`));
|
|
|
1921
3083
|
const { data, error } = await supabase.auth.getUser(token);
|
|
1922
3084
|
if (error || !data.user) {
|
|
1923
3085
|
spinner.fail("Invalid token");
|
|
1924
|
-
console.log(
|
|
3086
|
+
console.log(chalk6.red("The token is invalid or expired. Please try again."));
|
|
1925
3087
|
return;
|
|
1926
3088
|
}
|
|
1927
3089
|
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
@@ -1935,7 +3097,7 @@ Email: ${result.user?.email}`));
|
|
|
1935
3097
|
}
|
|
1936
3098
|
saveSession(sessionData.session);
|
|
1937
3099
|
await fetchAndSaveSubscription();
|
|
1938
|
-
spinner.succeed(`Logged in as ${
|
|
3100
|
+
spinner.succeed(`Logged in as ${chalk6.green(data.user.email)}`);
|
|
1939
3101
|
} else {
|
|
1940
3102
|
let email = options.email;
|
|
1941
3103
|
if (!email) {
|
|
@@ -1968,11 +3130,11 @@ Email: ${result.user?.email}`));
|
|
|
1968
3130
|
});
|
|
1969
3131
|
if (error) {
|
|
1970
3132
|
spinner.fail("Login failed");
|
|
1971
|
-
console.log(
|
|
3133
|
+
console.log(chalk6.red(`
|
|
1972
3134
|
${error.message}`));
|
|
1973
3135
|
if (error.message.includes("Invalid login")) {
|
|
1974
|
-
console.log(
|
|
1975
|
-
console.log(
|
|
3136
|
+
console.log(chalk6.dim("\nDon't have an account? Sign up at:"));
|
|
3137
|
+
console.log(chalk6.cyan(` ${WEB_APP_URL}/signup`));
|
|
1976
3138
|
}
|
|
1977
3139
|
return;
|
|
1978
3140
|
}
|
|
@@ -1982,41 +3144,41 @@ ${error.message}`));
|
|
|
1982
3144
|
}
|
|
1983
3145
|
saveSession(data.session);
|
|
1984
3146
|
await fetchAndSaveSubscription();
|
|
1985
|
-
spinner.succeed(`Logged in as ${
|
|
3147
|
+
spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
|
|
1986
3148
|
const { planName, sitesLimit } = getUserInfo();
|
|
1987
|
-
console.log(
|
|
3149
|
+
console.log(chalk6.dim(`
|
|
1988
3150
|
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
1989
3151
|
}
|
|
1990
3152
|
} catch (err) {
|
|
1991
3153
|
spinner.fail("Login failed");
|
|
1992
|
-
console.error(
|
|
3154
|
+
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
1993
3155
|
}
|
|
1994
3156
|
}
|
|
1995
3157
|
async function logout() {
|
|
1996
3158
|
if (!isLoggedIn()) {
|
|
1997
|
-
console.log(
|
|
3159
|
+
console.log(chalk6.yellow("Not logged in"));
|
|
1998
3160
|
return;
|
|
1999
3161
|
}
|
|
2000
3162
|
const { email } = getUserInfo();
|
|
2001
|
-
const spinner =
|
|
3163
|
+
const spinner = ora5("Signing out...").start();
|
|
2002
3164
|
try {
|
|
2003
3165
|
const supabase = getSupabaseClient();
|
|
2004
3166
|
await supabase.auth.signOut();
|
|
2005
3167
|
clearCredentials();
|
|
2006
|
-
spinner.succeed(`Logged out from ${
|
|
3168
|
+
spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
|
|
2007
3169
|
} catch (err) {
|
|
2008
3170
|
spinner.fail("Logout failed");
|
|
2009
|
-
console.error(
|
|
3171
|
+
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2010
3172
|
}
|
|
2011
3173
|
}
|
|
2012
3174
|
async function whoami() {
|
|
2013
3175
|
if (!isLoggedIn()) {
|
|
2014
|
-
console.log(
|
|
2015
|
-
console.log(
|
|
2016
|
-
console.log(
|
|
3176
|
+
console.log(chalk6.yellow("Not logged in"));
|
|
3177
|
+
console.log(chalk6.dim('\nRun "rankcli login" to sign in.'));
|
|
3178
|
+
console.log(chalk6.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
2017
3179
|
return;
|
|
2018
3180
|
}
|
|
2019
|
-
const spinner =
|
|
3181
|
+
const spinner = ora5("Checking account...").start();
|
|
2020
3182
|
try {
|
|
2021
3183
|
if (isApiKeyAuth()) {
|
|
2022
3184
|
const apiKey = getApiKey();
|
|
@@ -2036,18 +3198,18 @@ async function whoami() {
|
|
|
2036
3198
|
const { email, planName, sitesLimit } = getUserInfo();
|
|
2037
3199
|
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
2038
3200
|
spinner.stop();
|
|
2039
|
-
console.log(
|
|
3201
|
+
console.log(chalk6.bold("\nRankCLI Account"));
|
|
2040
3202
|
console.log("\u2500".repeat(40));
|
|
2041
|
-
console.log(` Email: ${
|
|
2042
|
-
console.log(` Plan: ${
|
|
3203
|
+
console.log(` Email: ${chalk6.cyan(email || "N/A")}`);
|
|
3204
|
+
console.log(` Plan: ${chalk6.green(planName)}`);
|
|
2043
3205
|
console.log(` Sites: ${sitesLimit} allowed`);
|
|
2044
|
-
console.log(` Auth: ${
|
|
3206
|
+
console.log(` Auth: ${chalk6.dim(authMethod)}`);
|
|
2045
3207
|
console.log("\u2500".repeat(40));
|
|
2046
|
-
console.log(
|
|
3208
|
+
console.log(chalk6.dim(`
|
|
2047
3209
|
Manage at: ${WEB_APP_URL}/account`));
|
|
2048
3210
|
} catch (err) {
|
|
2049
3211
|
spinner.fail("Failed to fetch account info");
|
|
2050
|
-
console.error(
|
|
3212
|
+
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2051
3213
|
}
|
|
2052
3214
|
}
|
|
2053
3215
|
async function fetchAndSaveSubscription() {
|
|
@@ -2079,7 +3241,7 @@ async function fetchAndSaveSubscription() {
|
|
|
2079
3241
|
}
|
|
2080
3242
|
|
|
2081
3243
|
// src/version.ts
|
|
2082
|
-
var VERSION = "0.0.
|
|
3244
|
+
var VERSION = "0.0.20";
|
|
2083
3245
|
|
|
2084
3246
|
// src/index.ts
|
|
2085
3247
|
var program = new Command();
|
|
@@ -2091,6 +3253,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
|
|
|
2091
3253
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
2092
3254
|
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);
|
|
2093
3255
|
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);
|
|
3256
|
+
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) => {
|
|
3257
|
+
const result = await fix({
|
|
3258
|
+
url: options.url,
|
|
3259
|
+
auto: options.auto,
|
|
3260
|
+
commit: options.commit,
|
|
3261
|
+
push: options.push,
|
|
3262
|
+
dryRun: options.dryRun,
|
|
3263
|
+
only: options.only,
|
|
3264
|
+
output: options.output
|
|
3265
|
+
});
|
|
3266
|
+
if (options.output === "json" && result.json) {
|
|
3267
|
+
console.log(result.json);
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
2094
3270
|
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);
|
|
2095
3271
|
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) => {
|
|
2096
3272
|
if (options.ai && options.url) {
|
|
@@ -2113,8 +3289,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
|
|
|
2113
3289
|
program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
|
|
2114
3290
|
console.log("Competitor analysis coming soon...");
|
|
2115
3291
|
});
|
|
2116
|
-
program.command("
|
|
2117
|
-
|
|
3292
|
+
var trackCmd = program.command("track").description("Keyword rank tracking");
|
|
3293
|
+
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) => {
|
|
3294
|
+
await trackAdd({
|
|
3295
|
+
keywords: keywords2,
|
|
3296
|
+
url: options.url,
|
|
3297
|
+
searchEngine: options.engine,
|
|
3298
|
+
country: options.country,
|
|
3299
|
+
language: options.language
|
|
3300
|
+
});
|
|
3301
|
+
});
|
|
3302
|
+
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) => {
|
|
3303
|
+
await trackStatus({
|
|
3304
|
+
url: options.url,
|
|
3305
|
+
output: options.output
|
|
3306
|
+
});
|
|
3307
|
+
});
|
|
3308
|
+
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) => {
|
|
3309
|
+
await trackHistory({
|
|
3310
|
+
url: options.url,
|
|
3311
|
+
keyword: options.keyword,
|
|
3312
|
+
days: parseInt(options.days, 10),
|
|
3313
|
+
output: options.output
|
|
3314
|
+
});
|
|
3315
|
+
});
|
|
3316
|
+
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) => {
|
|
3317
|
+
await trackCheck({
|
|
3318
|
+
url: options.url,
|
|
3319
|
+
force: options.force
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
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) => {
|
|
3323
|
+
await trackExport({
|
|
3324
|
+
url: options.url,
|
|
3325
|
+
format: options.format,
|
|
3326
|
+
days: parseInt(options.days, 10)
|
|
3327
|
+
});
|
|
2118
3328
|
});
|
|
2119
3329
|
program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
|
|
2120
3330
|
console.log("Report generation coming soon...");
|
|
@@ -2124,4 +3334,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
|
|
|
2124
3334
|
});
|
|
2125
3335
|
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);
|
|
2126
3336
|
program.command("gsc-setup").description("Show Google Search Console API setup instructions").action(showGSCInstructions);
|
|
3337
|
+
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);
|
|
2127
3338
|
program.parse();
|