@rankcli/cli 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -570,6 +570,23 @@ var UPSELL_MESSAGES = {
570
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
571
  ""
572
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
+ ],
573
590
  // Rate limit exceeded
574
591
  rate_limit: (ctx) => [
575
592
  "",
@@ -761,7 +778,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
761
778
  onPage: report.healthScore?.onPage || 0,
762
779
  content: report.healthScore?.content || 0,
763
780
  performance: report.healthScore?.performance || 0,
764
- security: report.healthScore?.security || 0
781
+ security: report.healthScore?.security || 0,
782
+ localSeo: report.healthScore?.localSeo || 0,
783
+ aiReadiness: report.healthScore?.aiReadiness || 0
765
784
  };
766
785
  const now = (/* @__PURE__ */ new Date()).toISOString();
767
786
  const { data: audit2, error: auditError } = await supabase.from("audits").insert({
@@ -825,7 +844,7 @@ async function audit(options) {
825
844
  const maxPages = parseInt(options.maxPages || "5", 10);
826
845
  const enableAI = options.ai || false;
827
846
  const aiProvider = options.aiProvider || "openai";
828
- 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) || "";
829
848
  const loggedIn = isLoggedIn();
830
849
  if (!url) {
831
850
  console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
@@ -978,6 +997,8 @@ async function audit(options) {
978
997
  console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
979
998
  console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
980
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}%`);
981
1002
  }
982
1003
  const errors = mergedIssues.filter((i) => i.severity === "error");
983
1004
  const warnings = mergedIssues.filter((i) => i.severity === "warning");
@@ -1004,9 +1025,9 @@ async function audit(options) {
1004
1025
  });
1005
1026
  if (aiAnalysis.prioritizedFixes?.length > 0) {
1006
1027
  console.log("\n " + chalk2.bold("Priority Fixes:"));
1007
- aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix, i) => {
1008
- console.log(` ${i + 1}. [P${fix.priority}] ${fix.code}`);
1009
- console.log(chalk2.dim(` ${fix.rationale}`));
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}`));
1010
1031
  });
1011
1032
  }
1012
1033
  }
@@ -1062,19 +1083,19 @@ var SEVERITY_ICON = {
1062
1083
  warning: "\u26A0\uFE0F",
1063
1084
  info: "\u2139\uFE0F"
1064
1085
  };
1065
- function printFix(fix, index) {
1066
- const icon = SEVERITY_ICON[fix.issue.severity] || "\u2022";
1086
+ function printFix(fix2, index) {
1087
+ const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
1067
1088
  console.log(`
1068
- ${index + 1}. ${icon} ${fix.issue.message}`);
1069
- console.log(` File: ${fix.file}`);
1070
- if (fix.skipped) {
1071
- 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}`);
1072
1093
  } else {
1073
- console.log(` Explanation: ${fix.explanation}`);
1074
- if (fix.before) {
1075
- 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)}`);
1076
1097
  }
1077
- console.log(` After: ${truncate(fix.after, 100)}`);
1098
+ console.log(` After: ${truncate(fix2.after, 100)}`);
1078
1099
  }
1079
1100
  }
1080
1101
  function truncate(str, maxLen) {
@@ -1200,15 +1221,15 @@ async function apply(options) {
1200
1221
  if (applied.length > 0) {
1201
1222
  console.log(`
1202
1223
  \u2705 Applied ${applied.length} fix(es):`);
1203
- for (const fix of applied) {
1204
- console.log(` - ${fix.file}: ${fix.issue.message}`);
1224
+ for (const fix2 of applied) {
1225
+ console.log(` - ${fix2.file}: ${fix2.issue.message}`);
1205
1226
  }
1206
1227
  }
1207
1228
  if (skipped.length > 0) {
1208
1229
  console.log(`
1209
1230
  \u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
1210
- for (const fix of skipped) {
1211
- 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"}`);
1212
1233
  }
1213
1234
  }
1214
1235
  if (config2 && !config2.url) {
@@ -1228,10 +1249,793 @@ async function apply(options) {
1228
1249
  console.log("");
1229
1250
  }
1230
1251
 
1231
- // src/commands/geo.ts
1232
- import { analyzers } from "@rankcli/agent-runtime";
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";
1233
1261
  import ora2 from "ora";
1234
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
+
2035
+ // src/commands/geo.ts
2036
+ import { analyzers } from "@rankcli/agent-runtime";
2037
+ import ora4 from "ora";
2038
+ import chalk5 from "chalk";
1235
2039
  async function fetchResources(url) {
1236
2040
  const headers = {};
1237
2041
  let html = null;
@@ -1277,7 +2081,7 @@ async function geo(options) {
1277
2081
  console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
1278
2082
  return;
1279
2083
  }
1280
- const spinner = ora2("Fetching page and robots.txt...").start();
2084
+ const spinner = ora4("Fetching page and robots.txt...").start();
1281
2085
  const { html, robotsTxt, headers } = await fetchResources(url);
1282
2086
  if (!html) {
1283
2087
  spinner.fail("Could not fetch page content");
@@ -1303,55 +2107,55 @@ async function geo(options) {
1303
2107
  console.log(JSON.stringify(jsonOutput, null, 2));
1304
2108
  return;
1305
2109
  }
1306
- console.log("\n" + chalk3.bold("\u2550".repeat(60)));
1307
- console.log(chalk3.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
1308
- console.log(chalk3.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)));
1309
2113
  const geoScore = geoResult.score;
1310
- const scoreColor = geoScore >= 80 ? chalk3.green : geoScore >= 60 ? chalk3.yellow : chalk3.red;
2114
+ const scoreColor = geoScore >= 80 ? chalk5.green : geoScore >= 60 ? chalk5.yellow : chalk5.red;
1311
2115
  console.log(`
1312
- ${chalk3.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
1313
- console.log("\n" + chalk3.bold(" AI Crawler Access:"));
2116
+ ${chalk5.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
2117
+ console.log("\n" + chalk5.bold(" AI Crawler Access:"));
1314
2118
  const { aiCrawlerAccess } = geoResult;
1315
2119
  if (aiCrawlerAccess.blockedCrawlers.length > 0) {
1316
- console.log(chalk3.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
2120
+ console.log(chalk5.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
1317
2121
  }
1318
2122
  if (aiCrawlerAccess.allowedCrawlers.length > 0) {
1319
2123
  const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
1320
- console.log(chalk3.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
2124
+ console.log(chalk5.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
1321
2125
  }
1322
- console.log("\n" + chalk3.bold(" Rendering:"));
2126
+ console.log("\n" + chalk5.bold(" Rendering:"));
1323
2127
  if (aiCrawlerAccess.serverSideRendered) {
1324
- console.log(chalk3.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
2128
+ console.log(chalk5.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
1325
2129
  } else if (aiCrawlerAccess.jsRenderingRequired) {
1326
- console.log(chalk3.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
2130
+ console.log(chalk5.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
1327
2131
  }
1328
- console.log("\n" + chalk3.bold(" LLM Friendliness:"));
2132
+ console.log("\n" + chalk5.bold(" LLM Friendliness:"));
1329
2133
  const { llmSignals } = geoResult;
1330
2134
  const bar = (score) => {
1331
2135
  const filled = Math.round(score / 5);
1332
2136
  const empty = 20 - filled;
1333
- const color = score >= 70 ? chalk3.green : score >= 40 ? chalk3.yellow : chalk3.red;
1334
- return color("\u2588".repeat(filled)) + chalk3.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));
1335
2139
  };
1336
2140
  console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
1337
2141
  console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
1338
2142
  console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
1339
2143
  console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
1340
- console.log("\n" + chalk3.bold(" Content Structure:"));
2144
+ console.log("\n" + chalk5.bold(" Content Structure:"));
1341
2145
  const { contentStructure } = geoResult;
1342
- console.log(` ${contentStructure.hasStructuredData ? chalk3.green("\u2705") : chalk3.red("\u274C")} JSON-LD Structured Data`);
1343
- console.log(` ${contentStructure.hasFAQSchema ? chalk3.green("\u2705") : chalk3.yellow("\u25CB")} FAQ Schema`);
1344
- console.log(` ${contentStructure.hasArticleSchema ? chalk3.green("\u2705") : chalk3.yellow("\u25CB")} Article Schema`);
1345
- console.log(` ${contentStructure.hasBreadcrumbs ? chalk3.green("\u2705") : chalk3.yellow("\u25CB")} Breadcrumbs`);
1346
- console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk3.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk3.yellow("Needs Work") : chalk3.red("Poor")}`);
1347
- console.log("\n" + chalk3.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:"));
1348
2152
  const { citationReadiness } = geoResult;
1349
2153
  if (citationReadiness.trustSignals.length > 0) {
1350
- console.log(chalk3.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
2154
+ console.log(chalk5.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
1351
2155
  } else {
1352
- console.log(chalk3.yellow(" \u25CB No trust signals detected"));
2156
+ console.log(chalk5.yellow(" \u25CB No trust signals detected"));
1353
2157
  }
1354
- console.log("\n" + chalk3.bold(" Other Scores:"));
2158
+ console.log("\n" + chalk5.bold(" Other Scores:"));
1355
2159
  console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
1356
2160
  console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
1357
2161
  console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
@@ -1365,18 +2169,18 @@ async function geo(options) {
1365
2169
  ];
1366
2170
  const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
1367
2171
  if (criticalIssues.length > 0) {
1368
- console.log("\n" + chalk3.bold.red(" Critical Issues:"));
2172
+ console.log("\n" + chalk5.bold.red(" Critical Issues:"));
1369
2173
  criticalIssues.slice(0, 5).forEach((issue, i) => {
1370
- console.log(chalk3.red(` ${i + 1}. ${issue.title}`));
2174
+ console.log(chalk5.red(` ${i + 1}. ${issue.title}`));
1371
2175
  });
1372
2176
  }
1373
2177
  if (geoResult.recommendations.length > 0) {
1374
- console.log("\n" + chalk3.bold(" Recommendations:"));
2178
+ console.log("\n" + chalk5.bold(" Recommendations:"));
1375
2179
  geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
1376
2180
  console.log(` ${i + 1}. ${rec}`);
1377
2181
  });
1378
2182
  }
1379
- console.log("\n" + chalk3.bold("\u2550".repeat(60)));
2183
+ console.log("\n" + chalk5.bold("\u2550".repeat(60)));
1380
2184
  console.log("");
1381
2185
  } catch (error) {
1382
2186
  spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -1404,7 +2208,7 @@ async function keywords(options) {
1404
2208
  input: process.stdin,
1405
2209
  output: process.stdout
1406
2210
  });
1407
- const ask = (question) => {
2211
+ const ask2 = (question) => {
1408
2212
  return new Promise((resolve) => {
1409
2213
  rl.question(question, (answer) => {
1410
2214
  resolve(answer.trim());
@@ -1419,7 +2223,7 @@ ${question}`);
1419
2223
  console.log(` ${choice.description}`);
1420
2224
  });
1421
2225
  while (true) {
1422
- const answer = await ask("\nEnter your choice (1-" + choices.length + "): ");
2226
+ const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
1423
2227
  const num = parseInt(answer, 10);
1424
2228
  if (num >= 1 && num <= choices.length) {
1425
2229
  return choices[num - 1].value;
@@ -1430,7 +2234,7 @@ ${question}`);
1430
2234
  try {
1431
2235
  let url = options.url;
1432
2236
  if (!url) {
1433
- url = await ask("Enter your website URL: ");
2237
+ url = await ask2("Enter your website URL: ");
1434
2238
  }
1435
2239
  try {
1436
2240
  const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
@@ -1454,7 +2258,7 @@ ${question}`);
1454
2258
  if (seedKeywords.length === 0) {
1455
2259
  console.log("\n\u{1F4DD} What is your website/product about?");
1456
2260
  console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
1457
- const seedInput = await ask(" > ");
2261
+ const seedInput = await ask2(" > ");
1458
2262
  seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1459
2263
  if (seedKeywords.length === 0) {
1460
2264
  console.log("\n\u274C Please provide at least one seed keyword.\n");
@@ -1914,7 +2718,7 @@ async function interactiveSetup(projectPath) {
1914
2718
  input: process.stdin,
1915
2719
  output: process.stdout
1916
2720
  });
1917
- const ask = (question) => {
2721
+ const ask2 = (question) => {
1918
2722
  return new Promise((resolve) => {
1919
2723
  rl.question(question, (answer) => {
1920
2724
  resolve(answer.trim());
@@ -1928,7 +2732,7 @@ async function interactiveSetup(projectPath) {
1928
2732
  console.log("GA4 tracks visitor behavior on your site.");
1929
2733
  console.log("Get your Measurement ID from: https://analytics.google.com");
1930
2734
  console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
1931
- 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: ");
1932
2736
  if (ga4Id && ga4Id.startsWith("G-")) {
1933
2737
  await setupGA4(projectPath, ga4Id);
1934
2738
  } else if (ga4Id) {
@@ -1941,7 +2745,7 @@ async function interactiveSetup(projectPath) {
1941
2745
  console.log("GSC shows how Google sees your site and what queries you rank for.");
1942
2746
  console.log("Get your verification code from: https://search.google.com/search-console");
1943
2747
  console.log("Go to: Settings > Ownership verification > HTML tag\n");
1944
- 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: ");
1945
2749
  if (gscCode && gscCode.length > 10) {
1946
2750
  await setupGSC(projectPath, gscCode);
1947
2751
  } else if (gscCode) {
@@ -1953,20 +2757,20 @@ async function interactiveSetup(projectPath) {
1953
2757
  console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
1954
2758
  console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
1955
2759
  console.log("It creates issues with reports and can auto-fix some problems.\n");
1956
- const setupAction = await ask("Set up GitHub Action? (y/n): ");
2760
+ const setupAction = await ask2("Set up GitHub Action? (y/n): ");
1957
2761
  if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
1958
2762
  console.log("\nHow often should the check run?");
1959
2763
  console.log(" 1. Weekly (recommended for most sites)");
1960
2764
  console.log(" 2. Daily (for high-traffic sites)");
1961
2765
  console.log(" 3. Monthly (for low-traffic sites)\n");
1962
- const scheduleChoice = await ask("Enter choice (1-3): ");
2766
+ const scheduleChoice = await ask2("Enter choice (1-3): ");
1963
2767
  const scheduleMap = {
1964
2768
  "1": "weekly",
1965
2769
  "2": "daily",
1966
2770
  "3": "monthly"
1967
2771
  };
1968
2772
  const schedule = scheduleMap[scheduleChoice] || "weekly";
1969
- const siteUrl = await ask("Enter your site URL (https://example.com): ");
2773
+ const siteUrl = await ask2("Enter your site URL (https://example.com): ");
1970
2774
  if (siteUrl) {
1971
2775
  await setupGitHubAction(projectPath, schedule, siteUrl);
1972
2776
  }
@@ -2054,31 +2858,196 @@ function showGSCInstructions() {
2054
2858
  console.log(getGSCSetupInstructions());
2055
2859
  }
2056
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
+
2057
3026
  // src/commands/auth.ts
2058
- import chalk4 from "chalk";
2059
- import ora3 from "ora";
3027
+ import chalk6 from "chalk";
3028
+ import ora5 from "ora";
2060
3029
  import inquirer from "inquirer";
2061
3030
  import open from "open";
2062
3031
  async function login(options) {
2063
3032
  if (isLoggedIn()) {
2064
3033
  const { email } = getUserInfo();
2065
- console.log(chalk4.yellow(`Already logged in as ${email}`));
2066
- console.log(chalk4.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.'));
2067
3036
  return;
2068
3037
  }
2069
- const spinner = ora3();
3038
+ const spinner = ora5();
2070
3039
  try {
2071
3040
  if (options.token) {
2072
3041
  const apiKey = options.token;
2073
3042
  if (!apiKey.startsWith("rankcli_")) {
2074
- console.log(chalk4.red('Invalid API key format. Keys should start with "rankcli_"'));
3043
+ console.log(chalk6.red('Invalid API key format. Keys should start with "rankcli_"'));
2075
3044
  return;
2076
3045
  }
2077
3046
  spinner.start("Validating API key...");
2078
3047
  const result = await validateApiKey(apiKey);
2079
3048
  if (!result.valid) {
2080
3049
  spinner.fail("Invalid API key");
2081
- console.log(chalk4.red(`
3050
+ console.log(chalk6.red(`
2082
3051
  ${result.error}`));
2083
3052
  return;
2084
3053
  }
@@ -2091,14 +3060,14 @@ ${result.error}`));
2091
3060
  });
2092
3061
  }
2093
3062
  spinner.succeed(`Authenticated via API key`);
2094
- console.log(chalk4.dim(`
3063
+ console.log(chalk6.dim(`
2095
3064
  Email: ${result.user?.email}`));
2096
- console.log(chalk4.dim(`Plan: ${result.subscription?.planName || "Free"}`));
3065
+ console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
2097
3066
  return;
2098
3067
  }
2099
3068
  if (options.browser) {
2100
- console.log(chalk4.cyan("\nOpening browser for login..."));
2101
- console.log(chalk4.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"));
2102
3071
  const loginUrl = `${WEB_APP_URL}/login?cli=true`;
2103
3072
  await open(loginUrl);
2104
3073
  const { token } = await inquirer.prompt([
@@ -2114,7 +3083,7 @@ Email: ${result.user?.email}`));
2114
3083
  const { data, error } = await supabase.auth.getUser(token);
2115
3084
  if (error || !data.user) {
2116
3085
  spinner.fail("Invalid token");
2117
- console.log(chalk4.red("The token is invalid or expired. Please try again."));
3086
+ console.log(chalk6.red("The token is invalid or expired. Please try again."));
2118
3087
  return;
2119
3088
  }
2120
3089
  const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
@@ -2128,7 +3097,7 @@ Email: ${result.user?.email}`));
2128
3097
  }
2129
3098
  saveSession(sessionData.session);
2130
3099
  await fetchAndSaveSubscription();
2131
- spinner.succeed(`Logged in as ${chalk4.green(data.user.email)}`);
3100
+ spinner.succeed(`Logged in as ${chalk6.green(data.user.email)}`);
2132
3101
  } else {
2133
3102
  let email = options.email;
2134
3103
  if (!email) {
@@ -2161,11 +3130,11 @@ Email: ${result.user?.email}`));
2161
3130
  });
2162
3131
  if (error) {
2163
3132
  spinner.fail("Login failed");
2164
- console.log(chalk4.red(`
3133
+ console.log(chalk6.red(`
2165
3134
  ${error.message}`));
2166
3135
  if (error.message.includes("Invalid login")) {
2167
- console.log(chalk4.dim("\nDon't have an account? Sign up at:"));
2168
- console.log(chalk4.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`));
2169
3138
  }
2170
3139
  return;
2171
3140
  }
@@ -2175,41 +3144,41 @@ ${error.message}`));
2175
3144
  }
2176
3145
  saveSession(data.session);
2177
3146
  await fetchAndSaveSubscription();
2178
- spinner.succeed(`Logged in as ${chalk4.green(data.user?.email)}`);
3147
+ spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
2179
3148
  const { planName, sitesLimit } = getUserInfo();
2180
- console.log(chalk4.dim(`
3149
+ console.log(chalk6.dim(`
2181
3150
  Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
2182
3151
  }
2183
3152
  } catch (err) {
2184
3153
  spinner.fail("Login failed");
2185
- console.error(chalk4.red(err instanceof Error ? err.message : "Unknown error"));
3154
+ console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
2186
3155
  }
2187
3156
  }
2188
3157
  async function logout() {
2189
3158
  if (!isLoggedIn()) {
2190
- console.log(chalk4.yellow("Not logged in"));
3159
+ console.log(chalk6.yellow("Not logged in"));
2191
3160
  return;
2192
3161
  }
2193
3162
  const { email } = getUserInfo();
2194
- const spinner = ora3("Signing out...").start();
3163
+ const spinner = ora5("Signing out...").start();
2195
3164
  try {
2196
3165
  const supabase = getSupabaseClient();
2197
3166
  await supabase.auth.signOut();
2198
3167
  clearCredentials();
2199
- spinner.succeed(`Logged out from ${chalk4.dim(email)}`);
3168
+ spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
2200
3169
  } catch (err) {
2201
3170
  spinner.fail("Logout failed");
2202
- console.error(chalk4.red(err instanceof Error ? err.message : "Unknown error"));
3171
+ console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
2203
3172
  }
2204
3173
  }
2205
3174
  async function whoami() {
2206
3175
  if (!isLoggedIn()) {
2207
- console.log(chalk4.yellow("Not logged in"));
2208
- console.log(chalk4.dim('\nRun "rankcli login" to sign in.'));
2209
- console.log(chalk4.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`));
2210
3179
  return;
2211
3180
  }
2212
- const spinner = ora3("Checking account...").start();
3181
+ const spinner = ora5("Checking account...").start();
2213
3182
  try {
2214
3183
  if (isApiKeyAuth()) {
2215
3184
  const apiKey = getApiKey();
@@ -2229,18 +3198,18 @@ async function whoami() {
2229
3198
  const { email, planName, sitesLimit } = getUserInfo();
2230
3199
  const authMethod = isApiKeyAuth() ? "API Key" : "Session";
2231
3200
  spinner.stop();
2232
- console.log(chalk4.bold("\nRankCLI Account"));
3201
+ console.log(chalk6.bold("\nRankCLI Account"));
2233
3202
  console.log("\u2500".repeat(40));
2234
- console.log(` Email: ${chalk4.cyan(email || "N/A")}`);
2235
- console.log(` Plan: ${chalk4.green(planName)}`);
3203
+ console.log(` Email: ${chalk6.cyan(email || "N/A")}`);
3204
+ console.log(` Plan: ${chalk6.green(planName)}`);
2236
3205
  console.log(` Sites: ${sitesLimit} allowed`);
2237
- console.log(` Auth: ${chalk4.dim(authMethod)}`);
3206
+ console.log(` Auth: ${chalk6.dim(authMethod)}`);
2238
3207
  console.log("\u2500".repeat(40));
2239
- console.log(chalk4.dim(`
3208
+ console.log(chalk6.dim(`
2240
3209
  Manage at: ${WEB_APP_URL}/account`));
2241
3210
  } catch (err) {
2242
3211
  spinner.fail("Failed to fetch account info");
2243
- console.error(chalk4.red(err instanceof Error ? err.message : "Unknown error"));
3212
+ console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
2244
3213
  }
2245
3214
  }
2246
3215
  async function fetchAndSaveSubscription() {
@@ -2272,7 +3241,7 @@ async function fetchAndSaveSubscription() {
2272
3241
  }
2273
3242
 
2274
3243
  // src/version.ts
2275
- var VERSION = "0.0.19";
3244
+ var VERSION = "0.0.21";
2276
3245
 
2277
3246
  // src/index.ts
2278
3247
  var program = new Command();
@@ -2284,6 +3253,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
2284
3253
  program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
2285
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);
2286
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
+ });
2287
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);
2288
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) => {
2289
3272
  if (options.ai && options.url) {
@@ -2306,8 +3289,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
2306
3289
  program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
2307
3290
  console.log("Competitor analysis coming soon...");
2308
3291
  });
2309
- program.command("rank").description("Check current keyword rankings").action(() => {
2310
- 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
+ });
2311
3328
  });
2312
3329
  program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
2313
3330
  console.log("Report generation coming soon...");
@@ -2317,4 +3334,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
2317
3334
  });
2318
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);
2319
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);
2320
3338
  program.parse();