@rankcli/cli 0.0.19 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +1151 -138
- package/dist/index.mjs +1114 -96
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -592,6 +592,23 @@ var UPSELL_MESSAGES = {
|
|
|
592
592
|
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
593
593
|
""
|
|
594
594
|
],
|
|
595
|
+
// Keyword tracking limit reached
|
|
596
|
+
keyword_limit: (ctx) => [
|
|
597
|
+
"",
|
|
598
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
599
|
+
import_chalk.default.red.bold(" \u2717 Keyword tracking limit reached"),
|
|
600
|
+
import_chalk.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
|
|
601
|
+
import_chalk.default.dim(` Currently tracking: ${ctx.current}`),
|
|
602
|
+
"",
|
|
603
|
+
import_chalk.default.dim(" Upgrade for more keywords:"),
|
|
604
|
+
import_chalk.default.dim(" \u2022 Solo ($9/mo): 100 keywords"),
|
|
605
|
+
import_chalk.default.dim(" \u2022 Pro ($29/mo): 500 keywords"),
|
|
606
|
+
import_chalk.default.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
|
|
607
|
+
"",
|
|
608
|
+
import_chalk.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
|
|
609
|
+
import_chalk.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
610
|
+
""
|
|
611
|
+
],
|
|
595
612
|
// Rate limit exceeded
|
|
596
613
|
rate_limit: (ctx) => [
|
|
597
614
|
"",
|
|
@@ -783,7 +800,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
783
800
|
onPage: report.healthScore?.onPage || 0,
|
|
784
801
|
content: report.healthScore?.content || 0,
|
|
785
802
|
performance: report.healthScore?.performance || 0,
|
|
786
|
-
security: report.healthScore?.security || 0
|
|
803
|
+
security: report.healthScore?.security || 0,
|
|
804
|
+
localSeo: report.healthScore?.localSeo || 0,
|
|
805
|
+
aiReadiness: report.healthScore?.aiReadiness || 0
|
|
787
806
|
};
|
|
788
807
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
789
808
|
const { data: audit2, error: auditError } = await supabase.from("audits").insert({
|
|
@@ -847,7 +866,7 @@ async function audit(options) {
|
|
|
847
866
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
848
867
|
const enableAI = options.ai || false;
|
|
849
868
|
const aiProvider = options.aiProvider || "openai";
|
|
850
|
-
const aiKey = options.aiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || "";
|
|
869
|
+
const aiKey = options.aiKey || (aiProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY : process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY) || "";
|
|
851
870
|
const loggedIn = isLoggedIn();
|
|
852
871
|
if (!url) {
|
|
853
872
|
console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
|
|
@@ -1000,6 +1019,8 @@ async function audit(options) {
|
|
|
1000
1019
|
console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
|
|
1001
1020
|
console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
|
|
1002
1021
|
console.log(` Security: ${bar(hs.security || 0)} ${hs.security || 0}%`);
|
|
1022
|
+
console.log(` Local SEO: ${bar(hs.localSeo || 0)} ${hs.localSeo || 0}%`);
|
|
1023
|
+
console.log(` AI Readiness: ${bar(hs.aiReadiness || 0)} ${hs.aiReadiness || 0}%`);
|
|
1003
1024
|
}
|
|
1004
1025
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
1005
1026
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
@@ -1026,9 +1047,9 @@ async function audit(options) {
|
|
|
1026
1047
|
});
|
|
1027
1048
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
1028
1049
|
console.log("\n " + import_chalk2.default.bold("Priority Fixes:"));
|
|
1029
|
-
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((
|
|
1030
|
-
console.log(` ${i + 1}. [P${
|
|
1031
|
-
console.log(import_chalk2.default.dim(` ${
|
|
1050
|
+
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1051
|
+
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1052
|
+
console.log(import_chalk2.default.dim(` ${fix2.rationale}`));
|
|
1032
1053
|
});
|
|
1033
1054
|
}
|
|
1034
1055
|
}
|
|
@@ -1079,19 +1100,19 @@ var SEVERITY_ICON = {
|
|
|
1079
1100
|
warning: "\u26A0\uFE0F",
|
|
1080
1101
|
info: "\u2139\uFE0F"
|
|
1081
1102
|
};
|
|
1082
|
-
function printFix(
|
|
1083
|
-
const icon = SEVERITY_ICON[
|
|
1103
|
+
function printFix(fix2, index) {
|
|
1104
|
+
const icon = SEVERITY_ICON[fix2.issue.severity] || "\u2022";
|
|
1084
1105
|
console.log(`
|
|
1085
|
-
${index + 1}. ${icon} ${
|
|
1086
|
-
console.log(` File: ${
|
|
1087
|
-
if (
|
|
1088
|
-
console.log(` Status: SKIPPED - ${
|
|
1106
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1107
|
+
console.log(` File: ${fix2.file}`);
|
|
1108
|
+
if (fix2.skipped) {
|
|
1109
|
+
console.log(` Status: SKIPPED - ${fix2.skipReason}`);
|
|
1089
1110
|
} else {
|
|
1090
|
-
console.log(` Explanation: ${
|
|
1091
|
-
if (
|
|
1092
|
-
console.log(` Before: ${truncate(
|
|
1111
|
+
console.log(` Explanation: ${fix2.explanation}`);
|
|
1112
|
+
if (fix2.before) {
|
|
1113
|
+
console.log(` Before: ${truncate(fix2.before, 60)}`);
|
|
1093
1114
|
}
|
|
1094
|
-
console.log(` After: ${truncate(
|
|
1115
|
+
console.log(` After: ${truncate(fix2.after, 100)}`);
|
|
1095
1116
|
}
|
|
1096
1117
|
}
|
|
1097
1118
|
function truncate(str, maxLen) {
|
|
@@ -1217,15 +1238,15 @@ async function apply(options) {
|
|
|
1217
1238
|
if (applied.length > 0) {
|
|
1218
1239
|
console.log(`
|
|
1219
1240
|
\u2705 Applied ${applied.length} fix(es):`);
|
|
1220
|
-
for (const
|
|
1221
|
-
console.log(` - ${
|
|
1241
|
+
for (const fix2 of applied) {
|
|
1242
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1222
1243
|
}
|
|
1223
1244
|
}
|
|
1224
1245
|
if (skipped.length > 0) {
|
|
1225
1246
|
console.log(`
|
|
1226
1247
|
\u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
|
|
1227
|
-
for (const
|
|
1228
|
-
console.log(` - ${
|
|
1248
|
+
for (const fix2 of skipped) {
|
|
1249
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1229
1250
|
}
|
|
1230
1251
|
}
|
|
1231
1252
|
if (config2 && !config2.url) {
|
|
@@ -1245,10 +1266,788 @@ async function apply(options) {
|
|
|
1245
1266
|
console.log("");
|
|
1246
1267
|
}
|
|
1247
1268
|
|
|
1248
|
-
// src/commands/
|
|
1269
|
+
// src/commands/fix.ts
|
|
1270
|
+
var import_child_process = require("child_process");
|
|
1271
|
+
var import_util = require("util");
|
|
1249
1272
|
var import_agent_runtime3 = require("@rankcli/agent-runtime");
|
|
1250
1273
|
var import_ora2 = __toESM(require("ora"));
|
|
1251
1274
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1275
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
1276
|
+
var SEVERITY_ICON2 = {
|
|
1277
|
+
critical: "\u274C",
|
|
1278
|
+
error: "\u274C",
|
|
1279
|
+
warning: "\u26A0\uFE0F",
|
|
1280
|
+
notice: "\u2139\uFE0F",
|
|
1281
|
+
info: "\u2139\uFE0F"
|
|
1282
|
+
};
|
|
1283
|
+
function generateCommitMessage(fixes, url) {
|
|
1284
|
+
const applied = fixes.filter((f) => f.applied && !f.skipped);
|
|
1285
|
+
if (applied.length === 0) {
|
|
1286
|
+
return "";
|
|
1287
|
+
}
|
|
1288
|
+
const isAddingNew = applied.some(
|
|
1289
|
+
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1290
|
+
);
|
|
1291
|
+
const type = isAddingNew ? "feat" : "fix";
|
|
1292
|
+
const categories = new Set(applied.map((f) => f.issue.category || "seo"));
|
|
1293
|
+
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1294
|
+
const issueCount = applied.length;
|
|
1295
|
+
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1296
|
+
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
1297
|
+
const body = `Fixed:
|
|
1298
|
+
${fixList}
|
|
1299
|
+
|
|
1300
|
+
URL: ${url}
|
|
1301
|
+
Generated by: rankcli fix`;
|
|
1302
|
+
return `${title}
|
|
1303
|
+
|
|
1304
|
+
${body}`;
|
|
1305
|
+
}
|
|
1306
|
+
async function executeGitCommit(cwd, commitMessage, push) {
|
|
1307
|
+
try {
|
|
1308
|
+
await execAsync("git add -A", { cwd });
|
|
1309
|
+
const escapedMessage = commitMessage.replace(/"/g, '\\"');
|
|
1310
|
+
await execAsync(`git commit -m "${escapedMessage}"`, { cwd });
|
|
1311
|
+
let pushed = false;
|
|
1312
|
+
if (push) {
|
|
1313
|
+
try {
|
|
1314
|
+
await execAsync("git push", { cwd });
|
|
1315
|
+
pushed = true;
|
|
1316
|
+
} catch (pushErr) {
|
|
1317
|
+
return {
|
|
1318
|
+
committed: true,
|
|
1319
|
+
pushed: false,
|
|
1320
|
+
error: `Push failed: ${pushErr instanceof Error ? pushErr.message : "Unknown error"}`
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return { committed: true, pushed };
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
return {
|
|
1327
|
+
committed: false,
|
|
1328
|
+
pushed: false,
|
|
1329
|
+
error: err instanceof Error ? err.message : "Git operation failed"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function truncate2(str, maxLen) {
|
|
1334
|
+
const oneLine = str.replace(/\n/g, " ").trim();
|
|
1335
|
+
if (oneLine.length <= maxLen) return oneLine;
|
|
1336
|
+
return oneLine.substring(0, maxLen - 3) + "...";
|
|
1337
|
+
}
|
|
1338
|
+
function printFix2(fix2, index) {
|
|
1339
|
+
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1340
|
+
console.log(`
|
|
1341
|
+
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1342
|
+
console.log(` Code: ${import_chalk3.default.dim(fix2.issue.code)}`);
|
|
1343
|
+
console.log(` File: ${import_chalk3.default.cyan(fix2.file)}`);
|
|
1344
|
+
if (fix2.skipped) {
|
|
1345
|
+
console.log(` Status: ${import_chalk3.default.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1346
|
+
} else if (fix2.applied) {
|
|
1347
|
+
console.log(` Status: ${import_chalk3.default.green("APPLIED")}`);
|
|
1348
|
+
console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1349
|
+
} else {
|
|
1350
|
+
console.log(` ${import_chalk3.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1351
|
+
if (fix2.before) {
|
|
1352
|
+
console.log(` ${import_chalk3.default.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1353
|
+
}
|
|
1354
|
+
console.log(` ${import_chalk3.default.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
async function fix(options) {
|
|
1358
|
+
const startTime = Date.now();
|
|
1359
|
+
const cwd = options.cwd || process.cwd();
|
|
1360
|
+
const url = options.url;
|
|
1361
|
+
if (!url) {
|
|
1362
|
+
const error = "Please provide a URL to fix:\n rankcli fix --url https://your-site.com";
|
|
1363
|
+
if (options.output !== "json") {
|
|
1364
|
+
console.log(`
|
|
1365
|
+
\u26A0\uFE0F ${error}
|
|
1366
|
+
`);
|
|
1367
|
+
}
|
|
1368
|
+
return { success: false, error };
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
new URL(url);
|
|
1372
|
+
} catch {
|
|
1373
|
+
const error = "Invalid URL format. Please provide a valid URL.";
|
|
1374
|
+
if (options.output !== "json") {
|
|
1375
|
+
console.log(`
|
|
1376
|
+
\u274C ${error}
|
|
1377
|
+
`);
|
|
1378
|
+
}
|
|
1379
|
+
return { success: false, error };
|
|
1380
|
+
}
|
|
1381
|
+
const loggedIn = isLoggedIn();
|
|
1382
|
+
const userInfo = getUserInfo();
|
|
1383
|
+
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1384
|
+
const hasAutoFix = loggedIn && hasFeature(tier, "auto_fix");
|
|
1385
|
+
if (!options.dryRun && !hasAutoFix) {
|
|
1386
|
+
if (options.output !== "json") {
|
|
1387
|
+
showUpsell("auto_fix_blocked");
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
error: "Auto-fix requires Solo tier or higher. Please upgrade at " + WEB_APP_URL + "/account"
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
if (options.output !== "json") {
|
|
1395
|
+
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1396
|
+
console.log("\u2550".repeat(50));
|
|
1397
|
+
}
|
|
1398
|
+
const frameworkSpinner = options.output !== "json" ? (0, import_ora2.default)("Detecting framework...").start() : null;
|
|
1399
|
+
const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
|
|
1400
|
+
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1401
|
+
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1402
|
+
const analyzeSpinner = options.output !== "json" ? (0, import_ora2.default)(`Analyzing ${url}...`).start() : null;
|
|
1403
|
+
let issues;
|
|
1404
|
+
let score;
|
|
1405
|
+
try {
|
|
1406
|
+
const analysis = await (0, import_agent_runtime3.runDirectAnalysis)({ url });
|
|
1407
|
+
issues = analysis.issues;
|
|
1408
|
+
score = analysis.score;
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1411
|
+
analyzeSpinner?.fail(`Analysis failed: ${errorMsg}`);
|
|
1412
|
+
return { success: false, error: `Analysis failed: ${errorMsg}` };
|
|
1413
|
+
}
|
|
1414
|
+
analyzeSpinner?.succeed(`Score: ${score}/100, Issues: ${issues.length}`);
|
|
1415
|
+
if (options.only && options.only.length > 0) {
|
|
1416
|
+
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1417
|
+
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1418
|
+
if (options.output !== "json") {
|
|
1419
|
+
console.log(import_chalk3.default.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (issues.length === 0) {
|
|
1423
|
+
if (options.output !== "json") {
|
|
1424
|
+
console.log("\n\u2705 No issues found! Your site looks great.\n");
|
|
1425
|
+
}
|
|
1426
|
+
return {
|
|
1427
|
+
success: true,
|
|
1428
|
+
url,
|
|
1429
|
+
dryRun: options.dryRun,
|
|
1430
|
+
fixes: [],
|
|
1431
|
+
appliedCount: 0,
|
|
1432
|
+
skippedCount: 0,
|
|
1433
|
+
durationMs: Date.now() - startTime
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
const fixSpinner = options.output !== "json" ? (0, import_ora2.default)("Generating fixes...").start() : null;
|
|
1437
|
+
const fixes = await (0, import_agent_runtime3.generateFixes)(issues, {
|
|
1438
|
+
cwd,
|
|
1439
|
+
url,
|
|
1440
|
+
framework
|
|
1441
|
+
});
|
|
1442
|
+
const actionableFixes = fixes.filter((f) => !f.skipped);
|
|
1443
|
+
const skippedFixes = fixes.filter((f) => f.skipped);
|
|
1444
|
+
fixSpinner?.succeed(`Generated ${actionableFixes.length} fix(es), ${skippedFixes.length} skipped`);
|
|
1445
|
+
if (options.dryRun) {
|
|
1446
|
+
if (options.output !== "json") {
|
|
1447
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1448
|
+
console.log(import_chalk3.default.yellow("DRY RUN - No changes will be made"));
|
|
1449
|
+
console.log("\u2500".repeat(50));
|
|
1450
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1451
|
+
printFix2(fixes[i], i);
|
|
1452
|
+
}
|
|
1453
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1454
|
+
console.log(`\u{1F4CB} ${import_chalk3.default.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1455
|
+
console.log(` Remove --dry-run to apply these fixes.
|
|
1456
|
+
`);
|
|
1457
|
+
}
|
|
1458
|
+
const result2 = {
|
|
1459
|
+
success: true,
|
|
1460
|
+
url,
|
|
1461
|
+
dryRun: true,
|
|
1462
|
+
fixes,
|
|
1463
|
+
appliedCount: 0,
|
|
1464
|
+
skippedCount: skippedFixes.length,
|
|
1465
|
+
durationMs: Date.now() - startTime
|
|
1466
|
+
};
|
|
1467
|
+
if (options.output === "json") {
|
|
1468
|
+
result2.json = JSON.stringify({
|
|
1469
|
+
url,
|
|
1470
|
+
dryRun: true,
|
|
1471
|
+
fixes: fixes.map((f) => ({
|
|
1472
|
+
code: f.issue.code,
|
|
1473
|
+
message: f.issue.message,
|
|
1474
|
+
file: f.file,
|
|
1475
|
+
skipped: f.skipped,
|
|
1476
|
+
skipReason: f.skipReason
|
|
1477
|
+
})),
|
|
1478
|
+
appliedCount: 0,
|
|
1479
|
+
skippedCount: skippedFixes.length,
|
|
1480
|
+
durationMs: result2.durationMs
|
|
1481
|
+
}, null, 2);
|
|
1482
|
+
}
|
|
1483
|
+
return result2;
|
|
1484
|
+
}
|
|
1485
|
+
if (actionableFixes.length === 0) {
|
|
1486
|
+
if (options.output !== "json") {
|
|
1487
|
+
console.log("\n\u26A0\uFE0F No automatic fixes available. Please address issues manually.\n");
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
success: true,
|
|
1491
|
+
url,
|
|
1492
|
+
fixes,
|
|
1493
|
+
appliedCount: 0,
|
|
1494
|
+
skippedCount: skippedFixes.length,
|
|
1495
|
+
durationMs: Date.now() - startTime
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
if (!options.auto) {
|
|
1499
|
+
if (options.output !== "json") {
|
|
1500
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1501
|
+
console.log("PROPOSED FIXES:");
|
|
1502
|
+
console.log("\u2500".repeat(50));
|
|
1503
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
1504
|
+
printFix2(fixes[i], i);
|
|
1505
|
+
}
|
|
1506
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1507
|
+
console.log(`Ready to apply ${actionableFixes.length} fix(es).`);
|
|
1508
|
+
console.log("Run with --auto to apply without confirmation.");
|
|
1509
|
+
console.log("Run with --dry-run to preview changes.\n");
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
success: true,
|
|
1513
|
+
url,
|
|
1514
|
+
fixes,
|
|
1515
|
+
appliedCount: 0,
|
|
1516
|
+
skippedCount: skippedFixes.length,
|
|
1517
|
+
durationMs: Date.now() - startTime
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const applySpinner = options.output !== "json" ? (0, import_ora2.default)("Applying fixes...").start() : null;
|
|
1521
|
+
const { applied, skipped } = await (0, import_agent_runtime3.applyFixes)(fixes, { cwd, dryRun: false });
|
|
1522
|
+
for (const fix2 of applied) {
|
|
1523
|
+
fix2.applied = true;
|
|
1524
|
+
}
|
|
1525
|
+
applySpinner?.succeed(`Applied ${applied.length} fix(es)`);
|
|
1526
|
+
const commitMessage = options.commit ? generateCommitMessage(fixes, url) : void 0;
|
|
1527
|
+
let committed = false;
|
|
1528
|
+
let pushed = false;
|
|
1529
|
+
let commitError;
|
|
1530
|
+
let pushError;
|
|
1531
|
+
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1532
|
+
const gitSpinner = options.output !== "json" ? (0, import_ora2.default)("Committing changes...").start() : null;
|
|
1533
|
+
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1534
|
+
committed = gitResult.committed;
|
|
1535
|
+
pushed = gitResult.pushed;
|
|
1536
|
+
if (gitResult.error) {
|
|
1537
|
+
if (gitResult.error.includes("Push")) {
|
|
1538
|
+
pushError = gitResult.error;
|
|
1539
|
+
gitSpinner?.warn("Committed but push failed");
|
|
1540
|
+
} else {
|
|
1541
|
+
commitError = gitResult.error;
|
|
1542
|
+
gitSpinner?.warn(`Commit failed: ${gitResult.error}`);
|
|
1543
|
+
}
|
|
1544
|
+
} else {
|
|
1545
|
+
if (pushed) {
|
|
1546
|
+
gitSpinner?.succeed("Committed and pushed");
|
|
1547
|
+
} else {
|
|
1548
|
+
gitSpinner?.succeed("Committed");
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (options.output !== "json") {
|
|
1553
|
+
console.log("\n" + "\u2550".repeat(50));
|
|
1554
|
+
console.log("RESULTS");
|
|
1555
|
+
console.log("\u2550".repeat(50));
|
|
1556
|
+
if (applied.length > 0) {
|
|
1557
|
+
console.log(`
|
|
1558
|
+
${import_chalk3.default.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1559
|
+
for (const fix2 of applied) {
|
|
1560
|
+
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (skipped.length > 0) {
|
|
1564
|
+
console.log(`
|
|
1565
|
+
${import_chalk3.default.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1566
|
+
for (const fix2 of skipped) {
|
|
1567
|
+
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (committed) {
|
|
1571
|
+
console.log(`
|
|
1572
|
+
${import_chalk3.default.green("\u2705")} Changes committed`);
|
|
1573
|
+
if (pushed) {
|
|
1574
|
+
console.log(`${import_chalk3.default.green("\u2705")} Changes pushed`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (!committed && applied.length > 0) {
|
|
1578
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1579
|
+
console.log("NEXT STEPS:");
|
|
1580
|
+
console.log("\u2500".repeat(50));
|
|
1581
|
+
console.log("1. Review the changes:");
|
|
1582
|
+
console.log(" git diff");
|
|
1583
|
+
console.log("\n2. Commit the SEO improvements:");
|
|
1584
|
+
console.log(` git add -A && git commit -m "feat(seo): fix ${applied.length} SEO issues"`);
|
|
1585
|
+
console.log("\n3. Re-run the audit to verify:");
|
|
1586
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1587
|
+
}
|
|
1588
|
+
console.log("");
|
|
1589
|
+
}
|
|
1590
|
+
const result = {
|
|
1591
|
+
success: true,
|
|
1592
|
+
url,
|
|
1593
|
+
dryRun: false,
|
|
1594
|
+
fixes,
|
|
1595
|
+
appliedCount: applied.length,
|
|
1596
|
+
skippedCount: skipped.length,
|
|
1597
|
+
durationMs: Date.now() - startTime,
|
|
1598
|
+
commitMessage,
|
|
1599
|
+
committed,
|
|
1600
|
+
pushed,
|
|
1601
|
+
commitError,
|
|
1602
|
+
pushError
|
|
1603
|
+
};
|
|
1604
|
+
if (options.output === "json") {
|
|
1605
|
+
result.json = JSON.stringify({
|
|
1606
|
+
url,
|
|
1607
|
+
fixes: fixes.map((f) => ({
|
|
1608
|
+
code: f.issue.code,
|
|
1609
|
+
message: f.issue.message,
|
|
1610
|
+
file: f.file,
|
|
1611
|
+
applied: f.applied,
|
|
1612
|
+
skipped: f.skipped,
|
|
1613
|
+
skipReason: f.skipReason
|
|
1614
|
+
})),
|
|
1615
|
+
appliedCount: applied.length,
|
|
1616
|
+
skippedCount: skipped.length,
|
|
1617
|
+
durationMs: result.durationMs,
|
|
1618
|
+
committed,
|
|
1619
|
+
pushed,
|
|
1620
|
+
commitMessage
|
|
1621
|
+
}, null, 2);
|
|
1622
|
+
}
|
|
1623
|
+
return result;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/commands/track.ts
|
|
1627
|
+
var import_ora3 = __toESM(require("ora"));
|
|
1628
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1629
|
+
var import_table = require("table");
|
|
1630
|
+
var import_agent_runtime4 = require("@rankcli/agent-runtime");
|
|
1631
|
+
async function getProjectForUrl(url) {
|
|
1632
|
+
const supabase = await getAuthenticatedClient();
|
|
1633
|
+
if (!supabase) return null;
|
|
1634
|
+
const { userId } = getUserInfo();
|
|
1635
|
+
if (!userId) return null;
|
|
1636
|
+
const parsedUrl = new URL(url);
|
|
1637
|
+
const domain = parsedUrl.hostname.replace(/^www\./, "");
|
|
1638
|
+
const { data, error } = await supabase.from("projects").select("id, domain, last_rank_check_at").eq("user_id", userId).eq("domain", domain).single();
|
|
1639
|
+
if (error || !data) {
|
|
1640
|
+
const { data: newProject, error: createError } = await supabase.from("projects").insert({
|
|
1641
|
+
user_id: userId,
|
|
1642
|
+
name: domain,
|
|
1643
|
+
url,
|
|
1644
|
+
domain
|
|
1645
|
+
}).select("id, domain, last_rank_check_at").single();
|
|
1646
|
+
if (createError || !newProject) return null;
|
|
1647
|
+
return {
|
|
1648
|
+
id: newProject.id,
|
|
1649
|
+
domain: newProject.domain,
|
|
1650
|
+
lastRankCheckAt: newProject.last_rank_check_at ? new Date(newProject.last_rank_check_at) : null
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
id: data.id,
|
|
1655
|
+
domain: data.domain,
|
|
1656
|
+
lastRankCheckAt: data.last_rank_check_at ? new Date(data.last_rank_check_at) : null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function getTierLimits(tier) {
|
|
1660
|
+
const limits = import_agent_runtime4.TIER_LIMITS[tier] || import_agent_runtime4.TIER_LIMITS.free;
|
|
1661
|
+
return {
|
|
1662
|
+
maxKeywords: limits.maxKeywords,
|
|
1663
|
+
checksPerDay: limits.checksPerDay
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function formatPosition(pos) {
|
|
1667
|
+
if (pos === null) return import_chalk4.default.dim("N/A");
|
|
1668
|
+
if (pos <= 3) return import_chalk4.default.green.bold(`#${pos}`);
|
|
1669
|
+
if (pos <= 10) return import_chalk4.default.green(`#${pos}`);
|
|
1670
|
+
if (pos <= 20) return import_chalk4.default.yellow(`#${pos}`);
|
|
1671
|
+
if (pos <= 50) return import_chalk4.default.red(`#${pos}`);
|
|
1672
|
+
return import_chalk4.default.red.dim(`#${pos}`);
|
|
1673
|
+
}
|
|
1674
|
+
function formatChange(change) {
|
|
1675
|
+
if (change === 0) return import_chalk4.default.dim("\u2192");
|
|
1676
|
+
if (change > 0) return import_chalk4.default.green(`\u2191${change}`);
|
|
1677
|
+
return import_chalk4.default.red(`\u2193${Math.abs(change)}`);
|
|
1678
|
+
}
|
|
1679
|
+
async function trackAdd(options) {
|
|
1680
|
+
if (!options.keywords || options.keywords.length === 0) {
|
|
1681
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1682
|
+
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1683
|
+
return { success: false, error: "No keywords provided" };
|
|
1684
|
+
}
|
|
1685
|
+
if (!options.url) {
|
|
1686
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1687
|
+
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1688
|
+
return { success: false, error: "No URL provided" };
|
|
1689
|
+
}
|
|
1690
|
+
if (!isLoggedIn()) {
|
|
1691
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1692
|
+
console.log(" rankcli login\n");
|
|
1693
|
+
return { success: false, error: "Not logged in" };
|
|
1694
|
+
}
|
|
1695
|
+
const spinner = (0, import_ora3.default)("Adding keywords to track...").start();
|
|
1696
|
+
try {
|
|
1697
|
+
const project = await getProjectForUrl(options.url);
|
|
1698
|
+
if (!project) {
|
|
1699
|
+
spinner.fail("Could not find or create project");
|
|
1700
|
+
return { success: false, error: "Project not found" };
|
|
1701
|
+
}
|
|
1702
|
+
const { planName } = getUserInfo();
|
|
1703
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1704
|
+
const { maxKeywords } = getTierLimits(tier);
|
|
1705
|
+
const supabase = await getAuthenticatedClient();
|
|
1706
|
+
if (!supabase) {
|
|
1707
|
+
spinner.fail("Could not connect to database");
|
|
1708
|
+
return { success: false, error: "Database connection failed" };
|
|
1709
|
+
}
|
|
1710
|
+
const { count } = await supabase.from("keywords").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("is_active", true);
|
|
1711
|
+
const existingCount = count || 0;
|
|
1712
|
+
const newCount = options.keywords.length;
|
|
1713
|
+
if (existingCount + newCount > maxKeywords) {
|
|
1714
|
+
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1715
|
+
showUpsell("keyword_limit", { tier, current: existingCount, limit: maxKeywords });
|
|
1716
|
+
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1717
|
+
}
|
|
1718
|
+
const keywordRecords = options.keywords.map((keyword) => ({
|
|
1719
|
+
project_id: project.id,
|
|
1720
|
+
keyword: keyword.toLowerCase().trim(),
|
|
1721
|
+
search_engine: options.searchEngine || "google",
|
|
1722
|
+
country: options.country || "US",
|
|
1723
|
+
language: options.language || "en",
|
|
1724
|
+
is_active: true
|
|
1725
|
+
}));
|
|
1726
|
+
const { data, error } = await supabase.from("keywords").upsert(keywordRecords, {
|
|
1727
|
+
onConflict: "project_id,keyword",
|
|
1728
|
+
ignoreDuplicates: false
|
|
1729
|
+
}).select();
|
|
1730
|
+
if (error) {
|
|
1731
|
+
spinner.fail(`Failed to add keywords: ${error.message}`);
|
|
1732
|
+
return { success: false, error: error.message };
|
|
1733
|
+
}
|
|
1734
|
+
const addedCount = data?.length || 0;
|
|
1735
|
+
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1736
|
+
console.log(import_chalk4.default.dim(`
|
|
1737
|
+
Tracking: ${options.keywords.join(", ")}`));
|
|
1738
|
+
console.log(import_chalk4.default.dim(`For: ${project.domain}`));
|
|
1739
|
+
console.log(import_chalk4.default.cyan(`
|
|
1740
|
+
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1741
|
+
`));
|
|
1742
|
+
return { success: true, addedCount };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
spinner.fail("Failed to add keywords");
|
|
1745
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function trackStatus(options) {
|
|
1749
|
+
if (!options.url) {
|
|
1750
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1751
|
+
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1752
|
+
return { success: false, error: "No URL provided" };
|
|
1753
|
+
}
|
|
1754
|
+
if (!isLoggedIn()) {
|
|
1755
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1756
|
+
console.log(" rankcli login\n");
|
|
1757
|
+
return { success: false, error: "Not logged in" };
|
|
1758
|
+
}
|
|
1759
|
+
const spinner = (0, import_ora3.default)("Loading keyword rankings...").start();
|
|
1760
|
+
try {
|
|
1761
|
+
const project = await getProjectForUrl(options.url);
|
|
1762
|
+
if (!project) {
|
|
1763
|
+
spinner.fail("Project not found");
|
|
1764
|
+
return { success: false, error: "Project not found" };
|
|
1765
|
+
}
|
|
1766
|
+
const supabase = await getAuthenticatedClient();
|
|
1767
|
+
if (!supabase) {
|
|
1768
|
+
spinner.fail("Could not connect to database");
|
|
1769
|
+
return { success: false, error: "Database connection failed" };
|
|
1770
|
+
}
|
|
1771
|
+
const { data, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true).order("current_position", { ascending: true, nullsFirst: false });
|
|
1772
|
+
if (error) {
|
|
1773
|
+
spinner.fail(`Failed to load keywords: ${error.message}`);
|
|
1774
|
+
return { success: false, error: error.message };
|
|
1775
|
+
}
|
|
1776
|
+
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1777
|
+
if (!data || data.length === 0) {
|
|
1778
|
+
console.log(import_chalk4.default.dim("\nNo keywords tracked yet."));
|
|
1779
|
+
console.log(import_chalk4.default.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
|
|
1780
|
+
`));
|
|
1781
|
+
return { success: true, keywords: [] };
|
|
1782
|
+
}
|
|
1783
|
+
const keywords2 = data.map((row) => ({
|
|
1784
|
+
keyword: row.keyword,
|
|
1785
|
+
currentPosition: row.current_position,
|
|
1786
|
+
previousPosition: row.previous_position,
|
|
1787
|
+
bestPosition: row.best_position,
|
|
1788
|
+
change: (row.previous_position || 0) - (row.current_position || 0),
|
|
1789
|
+
lastChecked: row.last_checked ? new Date(row.last_checked) : null
|
|
1790
|
+
}));
|
|
1791
|
+
if (options.output === "json") {
|
|
1792
|
+
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1793
|
+
} else {
|
|
1794
|
+
console.log("\n" + import_chalk4.default.bold("\u{1F4CA} Keyword Rankings"));
|
|
1795
|
+
console.log("\u2550".repeat(60));
|
|
1796
|
+
const tableData = [
|
|
1797
|
+
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
1798
|
+
...keywords2.map((kw) => [
|
|
1799
|
+
kw.keyword,
|
|
1800
|
+
formatPosition(kw.currentPosition),
|
|
1801
|
+
formatPosition(kw.bestPosition),
|
|
1802
|
+
formatChange(kw.change),
|
|
1803
|
+
kw.lastChecked ? kw.lastChecked.toLocaleDateString() : "Never"
|
|
1804
|
+
])
|
|
1805
|
+
];
|
|
1806
|
+
console.log((0, import_table.table)(tableData, {
|
|
1807
|
+
border: {
|
|
1808
|
+
topBody: "\u2500",
|
|
1809
|
+
topJoin: "\u252C",
|
|
1810
|
+
topLeft: "\u250C",
|
|
1811
|
+
topRight: "\u2510",
|
|
1812
|
+
bottomBody: "\u2500",
|
|
1813
|
+
bottomJoin: "\u2534",
|
|
1814
|
+
bottomLeft: "\u2514",
|
|
1815
|
+
bottomRight: "\u2518",
|
|
1816
|
+
bodyLeft: "\u2502",
|
|
1817
|
+
bodyRight: "\u2502",
|
|
1818
|
+
bodyJoin: "\u2502",
|
|
1819
|
+
joinBody: "\u2500",
|
|
1820
|
+
joinLeft: "\u251C",
|
|
1821
|
+
joinRight: "\u2524",
|
|
1822
|
+
joinJoin: "\u253C"
|
|
1823
|
+
}
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
return { success: true, keywords: keywords2 };
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
spinner.fail("Failed to load rankings");
|
|
1829
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
async function trackHistory(options) {
|
|
1833
|
+
if (!options.url) {
|
|
1834
|
+
return { success: false, error: "No URL provided" };
|
|
1835
|
+
}
|
|
1836
|
+
if (!options.keyword) {
|
|
1837
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
1838
|
+
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
1839
|
+
return { success: false, error: "No keyword provided" };
|
|
1840
|
+
}
|
|
1841
|
+
if (!isLoggedIn()) {
|
|
1842
|
+
return { success: false, error: "Not logged in" };
|
|
1843
|
+
}
|
|
1844
|
+
const spinner = (0, import_ora3.default)(`Loading history for "${options.keyword}"...`).start();
|
|
1845
|
+
try {
|
|
1846
|
+
const project = await getProjectForUrl(options.url);
|
|
1847
|
+
if (!project) {
|
|
1848
|
+
spinner.fail("Project not found");
|
|
1849
|
+
return { success: false, error: "Project not found" };
|
|
1850
|
+
}
|
|
1851
|
+
const supabase = await getAuthenticatedClient();
|
|
1852
|
+
if (!supabase) {
|
|
1853
|
+
spinner.fail("Could not connect to database");
|
|
1854
|
+
return { success: false, error: "Database connection failed" };
|
|
1855
|
+
}
|
|
1856
|
+
const { data: keywordData } = await supabase.from("keywords").select("id, keyword").eq("project_id", project.id).eq("keyword", options.keyword.toLowerCase().trim()).single();
|
|
1857
|
+
if (!keywordData) {
|
|
1858
|
+
spinner.fail("Keyword not found");
|
|
1859
|
+
return { success: false, error: "Keyword not found" };
|
|
1860
|
+
}
|
|
1861
|
+
const days = options.days || 30;
|
|
1862
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
1863
|
+
const { data: historyData, error } = await supabase.from("rankings").select("position, url, recorded_at").eq("keyword_id", keywordData.id).gte("recorded_at", cutoff.toISOString()).order("recorded_at", { ascending: false });
|
|
1864
|
+
if (error) {
|
|
1865
|
+
spinner.fail(`Failed to load history: ${error.message}`);
|
|
1866
|
+
return { success: false, error: error.message };
|
|
1867
|
+
}
|
|
1868
|
+
spinner.succeed(`Loaded ${historyData?.length || 0} data points`);
|
|
1869
|
+
const history = (historyData || []).map((row) => ({
|
|
1870
|
+
position: row.position,
|
|
1871
|
+
url: row.url,
|
|
1872
|
+
recordedAt: new Date(row.recorded_at)
|
|
1873
|
+
}));
|
|
1874
|
+
if (options.output === "json") {
|
|
1875
|
+
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
1876
|
+
} else {
|
|
1877
|
+
console.log("\n" + import_chalk4.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
1878
|
+
console.log("\u2550".repeat(50));
|
|
1879
|
+
if (history.length === 0) {
|
|
1880
|
+
console.log(import_chalk4.default.dim("\nNo history available yet.\n"));
|
|
1881
|
+
} else {
|
|
1882
|
+
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
1883
|
+
const sparkline = positions.map((p) => {
|
|
1884
|
+
if (p <= 10) return import_chalk4.default.green("\u2588");
|
|
1885
|
+
if (p <= 20) return import_chalk4.default.yellow("\u2593");
|
|
1886
|
+
if (p <= 50) return import_chalk4.default.red("\u2592");
|
|
1887
|
+
return import_chalk4.default.dim("\u2591");
|
|
1888
|
+
}).join("");
|
|
1889
|
+
console.log(`
|
|
1890
|
+
${sparkline} (last ${positions.length} checks)`);
|
|
1891
|
+
console.log(import_chalk4.default.dim(" \u2190 newer older \u2192\n"));
|
|
1892
|
+
const recent = history.slice(0, 10);
|
|
1893
|
+
for (const entry of recent) {
|
|
1894
|
+
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return { success: true, history };
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
spinner.fail("Failed to load history");
|
|
1901
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async function trackExport(options) {
|
|
1905
|
+
if (!options.url) {
|
|
1906
|
+
return { success: false, error: "No URL provided" };
|
|
1907
|
+
}
|
|
1908
|
+
if (!isLoggedIn()) {
|
|
1909
|
+
return { success: false, error: "Not logged in" };
|
|
1910
|
+
}
|
|
1911
|
+
const spinner = (0, import_ora3.default)("Exporting data...").start();
|
|
1912
|
+
try {
|
|
1913
|
+
const project = await getProjectForUrl(options.url);
|
|
1914
|
+
if (!project) {
|
|
1915
|
+
spinner.fail("Project not found");
|
|
1916
|
+
return { success: false, error: "Project not found" };
|
|
1917
|
+
}
|
|
1918
|
+
const supabase = await getAuthenticatedClient();
|
|
1919
|
+
if (!supabase) {
|
|
1920
|
+
spinner.fail("Could not connect to database");
|
|
1921
|
+
return { success: false, error: "Database connection failed" };
|
|
1922
|
+
}
|
|
1923
|
+
const { data: keywords2, error } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
1924
|
+
if (error) {
|
|
1925
|
+
spinner.fail(`Failed to export: ${error.message}`);
|
|
1926
|
+
return { success: false, error: error.message };
|
|
1927
|
+
}
|
|
1928
|
+
const format = options.format || "csv";
|
|
1929
|
+
if (format === "json") {
|
|
1930
|
+
const data = JSON.stringify({
|
|
1931
|
+
domain: project.domain,
|
|
1932
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1933
|
+
keywords: keywords2?.map((kw) => ({
|
|
1934
|
+
keyword: kw.keyword,
|
|
1935
|
+
currentPosition: kw.current_position,
|
|
1936
|
+
bestPosition: kw.best_position,
|
|
1937
|
+
lastChecked: kw.last_checked
|
|
1938
|
+
}))
|
|
1939
|
+
}, null, 2);
|
|
1940
|
+
spinner.succeed("Exported as JSON");
|
|
1941
|
+
console.log(data);
|
|
1942
|
+
return { success: true, data };
|
|
1943
|
+
} else {
|
|
1944
|
+
const rows = ["Keyword,Current Position,Best Position,Last Checked,Trend"];
|
|
1945
|
+
for (const kw of keywords2 || []) {
|
|
1946
|
+
const trend = (kw.previous_position || 0) - (kw.current_position || 0);
|
|
1947
|
+
const trendStr = trend > 0 ? "\u2191" : trend < 0 ? "\u2193" : "\u2192";
|
|
1948
|
+
rows.push([
|
|
1949
|
+
`"${kw.keyword}"`,
|
|
1950
|
+
kw.current_position?.toString() || "N/A",
|
|
1951
|
+
kw.best_position?.toString() || "N/A",
|
|
1952
|
+
kw.last_checked || "Never",
|
|
1953
|
+
trendStr
|
|
1954
|
+
].join(","));
|
|
1955
|
+
}
|
|
1956
|
+
const data = rows.join("\n");
|
|
1957
|
+
spinner.succeed("Exported as CSV");
|
|
1958
|
+
console.log(data);
|
|
1959
|
+
return { success: true, data };
|
|
1960
|
+
}
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
spinner.fail("Failed to export");
|
|
1963
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function trackCheck(options) {
|
|
1967
|
+
if (!options.url) {
|
|
1968
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1969
|
+
console.log(" rankcli track check --url https://your-site.com\n");
|
|
1970
|
+
return { success: false, error: "No URL provided" };
|
|
1971
|
+
}
|
|
1972
|
+
if (!isLoggedIn()) {
|
|
1973
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
1974
|
+
console.log(" rankcli login\n");
|
|
1975
|
+
return { success: false, error: "Not logged in" };
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
const project = await getProjectForUrl(options.url);
|
|
1979
|
+
if (!project) {
|
|
1980
|
+
return { success: false, error: "Project not found" };
|
|
1981
|
+
}
|
|
1982
|
+
const { planName } = getUserInfo();
|
|
1983
|
+
const tier = planName?.toLowerCase() || "free";
|
|
1984
|
+
const { checksPerDay } = getTierLimits(tier);
|
|
1985
|
+
if (!options.force && project.lastRankCheckAt) {
|
|
1986
|
+
const hoursSinceCheck = (Date.now() - project.lastRankCheckAt.getTime()) / (1e3 * 60 * 60);
|
|
1987
|
+
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
1988
|
+
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
1989
|
+
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
1990
|
+
console.log(import_chalk4.default.yellow(`
|
|
1991
|
+
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
1992
|
+
console.log(import_chalk4.default.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
1993
|
+
`));
|
|
1994
|
+
return { success: true, rateLimited: true };
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const spinner = (0, import_ora3.default)("Checking current rankings...").start();
|
|
1998
|
+
const supabase = await getAuthenticatedClient();
|
|
1999
|
+
if (!supabase) {
|
|
2000
|
+
spinner.fail("Could not connect to database");
|
|
2001
|
+
return { success: false, error: "Database connection failed" };
|
|
2002
|
+
}
|
|
2003
|
+
const { data: keywords2 } = await supabase.from("keywords").select("*").eq("project_id", project.id).eq("is_active", true);
|
|
2004
|
+
if (!keywords2 || keywords2.length === 0) {
|
|
2005
|
+
spinner.fail("No keywords to check");
|
|
2006
|
+
return { success: false, error: "No keywords tracked" };
|
|
2007
|
+
}
|
|
2008
|
+
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2009
|
+
if (!serpApiKey) {
|
|
2010
|
+
spinner.warn("No SERP API key configured");
|
|
2011
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
|
|
2012
|
+
console.log(import_chalk4.default.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
|
|
2013
|
+
return { success: false, error: "No SERP API key" };
|
|
2014
|
+
}
|
|
2015
|
+
const tracker = new import_agent_runtime4.RankTracker({
|
|
2016
|
+
supabase,
|
|
2017
|
+
serpConfig: {
|
|
2018
|
+
provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
|
|
2019
|
+
apiKey: serpApiKey
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2023
|
+
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2024
|
+
console.log("\n" + import_chalk4.default.bold("\u{1F50D} Rank Check Results"));
|
|
2025
|
+
console.log("\u2550".repeat(50));
|
|
2026
|
+
for (const result of results) {
|
|
2027
|
+
const posStr = formatPosition(result.position);
|
|
2028
|
+
console.log(` ${result.keyword}: ${posStr}`);
|
|
2029
|
+
if (result.serpFeatures.length > 0) {
|
|
2030
|
+
console.log(import_chalk4.default.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
console.log("");
|
|
2034
|
+
return {
|
|
2035
|
+
success: true,
|
|
2036
|
+
checkResults: results.map((r) => ({
|
|
2037
|
+
keyword: r.keyword,
|
|
2038
|
+
position: r.position,
|
|
2039
|
+
url: r.url
|
|
2040
|
+
}))
|
|
2041
|
+
};
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/commands/geo.ts
|
|
2048
|
+
var import_agent_runtime5 = require("@rankcli/agent-runtime");
|
|
2049
|
+
var import_ora4 = __toESM(require("ora"));
|
|
2050
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1252
2051
|
async function fetchResources(url) {
|
|
1253
2052
|
const headers = {};
|
|
1254
2053
|
let html = null;
|
|
@@ -1294,7 +2093,7 @@ async function geo(options) {
|
|
|
1294
2093
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1295
2094
|
return;
|
|
1296
2095
|
}
|
|
1297
|
-
const spinner = (0,
|
|
2096
|
+
const spinner = (0, import_ora4.default)("Fetching page and robots.txt...").start();
|
|
1298
2097
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
1299
2098
|
if (!html) {
|
|
1300
2099
|
spinner.fail("Could not fetch page content");
|
|
@@ -1302,11 +2101,11 @@ async function geo(options) {
|
|
|
1302
2101
|
}
|
|
1303
2102
|
spinner.text = "Running GEO analysis...";
|
|
1304
2103
|
try {
|
|
1305
|
-
const geoResult = await
|
|
1306
|
-
const cwvResult =
|
|
1307
|
-
const securityResult =
|
|
1308
|
-
const schemaResult =
|
|
1309
|
-
const mobileResult =
|
|
2104
|
+
const geoResult = await import_agent_runtime5.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
|
|
2105
|
+
const cwvResult = import_agent_runtime5.analyzers.analyzeCoreWebVitals(html, url, headers);
|
|
2106
|
+
const securityResult = import_agent_runtime5.analyzers.analyzeSecurityHeaders(headers, url);
|
|
2107
|
+
const schemaResult = import_agent_runtime5.analyzers.analyzeStructuredData(html, url);
|
|
2108
|
+
const mobileResult = import_agent_runtime5.analyzers.analyzeMobileSEO(html, url);
|
|
1310
2109
|
spinner.succeed("GEO analysis complete");
|
|
1311
2110
|
if (options.output === "json") {
|
|
1312
2111
|
const jsonOutput = {
|
|
@@ -1320,55 +2119,55 @@ async function geo(options) {
|
|
|
1320
2119
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1321
2120
|
return;
|
|
1322
2121
|
}
|
|
1323
|
-
console.log("\n" +
|
|
1324
|
-
console.log(
|
|
1325
|
-
console.log(
|
|
2122
|
+
console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
|
|
2123
|
+
console.log(import_chalk5.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2124
|
+
console.log(import_chalk5.default.bold("\u2550".repeat(60)));
|
|
1326
2125
|
const geoScore = geoResult.score;
|
|
1327
|
-
const scoreColor = geoScore >= 80 ?
|
|
2126
|
+
const scoreColor = geoScore >= 80 ? import_chalk5.default.green : geoScore >= 60 ? import_chalk5.default.yellow : import_chalk5.default.red;
|
|
1328
2127
|
console.log(`
|
|
1329
|
-
${
|
|
1330
|
-
console.log("\n" +
|
|
2128
|
+
${import_chalk5.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2129
|
+
console.log("\n" + import_chalk5.default.bold(" AI Crawler Access:"));
|
|
1331
2130
|
const { aiCrawlerAccess } = geoResult;
|
|
1332
2131
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
1333
|
-
console.log(
|
|
2132
|
+
console.log(import_chalk5.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
1334
2133
|
}
|
|
1335
2134
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
1336
2135
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
1337
|
-
console.log(
|
|
2136
|
+
console.log(import_chalk5.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
1338
2137
|
}
|
|
1339
|
-
console.log("\n" +
|
|
2138
|
+
console.log("\n" + import_chalk5.default.bold(" Rendering:"));
|
|
1340
2139
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
1341
|
-
console.log(
|
|
2140
|
+
console.log(import_chalk5.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
1342
2141
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
1343
|
-
console.log(
|
|
2142
|
+
console.log(import_chalk5.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
1344
2143
|
}
|
|
1345
|
-
console.log("\n" +
|
|
2144
|
+
console.log("\n" + import_chalk5.default.bold(" LLM Friendliness:"));
|
|
1346
2145
|
const { llmSignals } = geoResult;
|
|
1347
2146
|
const bar = (score) => {
|
|
1348
2147
|
const filled = Math.round(score / 5);
|
|
1349
2148
|
const empty = 20 - filled;
|
|
1350
|
-
const color = score >= 70 ?
|
|
1351
|
-
return color("\u2588".repeat(filled)) +
|
|
2149
|
+
const color = score >= 70 ? import_chalk5.default.green : score >= 40 ? import_chalk5.default.yellow : import_chalk5.default.red;
|
|
2150
|
+
return color("\u2588".repeat(filled)) + import_chalk5.default.gray("\u2591".repeat(empty));
|
|
1352
2151
|
};
|
|
1353
2152
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
1354
2153
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
1355
2154
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
1356
2155
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
1357
|
-
console.log("\n" +
|
|
2156
|
+
console.log("\n" + import_chalk5.default.bold(" Content Structure:"));
|
|
1358
2157
|
const { contentStructure } = geoResult;
|
|
1359
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
1360
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
1361
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
1362
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
1363
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
1364
|
-
console.log("\n" +
|
|
2158
|
+
console.log(` ${contentStructure.hasStructuredData ? import_chalk5.default.green("\u2705") : import_chalk5.default.red("\u274C")} JSON-LD Structured Data`);
|
|
2159
|
+
console.log(` ${contentStructure.hasFAQSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} FAQ Schema`);
|
|
2160
|
+
console.log(` ${contentStructure.hasArticleSchema ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Article Schema`);
|
|
2161
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk5.default.green("\u2705") : import_chalk5.default.yellow("\u25CB")} Breadcrumbs`);
|
|
2162
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk5.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk5.default.yellow("Needs Work") : import_chalk5.default.red("Poor")}`);
|
|
2163
|
+
console.log("\n" + import_chalk5.default.bold(" Citation Readiness:"));
|
|
1365
2164
|
const { citationReadiness } = geoResult;
|
|
1366
2165
|
if (citationReadiness.trustSignals.length > 0) {
|
|
1367
|
-
console.log(
|
|
2166
|
+
console.log(import_chalk5.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
1368
2167
|
} else {
|
|
1369
|
-
console.log(
|
|
2168
|
+
console.log(import_chalk5.default.yellow(" \u25CB No trust signals detected"));
|
|
1370
2169
|
}
|
|
1371
|
-
console.log("\n" +
|
|
2170
|
+
console.log("\n" + import_chalk5.default.bold(" Other Scores:"));
|
|
1372
2171
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
1373
2172
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
1374
2173
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -1382,18 +2181,18 @@ async function geo(options) {
|
|
|
1382
2181
|
];
|
|
1383
2182
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
1384
2183
|
if (criticalIssues.length > 0) {
|
|
1385
|
-
console.log("\n" +
|
|
2184
|
+
console.log("\n" + import_chalk5.default.bold.red(" Critical Issues:"));
|
|
1386
2185
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
1387
|
-
console.log(
|
|
2186
|
+
console.log(import_chalk5.default.red(` ${i + 1}. ${issue.title}`));
|
|
1388
2187
|
});
|
|
1389
2188
|
}
|
|
1390
2189
|
if (geoResult.recommendations.length > 0) {
|
|
1391
|
-
console.log("\n" +
|
|
2190
|
+
console.log("\n" + import_chalk5.default.bold(" Recommendations:"));
|
|
1392
2191
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
1393
2192
|
console.log(` ${i + 1}. ${rec}`);
|
|
1394
2193
|
});
|
|
1395
2194
|
}
|
|
1396
|
-
console.log("\n" +
|
|
2195
|
+
console.log("\n" + import_chalk5.default.bold("\u2550".repeat(60)));
|
|
1397
2196
|
console.log("");
|
|
1398
2197
|
} catch (error) {
|
|
1399
2198
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1402,7 +2201,7 @@ async function geo(options) {
|
|
|
1402
2201
|
|
|
1403
2202
|
// src/commands/keywords.ts
|
|
1404
2203
|
var readline = __toESM(require("readline"));
|
|
1405
|
-
var
|
|
2204
|
+
var import_agent_runtime6 = require("@rankcli/agent-runtime");
|
|
1406
2205
|
async function keywords(options) {
|
|
1407
2206
|
console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
|
|
1408
2207
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
@@ -1411,7 +2210,7 @@ async function keywords(options) {
|
|
|
1411
2210
|
input: process.stdin,
|
|
1412
2211
|
output: process.stdout
|
|
1413
2212
|
});
|
|
1414
|
-
const
|
|
2213
|
+
const ask2 = (question) => {
|
|
1415
2214
|
return new Promise((resolve) => {
|
|
1416
2215
|
rl.question(question, (answer) => {
|
|
1417
2216
|
resolve(answer.trim());
|
|
@@ -1426,7 +2225,7 @@ ${question}`);
|
|
|
1426
2225
|
console.log(` ${choice.description}`);
|
|
1427
2226
|
});
|
|
1428
2227
|
while (true) {
|
|
1429
|
-
const answer = await
|
|
2228
|
+
const answer = await ask2("\nEnter your choice (1-" + choices.length + "): ");
|
|
1430
2229
|
const num = parseInt(answer, 10);
|
|
1431
2230
|
if (num >= 1 && num <= choices.length) {
|
|
1432
2231
|
return choices[num - 1].value;
|
|
@@ -1437,7 +2236,7 @@ ${question}`);
|
|
|
1437
2236
|
try {
|
|
1438
2237
|
let url = options.url;
|
|
1439
2238
|
if (!url) {
|
|
1440
|
-
url = await
|
|
2239
|
+
url = await ask2("Enter your website URL: ");
|
|
1441
2240
|
}
|
|
1442
2241
|
try {
|
|
1443
2242
|
const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
|
|
@@ -1453,7 +2252,7 @@ ${question}`);
|
|
|
1453
2252
|
seedKeywords = options.seed.split(",").map((s) => s.trim());
|
|
1454
2253
|
} else if (options.auto) {
|
|
1455
2254
|
console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
|
|
1456
|
-
seedKeywords = await (0,
|
|
2255
|
+
seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
|
|
1457
2256
|
if (seedKeywords.length > 0) {
|
|
1458
2257
|
console.log(`Found: ${seedKeywords.join(", ")}`);
|
|
1459
2258
|
}
|
|
@@ -1461,7 +2260,7 @@ ${question}`);
|
|
|
1461
2260
|
if (seedKeywords.length === 0) {
|
|
1462
2261
|
console.log("\n\u{1F4DD} What is your website/product about?");
|
|
1463
2262
|
console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
|
|
1464
|
-
const seedInput = await
|
|
2263
|
+
const seedInput = await ask2(" > ");
|
|
1465
2264
|
seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1466
2265
|
if (seedKeywords.length === 0) {
|
|
1467
2266
|
console.log("\n\u274C Please provide at least one seed keyword.\n");
|
|
@@ -1478,7 +2277,7 @@ ${question}`);
|
|
|
1478
2277
|
contentCapacity: "medium",
|
|
1479
2278
|
targetGeo: "us"
|
|
1480
2279
|
};
|
|
1481
|
-
for (const q of
|
|
2280
|
+
for (const q of import_agent_runtime6.SITE_PROFILE_QUESTIONS) {
|
|
1482
2281
|
const answer = await askChoice(q.question, q.options);
|
|
1483
2282
|
siteProfile[q.id] = answer;
|
|
1484
2283
|
}
|
|
@@ -1497,13 +2296,13 @@ ${question}`);
|
|
|
1497
2296
|
url,
|
|
1498
2297
|
maxKeywords: 100
|
|
1499
2298
|
};
|
|
1500
|
-
const result = await (0,
|
|
1501
|
-
console.log((0,
|
|
2299
|
+
const result = await (0, import_agent_runtime6.runKeywordResearch)(researchOptions);
|
|
2300
|
+
console.log((0, import_agent_runtime6.formatKeywordReport)(result));
|
|
1502
2301
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
1503
2302
|
if (allKeywords.length > 5) {
|
|
1504
2303
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
1505
|
-
const topicResult = (0,
|
|
1506
|
-
console.log((0,
|
|
2304
|
+
const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
|
|
2305
|
+
console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
|
|
1507
2306
|
}
|
|
1508
2307
|
if (result.quickWins.length > 0) {
|
|
1509
2308
|
console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
|
|
@@ -1528,7 +2327,7 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
1528
2327
|
`);
|
|
1529
2328
|
} else {
|
|
1530
2329
|
console.log("\u{1F50D} Extracting keywords from your page...");
|
|
1531
|
-
seedKeywords = await (0,
|
|
2330
|
+
seedKeywords = await (0, import_agent_runtime6.extractSeedKeywords)(url);
|
|
1532
2331
|
if (seedKeywords.length === 0) {
|
|
1533
2332
|
console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
|
|
1534
2333
|
return;
|
|
@@ -1544,18 +2343,18 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
1544
2343
|
contentCapacity: "medium",
|
|
1545
2344
|
targetGeo: "us"
|
|
1546
2345
|
};
|
|
1547
|
-
const result = await (0,
|
|
2346
|
+
const result = await (0, import_agent_runtime6.runKeywordResearch)({
|
|
1548
2347
|
seedKeywords,
|
|
1549
2348
|
siteProfile,
|
|
1550
2349
|
url,
|
|
1551
2350
|
maxKeywords: 50
|
|
1552
2351
|
});
|
|
1553
|
-
console.log((0,
|
|
2352
|
+
console.log((0, import_agent_runtime6.formatKeywordReport)(result));
|
|
1554
2353
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
1555
2354
|
if (allKeywords.length > 5) {
|
|
1556
2355
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
1557
|
-
const topicResult = (0,
|
|
1558
|
-
console.log((0,
|
|
2356
|
+
const topicResult = (0, import_agent_runtime6.groupKeywordsByTopic)(allKeywords);
|
|
2357
|
+
console.log((0, import_agent_runtime6.formatTopicReport)(topicResult));
|
|
1559
2358
|
}
|
|
1560
2359
|
} catch (error) {
|
|
1561
2360
|
console.log(`
|
|
@@ -1618,7 +2417,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
1618
2417
|
return;
|
|
1619
2418
|
}
|
|
1620
2419
|
console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
|
|
1621
|
-
const result = await (0,
|
|
2420
|
+
const result = await (0, import_agent_runtime6.runAIKeywordResearch)({
|
|
1622
2421
|
url,
|
|
1623
2422
|
tier: "solo",
|
|
1624
2423
|
openaiApiKey,
|
|
@@ -1737,12 +2536,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
1737
2536
|
}
|
|
1738
2537
|
console.log("");
|
|
1739
2538
|
console.log("Analyzing competitor keywords (this may take a moment)...\n");
|
|
1740
|
-
const result = await (0,
|
|
2539
|
+
const result = await (0, import_agent_runtime6.discoverCompetitorKeywords)(
|
|
1741
2540
|
yourDomain,
|
|
1742
2541
|
seedKeywords,
|
|
1743
2542
|
competitors
|
|
1744
2543
|
);
|
|
1745
|
-
console.log((0,
|
|
2544
|
+
console.log((0, import_agent_runtime6.formatCompetitorReport)(result));
|
|
1746
2545
|
if (result.missingKeywords.length > 0) {
|
|
1747
2546
|
console.log("\n\u{1F3AF} ACTION ITEMS:");
|
|
1748
2547
|
console.log("\u2500".repeat(60));
|
|
@@ -1761,7 +2560,7 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
1761
2560
|
}
|
|
1762
2561
|
|
|
1763
2562
|
// src/commands/content.ts
|
|
1764
|
-
var
|
|
2563
|
+
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
1765
2564
|
async function analyzeContent(options) {
|
|
1766
2565
|
console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
|
|
1767
2566
|
const mode = options.mode || "full";
|
|
@@ -1771,12 +2570,12 @@ async function analyzeContent(options) {
|
|
|
1771
2570
|
console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
|
|
1772
2571
|
return;
|
|
1773
2572
|
}
|
|
1774
|
-
const analysis = (0,
|
|
1775
|
-
console.log((0,
|
|
2573
|
+
const analysis = (0, import_agent_runtime7.analyzeHeadline)(headline);
|
|
2574
|
+
console.log((0, import_agent_runtime7.formatHeadlineReport)(analysis));
|
|
1776
2575
|
if (options.keyword) {
|
|
1777
2576
|
console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
|
|
1778
2577
|
console.log("\u2500".repeat(60));
|
|
1779
|
-
const variations = (0,
|
|
2578
|
+
const variations = (0, import_agent_runtime7.generateHeadlineVariations)(options.keyword, [options.keyword]);
|
|
1780
2579
|
for (const v of variations.slice(0, 5)) {
|
|
1781
2580
|
console.log(` \u2022 ${v}`);
|
|
1782
2581
|
}
|
|
@@ -1787,7 +2586,7 @@ async function analyzeContent(options) {
|
|
|
1787
2586
|
if (options.url) {
|
|
1788
2587
|
console.log(`Fetching content from ${options.url}...`);
|
|
1789
2588
|
try {
|
|
1790
|
-
const result = await (0,
|
|
2589
|
+
const result = await (0, import_agent_runtime7.crawlUrl)(options.url);
|
|
1791
2590
|
if (!result.html) {
|
|
1792
2591
|
console.log("\u274C Could not fetch content from URL");
|
|
1793
2592
|
return;
|
|
@@ -1801,15 +2600,15 @@ async function analyzeContent(options) {
|
|
|
1801
2600
|
Analyzing: "${title || options.url}"
|
|
1802
2601
|
`);
|
|
1803
2602
|
if (mode === "full" || mode === "readability") {
|
|
1804
|
-
const readability = (0,
|
|
1805
|
-
console.log((0,
|
|
2603
|
+
const readability = (0, import_agent_runtime7.analyzeReadability)(bodyText);
|
|
2604
|
+
console.log((0, import_agent_runtime7.formatReadabilityReport)(readability));
|
|
1806
2605
|
}
|
|
1807
2606
|
if (mode === "full" && title) {
|
|
1808
|
-
const headlineAnalysis = (0,
|
|
1809
|
-
console.log((0,
|
|
2607
|
+
const headlineAnalysis = (0, import_agent_runtime7.analyzeHeadline)(title);
|
|
2608
|
+
console.log((0, import_agent_runtime7.formatHeadlineReport)(headlineAnalysis));
|
|
1810
2609
|
}
|
|
1811
2610
|
if (options.keyword && (mode === "full" || mode === "density")) {
|
|
1812
|
-
const densityAnalysis = (0,
|
|
2611
|
+
const densityAnalysis = (0, import_agent_runtime7.analyzeKeywordDensity)(options.keyword, {
|
|
1813
2612
|
title,
|
|
1814
2613
|
h1,
|
|
1815
2614
|
headings,
|
|
@@ -1817,19 +2616,19 @@ Analyzing: "${title || options.url}"
|
|
|
1817
2616
|
firstParagraph: paragraphs[0],
|
|
1818
2617
|
lastParagraph: paragraphs[paragraphs.length - 1]
|
|
1819
2618
|
});
|
|
1820
|
-
console.log((0,
|
|
2619
|
+
console.log((0, import_agent_runtime7.formatKeywordDensityReport)(densityAnalysis));
|
|
1821
2620
|
}
|
|
1822
2621
|
if (options.keyword && (mode === "full" || mode === "snippet")) {
|
|
1823
|
-
const snippetAnalysis = (0,
|
|
2622
|
+
const snippetAnalysis = (0, import_agent_runtime7.analyzeFeaturedSnippetPotential)(
|
|
1824
2623
|
options.keyword,
|
|
1825
2624
|
bodyText,
|
|
1826
2625
|
headings
|
|
1827
2626
|
);
|
|
1828
|
-
console.log((0,
|
|
2627
|
+
console.log((0, import_agent_runtime7.formatFeaturedSnippetReport)(snippetAnalysis));
|
|
1829
2628
|
}
|
|
1830
2629
|
if (options.keyword) {
|
|
1831
|
-
const intentAnalysis = (0,
|
|
1832
|
-
console.log((0,
|
|
2630
|
+
const intentAnalysis = (0, import_agent_runtime7.classifyIntent)(options.keyword);
|
|
2631
|
+
console.log((0, import_agent_runtime7.formatIntentReport)(options.keyword, intentAnalysis));
|
|
1833
2632
|
}
|
|
1834
2633
|
console.log("\n\u{1F4CA} CONTENT SUMMARY");
|
|
1835
2634
|
console.log("\u2500".repeat(60));
|
|
@@ -1856,8 +2655,8 @@ Analyzing: "${title || options.url}"
|
|
|
1856
2655
|
}
|
|
1857
2656
|
async function analyzeIntent(keyword) {
|
|
1858
2657
|
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
1859
|
-
const analysis = (0,
|
|
1860
|
-
console.log((0,
|
|
2658
|
+
const analysis = (0, import_agent_runtime7.classifyIntent)(keyword);
|
|
2659
|
+
console.log((0, import_agent_runtime7.formatIntentReport)(keyword, analysis));
|
|
1861
2660
|
}
|
|
1862
2661
|
function extractBodyText(html) {
|
|
1863
2662
|
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
@@ -1876,7 +2675,7 @@ function extractBodyText(html) {
|
|
|
1876
2675
|
var readline2 = __toESM(require("readline"));
|
|
1877
2676
|
var fs = __toESM(require("fs"));
|
|
1878
2677
|
var path = __toESM(require("path"));
|
|
1879
|
-
var
|
|
2678
|
+
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
1880
2679
|
async function setup(options) {
|
|
1881
2680
|
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
1882
2681
|
const projectPath = process.cwd();
|
|
@@ -1902,7 +2701,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1902
2701
|
input: process.stdin,
|
|
1903
2702
|
output: process.stdout
|
|
1904
2703
|
});
|
|
1905
|
-
const
|
|
2704
|
+
const ask2 = (question) => {
|
|
1906
2705
|
return new Promise((resolve) => {
|
|
1907
2706
|
rl.question(question, (answer) => {
|
|
1908
2707
|
resolve(answer.trim());
|
|
@@ -1916,7 +2715,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1916
2715
|
console.log("GA4 tracks visitor behavior on your site.");
|
|
1917
2716
|
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
1918
2717
|
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
1919
|
-
const ga4Id = await
|
|
2718
|
+
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
1920
2719
|
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
1921
2720
|
await setupGA4(projectPath, ga4Id);
|
|
1922
2721
|
} else if (ga4Id) {
|
|
@@ -1929,7 +2728,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1929
2728
|
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
1930
2729
|
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
1931
2730
|
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
1932
|
-
const gscCode = await
|
|
2731
|
+
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
1933
2732
|
if (gscCode && gscCode.length > 10) {
|
|
1934
2733
|
await setupGSC(projectPath, gscCode);
|
|
1935
2734
|
} else if (gscCode) {
|
|
@@ -1941,20 +2740,20 @@ async function interactiveSetup(projectPath) {
|
|
|
1941
2740
|
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
1942
2741
|
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
1943
2742
|
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
1944
|
-
const setupAction = await
|
|
2743
|
+
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
1945
2744
|
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
1946
2745
|
console.log("\nHow often should the check run?");
|
|
1947
2746
|
console.log(" 1. Weekly (recommended for most sites)");
|
|
1948
2747
|
console.log(" 2. Daily (for high-traffic sites)");
|
|
1949
2748
|
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
1950
|
-
const scheduleChoice = await
|
|
2749
|
+
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
1951
2750
|
const scheduleMap = {
|
|
1952
2751
|
"1": "weekly",
|
|
1953
2752
|
"2": "daily",
|
|
1954
2753
|
"3": "monthly"
|
|
1955
2754
|
};
|
|
1956
2755
|
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
1957
|
-
const siteUrl = await
|
|
2756
|
+
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
1958
2757
|
if (siteUrl) {
|
|
1959
2758
|
await setupGitHubAction(projectPath, schedule, siteUrl);
|
|
1960
2759
|
}
|
|
@@ -1978,7 +2777,7 @@ async function interactiveSetup(projectPath) {
|
|
|
1978
2777
|
}
|
|
1979
2778
|
async function setupGA4(projectPath, measurementId) {
|
|
1980
2779
|
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
1981
|
-
const result = await (0,
|
|
2780
|
+
const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
|
|
1982
2781
|
if (result.success) {
|
|
1983
2782
|
console.log(`\u2705 ${result.message}`);
|
|
1984
2783
|
if (result.file) {
|
|
@@ -1992,7 +2791,7 @@ async function setupGA4(projectPath, measurementId) {
|
|
|
1992
2791
|
}
|
|
1993
2792
|
}
|
|
1994
2793
|
const envExamplePath = path.join(projectPath, ".env.example");
|
|
1995
|
-
const envContent = (0,
|
|
2794
|
+
const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
|
|
1996
2795
|
if (fs.existsSync(envExamplePath)) {
|
|
1997
2796
|
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
1998
2797
|
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
@@ -2007,7 +2806,7 @@ async function setupGA4(projectPath, measurementId) {
|
|
|
2007
2806
|
}
|
|
2008
2807
|
async function setupGSC(projectPath, verificationCode) {
|
|
2009
2808
|
console.log("Setting up Google Search Console verification...");
|
|
2010
|
-
const result = await (0,
|
|
2809
|
+
const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
|
|
2011
2810
|
if (result.success) {
|
|
2012
2811
|
console.log(`\u2705 ${result.message}`);
|
|
2013
2812
|
if (result.file) {
|
|
@@ -2031,7 +2830,7 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
|
2031
2830
|
createPRs: true
|
|
2032
2831
|
}
|
|
2033
2832
|
};
|
|
2034
|
-
const { files, instructions } = (0,
|
|
2833
|
+
const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
|
|
2035
2834
|
console.log("\u2705 GitHub Action files created:");
|
|
2036
2835
|
for (const file of files) {
|
|
2037
2836
|
console.log(` - ${path.relative(projectPath, file)}`);
|
|
@@ -2039,34 +2838,199 @@ async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
|
2039
2838
|
console.log(instructions);
|
|
2040
2839
|
}
|
|
2041
2840
|
function showGSCInstructions() {
|
|
2042
|
-
console.log((0,
|
|
2841
|
+
console.log((0, import_agent_runtime8.getGSCSetupInstructions)());
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/commands/alerts.ts
|
|
2845
|
+
var readline3 = __toESM(require("readline"));
|
|
2846
|
+
var ALERT_TYPES = [
|
|
2847
|
+
{ id: "error", label: "Critical errors detected", default: true },
|
|
2848
|
+
{ id: "score_drop", label: "Score drops (configurable threshold)", default: true },
|
|
2849
|
+
{ id: "weekly_report", label: "Weekly summary reports", default: false },
|
|
2850
|
+
{ id: "pr_created", label: "Fix PRs created", default: true }
|
|
2851
|
+
];
|
|
2852
|
+
async function ask(rl, question) {
|
|
2853
|
+
return new Promise((resolve) => {
|
|
2854
|
+
rl.question(question, (answer) => {
|
|
2855
|
+
resolve(answer.trim());
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
2860
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
2861
|
+
const answer = await ask(rl, `${question} ${suffix}: `);
|
|
2862
|
+
if (answer === "") return defaultYes;
|
|
2863
|
+
return answer.toLowerCase().startsWith("y");
|
|
2864
|
+
}
|
|
2865
|
+
async function alerts(options) {
|
|
2866
|
+
console.log("\n\u{1F514} SEO Autopilot - Alert Configuration\n");
|
|
2867
|
+
const rl = readline3.createInterface({
|
|
2868
|
+
input: process.stdin,
|
|
2869
|
+
output: process.stdout
|
|
2870
|
+
});
|
|
2871
|
+
try {
|
|
2872
|
+
if (options.list) {
|
|
2873
|
+
console.log("\u{1F4CB} Current alert configurations:\n");
|
|
2874
|
+
console.log(" (Alert configs are stored in your RankCLI account)");
|
|
2875
|
+
console.log(" Run `rankcli auth login` first, then manage alerts at https://rankcli.dev/account/alerts\n");
|
|
2876
|
+
rl.close();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (options.test && options.webhook) {
|
|
2880
|
+
await testWebhook(options.channel || "slack", options.webhook);
|
|
2881
|
+
rl.close();
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
console.log("Configure notifications for SEO audit events.\n");
|
|
2885
|
+
console.log("Supported channels:");
|
|
2886
|
+
console.log(" \u2022 Slack - Use an Incoming Webhook URL");
|
|
2887
|
+
console.log(" \u2022 Discord - Use a Channel Webhook URL\n");
|
|
2888
|
+
const channelChoice = await ask(rl, "Which channel? (slack/discord): ");
|
|
2889
|
+
const channel = channelChoice.toLowerCase();
|
|
2890
|
+
if (channel !== "slack" && channel !== "discord") {
|
|
2891
|
+
console.log('\n\u274C Invalid channel. Please choose "slack" or "discord".\n');
|
|
2892
|
+
rl.close();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
console.log("");
|
|
2896
|
+
if (channel === "slack") {
|
|
2897
|
+
console.log("To create a Slack webhook:");
|
|
2898
|
+
console.log("1. Go to https://api.slack.com/apps");
|
|
2899
|
+
console.log("2. Create an app (or use existing)");
|
|
2900
|
+
console.log('3. Enable "Incoming Webhooks"');
|
|
2901
|
+
console.log("4. Add a new webhook to your workspace");
|
|
2902
|
+
console.log("5. Copy the webhook URL\n");
|
|
2903
|
+
} else {
|
|
2904
|
+
console.log("To create a Discord webhook:");
|
|
2905
|
+
console.log("1. Open Discord channel settings");
|
|
2906
|
+
console.log("2. Go to Integrations \u2192 Webhooks");
|
|
2907
|
+
console.log("3. Create a new webhook");
|
|
2908
|
+
console.log("4. Copy the webhook URL\n");
|
|
2909
|
+
}
|
|
2910
|
+
const webhookUrl = await ask(rl, "Webhook URL: ");
|
|
2911
|
+
if (!webhookUrl.startsWith("https://")) {
|
|
2912
|
+
console.log("\n\u274C Invalid webhook URL. Must start with https://\n");
|
|
2913
|
+
rl.close();
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
console.log("\nWhich events should trigger alerts?\n");
|
|
2917
|
+
const selectedTypes = [];
|
|
2918
|
+
for (const alertType of ALERT_TYPES) {
|
|
2919
|
+
const enabled = await askYesNo(rl, ` ${alertType.label}`, alertType.default);
|
|
2920
|
+
if (enabled) {
|
|
2921
|
+
selectedTypes.push(alertType.id);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
let scoreDropThreshold = 10;
|
|
2925
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2926
|
+
const thresholdStr = await ask(rl, "\nMinimum score drop to alert (default: 10): ");
|
|
2927
|
+
if (thresholdStr) {
|
|
2928
|
+
const parsed = parseInt(thresholdStr, 10);
|
|
2929
|
+
if (!isNaN(parsed) && parsed > 0 && parsed <= 100) {
|
|
2930
|
+
scoreDropThreshold = parsed;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
console.log("\n\u{1F4E4} Testing webhook...");
|
|
2935
|
+
const testResult = await testWebhook(channel, webhookUrl);
|
|
2936
|
+
if (!testResult) {
|
|
2937
|
+
console.log("\u274C Webhook test failed. Please check the URL and try again.\n");
|
|
2938
|
+
rl.close();
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
console.log("\u2705 Webhook test successful!\n");
|
|
2942
|
+
console.log("\u{1F4DD} Alert configuration:");
|
|
2943
|
+
console.log(` Channel: ${channel}`);
|
|
2944
|
+
console.log(` Events: ${selectedTypes.join(", ")}`);
|
|
2945
|
+
if (selectedTypes.includes("score_drop")) {
|
|
2946
|
+
console.log(` Score drop threshold: ${scoreDropThreshold} points`);
|
|
2947
|
+
}
|
|
2948
|
+
console.log("");
|
|
2949
|
+
console.log("To save this configuration:");
|
|
2950
|
+
console.log("1. Log in with `rankcli auth login`");
|
|
2951
|
+
console.log("2. Add alert config at https://rankcli.dev/account/alerts");
|
|
2952
|
+
console.log("");
|
|
2953
|
+
console.log("Or use the API:");
|
|
2954
|
+
console.log(` POST /api/alerts with webhook_url and settings`);
|
|
2955
|
+
console.log("");
|
|
2956
|
+
rl.close();
|
|
2957
|
+
} catch (error) {
|
|
2958
|
+
console.error("Error:", error);
|
|
2959
|
+
rl.close();
|
|
2960
|
+
process.exit(1);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
async function testWebhook(channel, webhookUrl) {
|
|
2964
|
+
const testPayload = channel === "slack" ? {
|
|
2965
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2966
|
+
blocks: [
|
|
2967
|
+
{
|
|
2968
|
+
type: "header",
|
|
2969
|
+
text: {
|
|
2970
|
+
type: "plain_text",
|
|
2971
|
+
text: "\u{1F514} RankCLI Alert Test",
|
|
2972
|
+
emoji: true
|
|
2973
|
+
}
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
type: "section",
|
|
2977
|
+
text: {
|
|
2978
|
+
type: "mrkdwn",
|
|
2979
|
+
text: "This is a test message from RankCLI. If you see this, your webhook is working correctly!"
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
]
|
|
2983
|
+
} : {
|
|
2984
|
+
embeds: [
|
|
2985
|
+
{
|
|
2986
|
+
title: "\u{1F514} RankCLI Alert Test",
|
|
2987
|
+
description: "This is a test message from RankCLI. If you see this, your webhook is working correctly!",
|
|
2988
|
+
color: 2450411,
|
|
2989
|
+
footer: {
|
|
2990
|
+
text: "\u{1F916} Sent by RankCLI"
|
|
2991
|
+
},
|
|
2992
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2993
|
+
}
|
|
2994
|
+
]
|
|
2995
|
+
};
|
|
2996
|
+
try {
|
|
2997
|
+
const response = await fetch(webhookUrl, {
|
|
2998
|
+
method: "POST",
|
|
2999
|
+
headers: { "Content-Type": "application/json" },
|
|
3000
|
+
body: JSON.stringify(testPayload)
|
|
3001
|
+
});
|
|
3002
|
+
return response.ok;
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
console.error("Webhook test error:", error);
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
2043
3007
|
}
|
|
2044
3008
|
|
|
2045
3009
|
// src/commands/auth.ts
|
|
2046
|
-
var
|
|
2047
|
-
var
|
|
3010
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
3011
|
+
var import_ora5 = __toESM(require("ora"));
|
|
2048
3012
|
var import_inquirer = __toESM(require("inquirer"));
|
|
2049
3013
|
var import_open = __toESM(require("open"));
|
|
2050
3014
|
async function login(options) {
|
|
2051
3015
|
if (isLoggedIn()) {
|
|
2052
3016
|
const { email } = getUserInfo();
|
|
2053
|
-
console.log(
|
|
2054
|
-
console.log(
|
|
3017
|
+
console.log(import_chalk6.default.yellow(`Already logged in as ${email}`));
|
|
3018
|
+
console.log(import_chalk6.default.dim('Run "rankcli logout" to sign out first.'));
|
|
2055
3019
|
return;
|
|
2056
3020
|
}
|
|
2057
|
-
const spinner = (0,
|
|
3021
|
+
const spinner = (0, import_ora5.default)();
|
|
2058
3022
|
try {
|
|
2059
3023
|
if (options.token) {
|
|
2060
3024
|
const apiKey = options.token;
|
|
2061
3025
|
if (!apiKey.startsWith("rankcli_")) {
|
|
2062
|
-
console.log(
|
|
3026
|
+
console.log(import_chalk6.default.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
2063
3027
|
return;
|
|
2064
3028
|
}
|
|
2065
3029
|
spinner.start("Validating API key...");
|
|
2066
3030
|
const result = await validateApiKey(apiKey);
|
|
2067
3031
|
if (!result.valid) {
|
|
2068
3032
|
spinner.fail("Invalid API key");
|
|
2069
|
-
console.log(
|
|
3033
|
+
console.log(import_chalk6.default.red(`
|
|
2070
3034
|
${result.error}`));
|
|
2071
3035
|
return;
|
|
2072
3036
|
}
|
|
@@ -2079,14 +3043,14 @@ ${result.error}`));
|
|
|
2079
3043
|
});
|
|
2080
3044
|
}
|
|
2081
3045
|
spinner.succeed(`Authenticated via API key`);
|
|
2082
|
-
console.log(
|
|
3046
|
+
console.log(import_chalk6.default.dim(`
|
|
2083
3047
|
Email: ${result.user?.email}`));
|
|
2084
|
-
console.log(
|
|
3048
|
+
console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
2085
3049
|
return;
|
|
2086
3050
|
}
|
|
2087
3051
|
if (options.browser) {
|
|
2088
|
-
console.log(
|
|
2089
|
-
console.log(
|
|
3052
|
+
console.log(import_chalk6.default.cyan("\nOpening browser for login..."));
|
|
3053
|
+
console.log(import_chalk6.default.dim("Complete the login in your browser, then return here.\n"));
|
|
2090
3054
|
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
2091
3055
|
await (0, import_open.default)(loginUrl);
|
|
2092
3056
|
const { token } = await import_inquirer.default.prompt([
|
|
@@ -2102,7 +3066,7 @@ Email: ${result.user?.email}`));
|
|
|
2102
3066
|
const { data, error } = await supabase.auth.getUser(token);
|
|
2103
3067
|
if (error || !data.user) {
|
|
2104
3068
|
spinner.fail("Invalid token");
|
|
2105
|
-
console.log(
|
|
3069
|
+
console.log(import_chalk6.default.red("The token is invalid or expired. Please try again."));
|
|
2106
3070
|
return;
|
|
2107
3071
|
}
|
|
2108
3072
|
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
@@ -2116,7 +3080,7 @@ Email: ${result.user?.email}`));
|
|
|
2116
3080
|
}
|
|
2117
3081
|
saveSession(sessionData.session);
|
|
2118
3082
|
await fetchAndSaveSubscription();
|
|
2119
|
-
spinner.succeed(`Logged in as ${
|
|
3083
|
+
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
|
|
2120
3084
|
} else {
|
|
2121
3085
|
let email = options.email;
|
|
2122
3086
|
if (!email) {
|
|
@@ -2149,11 +3113,11 @@ Email: ${result.user?.email}`));
|
|
|
2149
3113
|
});
|
|
2150
3114
|
if (error) {
|
|
2151
3115
|
spinner.fail("Login failed");
|
|
2152
|
-
console.log(
|
|
3116
|
+
console.log(import_chalk6.default.red(`
|
|
2153
3117
|
${error.message}`));
|
|
2154
3118
|
if (error.message.includes("Invalid login")) {
|
|
2155
|
-
console.log(
|
|
2156
|
-
console.log(
|
|
3119
|
+
console.log(import_chalk6.default.dim("\nDon't have an account? Sign up at:"));
|
|
3120
|
+
console.log(import_chalk6.default.cyan(` ${WEB_APP_URL}/signup`));
|
|
2157
3121
|
}
|
|
2158
3122
|
return;
|
|
2159
3123
|
}
|
|
@@ -2163,41 +3127,41 @@ ${error.message}`));
|
|
|
2163
3127
|
}
|
|
2164
3128
|
saveSession(data.session);
|
|
2165
3129
|
await fetchAndSaveSubscription();
|
|
2166
|
-
spinner.succeed(`Logged in as ${
|
|
3130
|
+
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
|
|
2167
3131
|
const { planName, sitesLimit } = getUserInfo();
|
|
2168
|
-
console.log(
|
|
3132
|
+
console.log(import_chalk6.default.dim(`
|
|
2169
3133
|
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
2170
3134
|
}
|
|
2171
3135
|
} catch (err) {
|
|
2172
3136
|
spinner.fail("Login failed");
|
|
2173
|
-
console.error(
|
|
3137
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2174
3138
|
}
|
|
2175
3139
|
}
|
|
2176
3140
|
async function logout() {
|
|
2177
3141
|
if (!isLoggedIn()) {
|
|
2178
|
-
console.log(
|
|
3142
|
+
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
2179
3143
|
return;
|
|
2180
3144
|
}
|
|
2181
3145
|
const { email } = getUserInfo();
|
|
2182
|
-
const spinner = (0,
|
|
3146
|
+
const spinner = (0, import_ora5.default)("Signing out...").start();
|
|
2183
3147
|
try {
|
|
2184
3148
|
const supabase = getSupabaseClient();
|
|
2185
3149
|
await supabase.auth.signOut();
|
|
2186
3150
|
clearCredentials();
|
|
2187
|
-
spinner.succeed(`Logged out from ${
|
|
3151
|
+
spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
|
|
2188
3152
|
} catch (err) {
|
|
2189
3153
|
spinner.fail("Logout failed");
|
|
2190
|
-
console.error(
|
|
3154
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2191
3155
|
}
|
|
2192
3156
|
}
|
|
2193
3157
|
async function whoami() {
|
|
2194
3158
|
if (!isLoggedIn()) {
|
|
2195
|
-
console.log(
|
|
2196
|
-
console.log(
|
|
2197
|
-
console.log(
|
|
3159
|
+
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3160
|
+
console.log(import_chalk6.default.dim('\nRun "rankcli login" to sign in.'));
|
|
3161
|
+
console.log(import_chalk6.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
2198
3162
|
return;
|
|
2199
3163
|
}
|
|
2200
|
-
const spinner = (0,
|
|
3164
|
+
const spinner = (0, import_ora5.default)("Checking account...").start();
|
|
2201
3165
|
try {
|
|
2202
3166
|
if (isApiKeyAuth()) {
|
|
2203
3167
|
const apiKey = getApiKey();
|
|
@@ -2217,18 +3181,18 @@ async function whoami() {
|
|
|
2217
3181
|
const { email, planName, sitesLimit } = getUserInfo();
|
|
2218
3182
|
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
2219
3183
|
spinner.stop();
|
|
2220
|
-
console.log(
|
|
3184
|
+
console.log(import_chalk6.default.bold("\nRankCLI Account"));
|
|
2221
3185
|
console.log("\u2500".repeat(40));
|
|
2222
|
-
console.log(` Email: ${
|
|
2223
|
-
console.log(` Plan: ${
|
|
3186
|
+
console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
|
|
3187
|
+
console.log(` Plan: ${import_chalk6.default.green(planName)}`);
|
|
2224
3188
|
console.log(` Sites: ${sitesLimit} allowed`);
|
|
2225
|
-
console.log(` Auth: ${
|
|
3189
|
+
console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
|
|
2226
3190
|
console.log("\u2500".repeat(40));
|
|
2227
|
-
console.log(
|
|
3191
|
+
console.log(import_chalk6.default.dim(`
|
|
2228
3192
|
Manage at: ${WEB_APP_URL}/account`));
|
|
2229
3193
|
} catch (err) {
|
|
2230
3194
|
spinner.fail("Failed to fetch account info");
|
|
2231
|
-
console.error(
|
|
3195
|
+
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
2232
3196
|
}
|
|
2233
3197
|
}
|
|
2234
3198
|
async function fetchAndSaveSubscription() {
|
|
@@ -2260,7 +3224,7 @@ async function fetchAndSaveSubscription() {
|
|
|
2260
3224
|
}
|
|
2261
3225
|
|
|
2262
3226
|
// src/version.ts
|
|
2263
|
-
var VERSION = "0.0.
|
|
3227
|
+
var VERSION = "0.0.20";
|
|
2264
3228
|
|
|
2265
3229
|
// src/index.ts
|
|
2266
3230
|
var program = new import_commander.Command();
|
|
@@ -2272,6 +3236,20 @@ program.command("init").description("Initialize SEO Autopilot in your project").
|
|
|
2272
3236
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
2273
3237
|
program.command("audit").description("Run a comprehensive SEO audit (200+ checks, Ahrefs-level)").option("-u, --url <url>", "URL to audit").option("-o, --output <format>", "Output format (json, console)", "console").option("--check-links", "Check for broken internal/external links (slower)").option("--max-pages <n>", "Max pages to crawl (default: 5)").option("--ai", "Enable AI analysis").option("--ai-provider <provider>", "AI provider: openai or anthropic").option("--ai-key <key>", "API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)").action(audit);
|
|
2274
3238
|
program.command("apply").description("Analyze a live URL and apply SEO fixes to your codebase").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--dry-run", "Preview changes without applying").action(apply);
|
|
3239
|
+
program.command("fix").description("Agentic SEO fixer - detect, fix, commit, and push").option("-u, --url <url>", "URL to analyze").option("--auto", "Auto-apply fixes without confirmation").option("--commit", "Commit changes with conventional commit message").option("--push", "Push changes after committing (requires --commit)").option("--dry-run", "Preview changes without applying").option("--only <codes>", "Fix only specific issue codes (comma-separated)", (val) => val.split(",")).option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3240
|
+
const result = await fix({
|
|
3241
|
+
url: options.url,
|
|
3242
|
+
auto: options.auto,
|
|
3243
|
+
commit: options.commit,
|
|
3244
|
+
push: options.push,
|
|
3245
|
+
dryRun: options.dryRun,
|
|
3246
|
+
only: options.only,
|
|
3247
|
+
output: options.output
|
|
3248
|
+
});
|
|
3249
|
+
if (options.output === "json" && result.json) {
|
|
3250
|
+
console.log(result.json);
|
|
3251
|
+
}
|
|
3252
|
+
});
|
|
2275
3253
|
program.command("geo").description("GEO analysis - Check AI search visibility (ChatGPT, Perplexity, Claude)").option("-u, --url <url>", "URL to analyze").option("-o, --output <format>", "Output format (json, console)", "console").action(geo);
|
|
2276
3254
|
program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "Use GPT-4 for enhanced analysis with free tool ideas").option("--local", "Force local processing (skip cloud worker)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
|
|
2277
3255
|
if (options.ai && options.url) {
|
|
@@ -2294,8 +3272,42 @@ program.command("intent").description("Classify search intent for a keyword").ar
|
|
|
2294
3272
|
program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
|
|
2295
3273
|
console.log("Competitor analysis coming soon...");
|
|
2296
3274
|
});
|
|
2297
|
-
program.command("
|
|
2298
|
-
|
|
3275
|
+
var trackCmd = program.command("track").description("Keyword rank tracking");
|
|
3276
|
+
trackCmd.command("add <keywords...>").description("Add keywords to track").option("-u, --url <url>", "URL to track rankings for").option("-e, --engine <engine>", "Search engine (google, bing)", "google").option("-c, --country <country>", "Country code (US, UK, DE, etc.)", "US").option("-l, --language <lang>", "Language code (en, de, fr, etc.)", "en").action(async (keywords2, options) => {
|
|
3277
|
+
await trackAdd({
|
|
3278
|
+
keywords: keywords2,
|
|
3279
|
+
url: options.url,
|
|
3280
|
+
searchEngine: options.engine,
|
|
3281
|
+
country: options.country,
|
|
3282
|
+
language: options.language
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
trackCmd.command("status").description("Show current keyword rankings").option("-u, --url <url>", "URL to show rankings for").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3286
|
+
await trackStatus({
|
|
3287
|
+
url: options.url,
|
|
3288
|
+
output: options.output
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
trackCmd.command("history").description("Show keyword position history").option("-u, --url <url>", "URL to show history for").option("-k, --keyword <keyword>", "Keyword to show history for").option("-d, --days <days>", "Number of days of history", "30").option("-o, --output <format>", "Output format (text, json)", "text").action(async (options) => {
|
|
3292
|
+
await trackHistory({
|
|
3293
|
+
url: options.url,
|
|
3294
|
+
keyword: options.keyword,
|
|
3295
|
+
days: parseInt(options.days, 10),
|
|
3296
|
+
output: options.output
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
trackCmd.command("check").description("Check current rankings (calls SERP API)").option("-u, --url <url>", "URL to check rankings for").option("-f, --force", "Force check even if rate limited").action(async (options) => {
|
|
3300
|
+
await trackCheck({
|
|
3301
|
+
url: options.url,
|
|
3302
|
+
force: options.force
|
|
3303
|
+
});
|
|
3304
|
+
});
|
|
3305
|
+
trackCmd.command("export").description("Export tracking data").option("-u, --url <url>", "URL to export data for").option("-f, --format <format>", "Export format (csv, json)", "csv").option("-d, --days <days>", "Number of days of history", "30").action(async (options) => {
|
|
3306
|
+
await trackExport({
|
|
3307
|
+
url: options.url,
|
|
3308
|
+
format: options.format,
|
|
3309
|
+
days: parseInt(options.days, 10)
|
|
3310
|
+
});
|
|
2299
3311
|
});
|
|
2300
3312
|
program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
|
|
2301
3313
|
console.log("Report generation coming soon...");
|
|
@@ -2305,4 +3317,5 @@ program.command("watch").description("Continuous monitoring mode").action(() =>
|
|
|
2305
3317
|
});
|
|
2306
3318
|
program.command("setup").description("Set up SEO tracking (GA4, Search Console, GitHub Actions)").option("--ga4 <id>", "Google Analytics 4 Measurement ID (G-XXXXXXXXXX)").option("--gsc <code>", "Google Search Console verification code").option("--github-action", "Set up automated GitHub Action").option("--schedule <freq>", "GitHub Action schedule (daily, weekly, monthly)", "weekly").option("--all", "Interactive setup wizard").action(setup);
|
|
2307
3319
|
program.command("gsc-setup").description("Show Google Search Console API setup instructions").action(showGSCInstructions);
|
|
3320
|
+
program.command("alerts").description("Configure Slack/Discord notifications for SEO events").option("-c, --channel <channel>", "Alert channel (slack, discord)").option("-w, --webhook <url>", "Webhook URL").option("--test", "Test the webhook").option("--list", "List current alert configurations").option("--remove <id>", "Remove an alert configuration").action(alerts);
|
|
2308
3321
|
program.parse();
|