@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.
Files changed (4) hide show
  1. package/README.md +17 -12
  2. package/dist/index.js +1417 -210
  3. package/dist/index.mjs +1379 -168
  4. 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 chalk from "chalk";
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(chalk.yellow(`AI analysis failed: ${response.status}`));
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(chalk.yellow(`AI analysis error: ${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(chalk.yellow(`
757
+ console.log(chalk2.yellow(`
573
758
  \u26A0\uFE0F Project limit reached (${count}/${limit})`));
574
- console.log(chalk.dim(` Upgrade at: ${WEB_APP_URL}/account`));
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(chalk.dim("Failed to create project"));
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: mergedIssues.length > 0 ? 200 : 0,
607
- // Approximate
794
+ checks_run: checksRun,
608
795
  health_scores: healthScores,
609
- completed_at: (/* @__PURE__ */ new Date()).toISOString(),
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(chalk.dim("Failed to save audit"));
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: (/* @__PURE__ */ new Date()).toISOString(),
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(chalk.dim("Cloud sync failed"));
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(chalk.dim("\nTip: Log in to sync results to the web dashboard:"));
668
- console.log(chalk.dim(" rankcli login"));
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(chalk.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
753
- console.log(chalk.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
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" + chalk.bold("\u2550".repeat(60)));
771
- console.log(chalk.bold.cyan(` SEO AUDIT REPORT: ${url}`));
772
- console.log(chalk.bold("\u2550".repeat(60)));
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 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
977
+ const scoreColor = score >= 80 ? chalk2.green : score >= 60 ? chalk2.yellow : chalk2.red;
775
978
  console.log(`
776
- ${chalk.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
777
- console.log(` ${chalk.bold("Pages audited:")} ${successCount}`);
778
- console.log(` ${chalk.bold("Issues found:")} ${mergedIssues.length}`);
779
- console.log(` ${chalk.bold("Time:")} ${totalTime}ms`);
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(` ${chalk.bold("Framework:")} ${chalk.cyan(frameworkName)}`);
984
+ console.log(` ${chalk2.bold("Framework:")} ${chalk2.cyan(frameworkName)}`);
782
985
  }
783
986
  if (primaryReport.healthScore) {
784
- console.log("\n" + chalk.bold(" Health Scores:"));
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 ? chalk.green : score2 >= 60 ? chalk.yellow : chalk.red;
790
- return color("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
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" + chalk.bold(" Issues by Severity:"));
802
- console.log(` ${chalk.red("\u25CF")} Errors: ${errors.length}`);
803
- console.log(` ${chalk.yellow("\u25CF")} Warnings: ${warnings.length}`);
804
- console.log(` ${chalk.blue("\u25CF")} Notices: ${notices.length}`);
805
- console.log("\n" + chalk.bold(" Top Issues:"));
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" ? chalk.red("\u2717") : issue.severity === "warning" ? chalk.yellow("!") : chalk.blue("i");
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(chalk.dim(` Affects ${issue.affectedUrls.length} pages`));
1016
+ console.log(chalk2.dim(` Affects ${issue.affectedUrls.length} pages`));
812
1017
  }
813
1018
  });
814
1019
  if (aiAnalysis) {
815
- console.log("\n" + chalk.bold(" AI Analysis:"));
1020
+ console.log("\n" + chalk2.bold(" AI Analysis:"));
816
1021
  console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
817
- console.log("\n " + chalk.bold("Suggestions:"));
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 " + chalk.bold("Priority Fixes:"));
823
- aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix, i) => {
824
- console.log(` ${i + 1}. [P${fix.priority}] ${fix.code}`);
825
- console.log(chalk.dim(` ${fix.rationale}`));
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" + chalk.bold("\u2550".repeat(60)));
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(chalk.cyan(`
1054
+ console.log(chalk2.cyan(`
836
1055
  View and manage issues in the web dashboard:`));
837
- console.log(chalk.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
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
- console.log(chalk.dim("\n Tip: Log in to sync results and manage issues in the web dashboard:"));
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(fix, index) {
870
- const icon = SEVERITY_ICON[fix.issue.severity] || "\u2022";
1086
+ function printFix(fix2, index) {
1087
+ const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
871
1088
  console.log(`
872
- ${index + 1}. ${icon} ${fix.issue.message}`);
873
- console.log(` File: ${fix.file}`);
874
- if (fix.skipped) {
875
- console.log(` Status: SKIPPED - ${fix.skipReason}`);
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: ${fix.explanation}`);
878
- if (fix.before) {
879
- console.log(` Before: ${truncate(fix.before, 60)}`);
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(fix.after, 100)}`);
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
- if (isPaidUser) {
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
- if (isPaidUser) {
934
- auditResult = await runAuditWithFixes({
935
- url,
936
- generateFixes: true
937
- });
938
- issues = auditResult.report.issues.map((issue) => ({
939
- severity: issue.severity,
940
- category: issue.category,
941
- code: issue.code,
942
- message: issue.title,
943
- impact: issue.description || void 0
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 fix of applied) {
1011
- console.log(` - ${fix.file}: ${fix.issue.message}`);
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 fix of skipped) {
1018
- console.log(` - ${fix.file}: ${fix.skipReason || "Manual fix required"}`);
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 ora2 from "ora";
1041
- import chalk2 from "chalk";
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 = ora2("Fetching page and robots.txt...").start();
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" + chalk2.bold("\u2550".repeat(60)));
1114
- console.log(chalk2.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
1115
- console.log(chalk2.bold("\u2550".repeat(60)));
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 ? chalk2.green : geoScore >= 60 ? chalk2.yellow : chalk2.red;
2114
+ const scoreColor = geoScore >= 80 ? chalk5.green : geoScore >= 60 ? chalk5.yellow : chalk5.red;
1118
2115
  console.log(`
1119
- ${chalk2.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
1120
- console.log("\n" + chalk2.bold(" AI Crawler Access:"));
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(chalk2.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
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(chalk2.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
2124
+ console.log(chalk5.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
1128
2125
  }
1129
- console.log("\n" + chalk2.bold(" Rendering:"));
2126
+ console.log("\n" + chalk5.bold(" Rendering:"));
1130
2127
  if (aiCrawlerAccess.serverSideRendered) {
1131
- console.log(chalk2.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
2128
+ console.log(chalk5.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
1132
2129
  } else if (aiCrawlerAccess.jsRenderingRequired) {
1133
- console.log(chalk2.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
2130
+ console.log(chalk5.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
1134
2131
  }
1135
- console.log("\n" + chalk2.bold(" LLM Friendliness:"));
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 ? chalk2.green : score >= 40 ? chalk2.yellow : chalk2.red;
1141
- return color("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(empty));
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" + chalk2.bold(" Content Structure:"));
2144
+ console.log("\n" + chalk5.bold(" Content Structure:"));
1148
2145
  const { contentStructure } = geoResult;
1149
- console.log(` ${contentStructure.hasStructuredData ? chalk2.green("\u2705") : chalk2.red("\u274C")} JSON-LD Structured Data`);
1150
- console.log(` ${contentStructure.hasFAQSchema ? chalk2.green("\u2705") : chalk2.yellow("\u25CB")} FAQ Schema`);
1151
- console.log(` ${contentStructure.hasArticleSchema ? chalk2.green("\u2705") : chalk2.yellow("\u25CB")} Article Schema`);
1152
- console.log(` ${contentStructure.hasBreadcrumbs ? chalk2.green("\u2705") : chalk2.yellow("\u25CB")} Breadcrumbs`);
1153
- console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk2.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk2.yellow("Needs Work") : chalk2.red("Poor")}`);
1154
- console.log("\n" + chalk2.bold(" Citation Readiness:"));
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(chalk2.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2154
+ console.log(chalk5.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
1158
2155
  } else {
1159
- console.log(chalk2.yellow(" \u25CB No trust signals detected"));
2156
+ console.log(chalk5.yellow(" \u25CB No trust signals detected"));
1160
2157
  }
1161
- console.log("\n" + chalk2.bold(" Other Scores:"));
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" + chalk2.bold.red(" Critical Issues:"));
2172
+ console.log("\n" + chalk5.bold.red(" Critical Issues:"));
1176
2173
  criticalIssues.slice(0, 5).forEach((issue, i) => {
1177
- console.log(chalk2.red(` ${i + 1}. ${issue.title}`));
2174
+ console.log(chalk5.red(` ${i + 1}. ${issue.title}`));
1178
2175
  });
1179
2176
  }
1180
2177
  if (geoResult.recommendations.length > 0) {
1181
- console.log("\n" + chalk2.bold(" Recommendations:"));
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" + chalk2.bold("\u2550".repeat(60)));
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 ask = (question) => {
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 ask("\nEnter your choice (1-" + choices.length + "): ");
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 ask("Enter your website URL: ");
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 ask(" > ");
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 ask = (question) => {
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 ask("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
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 ask("Enter GSC verification code or press Enter to skip: ");
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 ask("Set up GitHub Action? (y/n): ");
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 ask("Enter choice (1-3): ");
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 ask("Enter your site URL (https://example.com): ");
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 chalk3 from "chalk";
1866
- import ora3 from "ora";
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(chalk3.yellow(`Already logged in as ${email}`));
1873
- console.log(chalk3.dim('Run "rankcli logout" to sign out first.'));
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 = ora3();
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(chalk3.red('Invalid API key format. Keys should start with "rankcli_"'));
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(chalk3.red(`
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(chalk3.dim(`
3063
+ console.log(chalk6.dim(`
1902
3064
  Email: ${result.user?.email}`));
1903
- console.log(chalk3.dim(`Plan: ${result.subscription?.planName || "Free"}`));
3065
+ console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
1904
3066
  return;
1905
3067
  }
1906
3068
  if (options.browser) {
1907
- console.log(chalk3.cyan("\nOpening browser for login..."));
1908
- console.log(chalk3.dim("Complete the login in your browser, then return here.\n"));
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(chalk3.red("The token is invalid or expired. Please try again."));
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 ${chalk3.green(data.user.email)}`);
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(chalk3.red(`
3133
+ console.log(chalk6.red(`
1972
3134
  ${error.message}`));
1973
3135
  if (error.message.includes("Invalid login")) {
1974
- console.log(chalk3.dim("\nDon't have an account? Sign up at:"));
1975
- console.log(chalk3.cyan(` ${WEB_APP_URL}/signup`));
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 ${chalk3.green(data.user?.email)}`);
3147
+ spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
1986
3148
  const { planName, sitesLimit } = getUserInfo();
1987
- console.log(chalk3.dim(`
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(chalk3.red(err instanceof Error ? err.message : "Unknown 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(chalk3.yellow("Not logged in"));
3159
+ console.log(chalk6.yellow("Not logged in"));
1998
3160
  return;
1999
3161
  }
2000
3162
  const { email } = getUserInfo();
2001
- const spinner = ora3("Signing out...").start();
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 ${chalk3.dim(email)}`);
3168
+ spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
2007
3169
  } catch (err) {
2008
3170
  spinner.fail("Logout failed");
2009
- console.error(chalk3.red(err instanceof Error ? err.message : "Unknown 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(chalk3.yellow("Not logged in"));
2015
- console.log(chalk3.dim('\nRun "rankcli login" to sign in.'));
2016
- console.log(chalk3.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
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 = ora3("Checking account...").start();
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(chalk3.bold("\nRankCLI Account"));
3201
+ console.log(chalk6.bold("\nRankCLI Account"));
2040
3202
  console.log("\u2500".repeat(40));
2041
- console.log(` Email: ${chalk3.cyan(email || "N/A")}`);
2042
- console.log(` Plan: ${chalk3.green(planName)}`);
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: ${chalk3.dim(authMethod)}`);
3206
+ console.log(` Auth: ${chalk6.dim(authMethod)}`);
2045
3207
  console.log("\u2500".repeat(40));
2046
- console.log(chalk3.dim(`
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(chalk3.red(err instanceof Error ? err.message : "Unknown 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.18";
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("rank").description("Check current keyword rankings").action(() => {
2117
- console.log("Rank tracking coming soon...");
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();