@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/README.md +2 -2
- package/dist/index.js +1151 -138
- package/dist/index.mjs +1114 -96
- package/package.json +3 -3
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((
|
|
1008
|
-
console.log(` ${i + 1}. [P${
|
|
1009
|
-
console.log(chalk2.dim(` ${
|
|
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(
|
|
1066
|
-
const icon = SEVERITY_ICON[
|
|
1086
|
+
function printFix(fix2, index) {
|
|
1087
|
+
const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
|
|
1067
1088
|
console.log(`
|
|
1068
|
-
${index + 1}. ${icon} ${
|
|
1069
|
-
console.log(` File: ${
|
|
1070
|
-
if (
|
|
1071
|
-
console.log(` Status: SKIPPED - ${
|
|
1089
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1090
|
+
console.log(` File: ${fix2.file}`);
|
|
1091
|
+
if (fix2.skipped) {
|
|
1092
|
+
console.log(` Status: SKIPPED - ${fix2.skipReason}`);
|
|
1072
1093
|
} else {
|
|
1073
|
-
console.log(` Explanation: ${
|
|
1074
|
-
if (
|
|
1075
|
-
console.log(` Before: ${truncate(
|
|
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(
|
|
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
|
|
1204
|
-
console.log(` - ${
|
|
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
|
|
1211
|
-
console.log(` - ${
|
|
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/
|
|
1232
|
-
import {
|
|
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 =
|
|
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" +
|
|
1307
|
-
console.log(
|
|
1308
|
-
console.log(
|
|
2110
|
+
console.log("\n" + chalk5.bold("\u2550".repeat(60)));
|
|
2111
|
+
console.log(chalk5.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2112
|
+
console.log(chalk5.bold("\u2550".repeat(60)));
|
|
1309
2113
|
const geoScore = geoResult.score;
|
|
1310
|
-
const scoreColor = geoScore >= 80 ?
|
|
2114
|
+
const scoreColor = geoScore >= 80 ? chalk5.green : geoScore >= 60 ? chalk5.yellow : chalk5.red;
|
|
1311
2115
|
console.log(`
|
|
1312
|
-
${
|
|
1313
|
-
console.log("\n" +
|
|
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(
|
|
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(
|
|
2124
|
+
console.log(chalk5.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
1321
2125
|
}
|
|
1322
|
-
console.log("\n" +
|
|
2126
|
+
console.log("\n" + chalk5.bold(" Rendering:"));
|
|
1323
2127
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
1324
|
-
console.log(
|
|
2128
|
+
console.log(chalk5.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
1325
2129
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
1326
|
-
console.log(
|
|
2130
|
+
console.log(chalk5.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
1327
2131
|
}
|
|
1328
|
-
console.log("\n" +
|
|
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 ?
|
|
1334
|
-
return color("\u2588".repeat(filled)) +
|
|
2137
|
+
const color = score >= 70 ? chalk5.green : score >= 40 ? chalk5.yellow : chalk5.red;
|
|
2138
|
+
return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
|
|
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" +
|
|
2144
|
+
console.log("\n" + chalk5.bold(" Content Structure:"));
|
|
1341
2145
|
const { contentStructure } = geoResult;
|
|
1342
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
1343
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
1344
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
1345
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
1346
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
1347
|
-
console.log("\n" +
|
|
2146
|
+
console.log(` ${contentStructure.hasStructuredData ? chalk5.green("\u2705") : chalk5.red("\u274C")} JSON-LD Structured Data`);
|
|
2147
|
+
console.log(` ${contentStructure.hasFAQSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} FAQ Schema`);
|
|
2148
|
+
console.log(` ${contentStructure.hasArticleSchema ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Article Schema`);
|
|
2149
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? chalk5.green("\u2705") : chalk5.yellow("\u25CB")} Breadcrumbs`);
|
|
2150
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk5.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk5.yellow("Needs Work") : chalk5.red("Poor")}`);
|
|
2151
|
+
console.log("\n" + chalk5.bold(" Citation Readiness:"));
|
|
1348
2152
|
const { citationReadiness } = geoResult;
|
|
1349
2153
|
if (citationReadiness.trustSignals.length > 0) {
|
|
1350
|
-
console.log(
|
|
2154
|
+
console.log(chalk5.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
1351
2155
|
} else {
|
|
1352
|
-
console.log(
|
|
2156
|
+
console.log(chalk5.yellow(" \u25CB No trust signals detected"));
|
|
1353
2157
|
}
|
|
1354
|
-
console.log("\n" +
|
|
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" +
|
|
2172
|
+
console.log("\n" + chalk5.bold.red(" Critical Issues:"));
|
|
1369
2173
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
1370
|
-
console.log(
|
|
2174
|
+
console.log(chalk5.red(` ${i + 1}. ${issue.title}`));
|
|
1371
2175
|
});
|
|
1372
2176
|
}
|
|
1373
2177
|
if (geoResult.recommendations.length > 0) {
|
|
1374
|
-
console.log("\n" +
|
|
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" +
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2059
|
-
import
|
|
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(
|
|
2066
|
-
console.log(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
3063
|
+
console.log(chalk6.dim(`
|
|
2095
3064
|
Email: ${result.user?.email}`));
|
|
2096
|
-
console.log(
|
|
3065
|
+
console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
2097
3066
|
return;
|
|
2098
3067
|
}
|
|
2099
3068
|
if (options.browser) {
|
|
2100
|
-
console.log(
|
|
2101
|
-
console.log(
|
|
3069
|
+
console.log(chalk6.cyan("\nOpening browser for login..."));
|
|
3070
|
+
console.log(chalk6.dim("Complete the login in your browser, then return here.\n"));
|
|
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(
|
|
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 ${
|
|
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(
|
|
3133
|
+
console.log(chalk6.red(`
|
|
2165
3134
|
${error.message}`));
|
|
2166
3135
|
if (error.message.includes("Invalid login")) {
|
|
2167
|
-
console.log(
|
|
2168
|
-
console.log(
|
|
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 ${
|
|
3147
|
+
spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
|
|
2179
3148
|
const { planName, sitesLimit } = getUserInfo();
|
|
2180
|
-
console.log(
|
|
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(
|
|
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(
|
|
3159
|
+
console.log(chalk6.yellow("Not logged in"));
|
|
2191
3160
|
return;
|
|
2192
3161
|
}
|
|
2193
3162
|
const { email } = getUserInfo();
|
|
2194
|
-
const spinner =
|
|
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 ${
|
|
3168
|
+
spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
|
|
2200
3169
|
} catch (err) {
|
|
2201
3170
|
spinner.fail("Logout failed");
|
|
2202
|
-
console.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(
|
|
2208
|
-
console.log(
|
|
2209
|
-
console.log(
|
|
3176
|
+
console.log(chalk6.yellow("Not logged in"));
|
|
3177
|
+
console.log(chalk6.dim('\nRun "rankcli login" to sign in.'));
|
|
3178
|
+
console.log(chalk6.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
2210
3179
|
return;
|
|
2211
3180
|
}
|
|
2212
|
-
const spinner =
|
|
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(
|
|
3201
|
+
console.log(chalk6.bold("\nRankCLI Account"));
|
|
2233
3202
|
console.log("\u2500".repeat(40));
|
|
2234
|
-
console.log(` Email: ${
|
|
2235
|
-
console.log(` Plan: ${
|
|
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: ${
|
|
3206
|
+
console.log(` Auth: ${chalk6.dim(authMethod)}`);
|
|
2238
3207
|
console.log("\u2500".repeat(40));
|
|
2239
|
-
console.log(
|
|
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(
|
|
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.
|
|
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("
|
|
2310
|
-
|
|
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();
|