@rankcli/cli 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1189 -993
- package/dist/index.mjs +1147 -949
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -8,264 +8,17 @@ import {
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// src/commands/init.ts
|
|
11
|
-
import { existsSync, writeFileSync, readFileSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ pattern: "nuxt.config", name: "Nuxt" },
|
|
17
|
-
{ pattern: "svelte.config", name: "SvelteKit" },
|
|
18
|
-
{ pattern: "gatsby-config", name: "Gatsby" },
|
|
19
|
-
{ pattern: "vite.config", name: "Vite" },
|
|
20
|
-
{ pattern: "hugo.toml", name: "Hugo" },
|
|
21
|
-
{ pattern: "_config.yml", name: "Jekyll" },
|
|
22
|
-
{ pattern: "angular.json", name: "Angular" },
|
|
23
|
-
{ pattern: "vue.config", name: "Vue CLI" }
|
|
24
|
-
];
|
|
25
|
-
function detectFramework() {
|
|
26
|
-
const cwd = process.cwd();
|
|
27
|
-
for (const fw of FRAMEWORKS) {
|
|
28
|
-
const patterns = [
|
|
29
|
-
`${fw.pattern}.js`,
|
|
30
|
-
`${fw.pattern}.ts`,
|
|
31
|
-
`${fw.pattern}.mjs`,
|
|
32
|
-
`${fw.pattern}.cjs`,
|
|
33
|
-
fw.pattern
|
|
34
|
-
];
|
|
35
|
-
for (const p of patterns) {
|
|
36
|
-
if (existsSync(join(cwd, p))) {
|
|
37
|
-
return fw.name;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const pkgPath = join(cwd, "package.json");
|
|
42
|
-
if (existsSync(pkgPath)) {
|
|
43
|
-
try {
|
|
44
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
46
|
-
if (deps["next"]) return "Next.js";
|
|
47
|
-
if (deps["astro"]) return "Astro";
|
|
48
|
-
if (deps["nuxt"]) return "Nuxt";
|
|
49
|
-
if (deps["@sveltejs/kit"]) return "SvelteKit";
|
|
50
|
-
if (deps["gatsby"]) return "Gatsby";
|
|
51
|
-
if (deps["react"]) return "React";
|
|
52
|
-
if (deps["vue"]) return "Vue";
|
|
53
|
-
} catch {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
function getProjectName() {
|
|
59
|
-
const cwd = process.cwd();
|
|
60
|
-
const pkgPath = join(cwd, "package.json");
|
|
61
|
-
if (existsSync(pkgPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
64
|
-
if (pkg.name) return pkg.name;
|
|
65
|
-
} catch {
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return cwd.split("/").pop() || "unknown";
|
|
69
|
-
}
|
|
70
|
-
async function init(options) {
|
|
71
|
-
console.log("\n\u{1F680} Initializing SEO Autopilot...\n");
|
|
72
|
-
const configPath = join(process.cwd(), "seo-autopilot.config.json");
|
|
73
|
-
if (existsSync(configPath)) {
|
|
74
|
-
console.log("\u26A0\uFE0F Configuration file already exists: seo-autopilot.config.json");
|
|
75
|
-
console.log(' Run "seo analyze" to analyze your project.\n');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const framework = detectFramework();
|
|
79
|
-
const projectName = getProjectName();
|
|
80
|
-
console.log(`\u{1F4E6} Project: ${projectName}`);
|
|
81
|
-
console.log(`\u{1F527} Framework: ${framework || "Unknown (static site?)"}`);
|
|
82
|
-
const config2 = {
|
|
83
|
-
name: projectName,
|
|
84
|
-
framework,
|
|
85
|
-
url: null,
|
|
86
|
-
initialized: (/* @__PURE__ */ new Date()).toISOString()
|
|
87
|
-
};
|
|
88
|
-
writeFileSync(configPath, JSON.stringify(config2, null, 2));
|
|
89
|
-
console.log("\n\u2705 Created seo-autopilot.config.json");
|
|
90
|
-
console.log("\n\u{1F4CB} Next steps:");
|
|
91
|
-
console.log(' 1. Run "seo analyze" to analyze your project');
|
|
92
|
-
console.log(' 2. Run "seo audit --url <your-site>" to run a technical audit');
|
|
93
|
-
console.log(" 3. Connect Google Search Console at https://seo-autopilot.dev/connect\n");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/commands/analyze.ts
|
|
97
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
98
|
-
import { join as join2, extname } from "path";
|
|
99
|
-
var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
|
|
100
|
-
function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
|
|
101
|
-
if (depth > maxDepth) return [];
|
|
102
|
-
const files = [];
|
|
103
|
-
try {
|
|
104
|
-
const items = readdirSync(dir);
|
|
105
|
-
for (const item of items) {
|
|
106
|
-
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const fullPath = join2(dir, item);
|
|
110
|
-
const stat = statSync(fullPath);
|
|
111
|
-
if (stat.isDirectory()) {
|
|
112
|
-
files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
|
|
113
|
-
} else if (extensions.includes(extname(item))) {
|
|
114
|
-
files.push(fullPath);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
}
|
|
119
|
-
return files;
|
|
120
|
-
}
|
|
121
|
-
function extractKeywords(text) {
|
|
122
|
-
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
123
|
-
const frequency = {};
|
|
124
|
-
for (const word of words) {
|
|
125
|
-
frequency[word] = (frequency[word] || 0) + 1;
|
|
126
|
-
}
|
|
127
|
-
const stopWords = /* @__PURE__ */ new Set([
|
|
128
|
-
"this",
|
|
129
|
-
"that",
|
|
130
|
-
"with",
|
|
131
|
-
"from",
|
|
132
|
-
"have",
|
|
133
|
-
"been",
|
|
134
|
-
"were",
|
|
135
|
-
"will",
|
|
136
|
-
"would",
|
|
137
|
-
"could",
|
|
138
|
-
"should",
|
|
139
|
-
"their",
|
|
140
|
-
"there",
|
|
141
|
-
"about",
|
|
142
|
-
"which",
|
|
143
|
-
"when",
|
|
144
|
-
"what",
|
|
145
|
-
"your",
|
|
146
|
-
"more",
|
|
147
|
-
"some",
|
|
148
|
-
"just",
|
|
149
|
-
"into",
|
|
150
|
-
"over",
|
|
151
|
-
"such",
|
|
152
|
-
"only",
|
|
153
|
-
"than",
|
|
154
|
-
"them",
|
|
155
|
-
"then",
|
|
156
|
-
"these",
|
|
157
|
-
"most",
|
|
158
|
-
"other",
|
|
159
|
-
"also",
|
|
160
|
-
"being",
|
|
161
|
-
"after",
|
|
162
|
-
"const",
|
|
163
|
-
"function",
|
|
164
|
-
"return",
|
|
165
|
-
"import",
|
|
166
|
-
"export",
|
|
167
|
-
"default",
|
|
168
|
-
"true",
|
|
169
|
-
"false"
|
|
170
|
-
]);
|
|
171
|
-
return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
|
|
172
|
-
}
|
|
173
|
-
function loadConfig() {
|
|
174
|
-
const configPath = join2(process.cwd(), "seo-autopilot.config.json");
|
|
175
|
-
if (existsSync2(configPath)) {
|
|
176
|
-
try {
|
|
177
|
-
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
178
|
-
} catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
async function analyze(options) {
|
|
185
|
-
console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
|
|
186
|
-
const cwd = process.cwd();
|
|
187
|
-
const config2 = loadConfig();
|
|
188
|
-
const result = {
|
|
189
|
-
projectName: config2?.name || cwd.split("/").pop() || "unknown",
|
|
190
|
-
framework: config2?.framework || null,
|
|
191
|
-
hasReadme: false,
|
|
192
|
-
readmeSummary: null,
|
|
193
|
-
pageCount: 0,
|
|
194
|
-
hasMetaTags: false,
|
|
195
|
-
hasSitemap: false,
|
|
196
|
-
hasRobotsTxt: false,
|
|
197
|
-
keywords: [],
|
|
198
|
-
suggestions: []
|
|
199
|
-
};
|
|
200
|
-
const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
|
|
201
|
-
for (const rp of readmePaths) {
|
|
202
|
-
if (existsSync2(join2(cwd, rp))) {
|
|
203
|
-
result.hasReadme = true;
|
|
204
|
-
const content = readFileSync2(join2(cwd, rp), "utf-8");
|
|
205
|
-
result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
|
|
206
|
-
result.keywords.push(...extractKeywords(content));
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
result.hasSitemap = existsSync2(join2(cwd, "public", "sitemap.xml")) || existsSync2(join2(cwd, "sitemap.xml"));
|
|
211
|
-
result.hasRobotsTxt = existsSync2(join2(cwd, "public", "robots.txt")) || existsSync2(join2(cwd, "robots.txt"));
|
|
212
|
-
const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
|
|
213
|
-
result.pageCount = contentFiles.length;
|
|
214
|
-
const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
|
|
215
|
-
for (const hf of htmlFiles.slice(0, 5)) {
|
|
216
|
-
const content = readFileSync2(hf, "utf-8");
|
|
217
|
-
if (content.includes("<meta") || content.includes("metadata")) {
|
|
218
|
-
result.hasMetaTags = true;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
for (const cf of contentFiles.slice(0, 10)) {
|
|
223
|
-
const content = readFileSync2(cf, "utf-8");
|
|
224
|
-
result.keywords.push(...extractKeywords(content));
|
|
225
|
-
}
|
|
226
|
-
result.keywords = [...new Set(result.keywords)].slice(0, 20);
|
|
227
|
-
if (!result.hasReadme) {
|
|
228
|
-
result.suggestions.push("Add a README.md with clear project description");
|
|
229
|
-
}
|
|
230
|
-
if (!result.hasSitemap) {
|
|
231
|
-
result.suggestions.push("Generate a sitemap.xml for better crawlability");
|
|
232
|
-
}
|
|
233
|
-
if (!result.hasRobotsTxt) {
|
|
234
|
-
result.suggestions.push("Add robots.txt to guide search engine crawlers");
|
|
235
|
-
}
|
|
236
|
-
if (!result.hasMetaTags) {
|
|
237
|
-
result.suggestions.push("Add meta tags (title, description, og:*) to pages");
|
|
238
|
-
}
|
|
239
|
-
if (result.pageCount < 5) {
|
|
240
|
-
result.suggestions.push("Consider adding more content pages for SEO coverage");
|
|
241
|
-
}
|
|
242
|
-
console.log("\u{1F4CA} Analysis Results\n");
|
|
243
|
-
console.log(`Project: ${result.projectName}`);
|
|
244
|
-
console.log(`Framework: ${result.framework || "Unknown"}`);
|
|
245
|
-
console.log(`Content files: ${result.pageCount}`);
|
|
246
|
-
console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
|
|
247
|
-
console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
|
|
248
|
-
console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
|
|
249
|
-
console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
|
|
250
|
-
if (result.keywords.length > 0) {
|
|
251
|
-
console.log("\n\u{1F511} Detected Keywords:");
|
|
252
|
-
console.log(` ${result.keywords.join(", ")}`);
|
|
253
|
-
}
|
|
254
|
-
if (result.suggestions.length > 0) {
|
|
255
|
-
console.log("\n\u{1F4A1} Suggestions:");
|
|
256
|
-
for (const s of result.suggestions) {
|
|
257
|
-
console.log(` \u2022 ${s}`);
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
console.log("\n\u2705 No critical SEO issues found!");
|
|
261
|
-
}
|
|
262
|
-
console.log('\n\u{1F4C8} Next: Run "seo audit --url <your-deployed-site>" for a full technical audit\n');
|
|
263
|
-
}
|
|
11
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
|
|
12
|
+
import { join as join2 } from "path";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import chalk2 from "chalk";
|
|
15
|
+
import inquirer2 from "inquirer";
|
|
264
16
|
|
|
265
|
-
// src/commands/
|
|
266
|
-
import
|
|
17
|
+
// src/commands/auth.ts
|
|
18
|
+
import chalk from "chalk";
|
|
267
19
|
import ora from "ora";
|
|
268
|
-
import
|
|
20
|
+
import inquirer from "inquirer";
|
|
21
|
+
import open from "open";
|
|
269
22
|
|
|
270
23
|
// src/lib/config.ts
|
|
271
24
|
import Conf from "conf";
|
|
@@ -354,11 +107,14 @@ function isApiKeyAuth() {
|
|
|
354
107
|
function getApiKey() {
|
|
355
108
|
return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
|
|
356
109
|
}
|
|
357
|
-
function saveApiKey(apiKey, keyName) {
|
|
110
|
+
function saveApiKey(apiKey, keyName, email) {
|
|
358
111
|
config.set("apiKey", apiKey);
|
|
359
112
|
if (keyName) {
|
|
360
113
|
config.set("apiKeyName", keyName);
|
|
361
114
|
}
|
|
115
|
+
if (email) {
|
|
116
|
+
config.set("email", email);
|
|
117
|
+
}
|
|
362
118
|
}
|
|
363
119
|
async function validateApiKey(apiKey) {
|
|
364
120
|
try {
|
|
@@ -464,34 +220,832 @@ async function getAuthenticatedClient() {
|
|
|
464
220
|
return supabase;
|
|
465
221
|
}
|
|
466
222
|
|
|
467
|
-
// src/
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
223
|
+
// src/commands/auth.ts
|
|
224
|
+
async function browserLogin(spinner) {
|
|
225
|
+
spinner.start("Starting login...");
|
|
226
|
+
let start;
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
229
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
230
|
+
start = await res.json();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
spinner.fail("Could not start login");
|
|
233
|
+
console.log(chalk.red(err instanceof Error ? err.message : "Network error"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
spinner.text = "Opening browser to approve...";
|
|
237
|
+
try {
|
|
238
|
+
await open(start.url);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
console.log(chalk.dim(`
|
|
242
|
+
If a browser didn't open, visit:
|
|
243
|
+
${start.url}
|
|
244
|
+
`));
|
|
245
|
+
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
246
|
+
spinner.text = "Waiting for approval in your browser...";
|
|
247
|
+
while (Date.now() < deadline) {
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
249
|
+
let status;
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
body: JSON.stringify({ connectId: start.connectId })
|
|
255
|
+
});
|
|
256
|
+
status = await res.json();
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (status.status === "claimed" && status.apiKey) {
|
|
261
|
+
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
262
|
+
await fetchAndSaveSubscription();
|
|
263
|
+
spinner.succeed(`Logged in as ${chalk.green(status.email)}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (status.status === "expired" || status.status === "not_found") {
|
|
267
|
+
spinner.fail("Login link expired");
|
|
268
|
+
console.log(chalk.dim('Run "rankcli login" again.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
spinner.fail("Timed out waiting for approval");
|
|
273
|
+
console.log(chalk.dim(`Run "rankcli login" again when you're ready.`));
|
|
274
|
+
}
|
|
275
|
+
async function login(options) {
|
|
276
|
+
if (isLoggedIn()) {
|
|
277
|
+
const { email } = getUserInfo();
|
|
278
|
+
console.log(chalk.yellow(`Already logged in as ${email}`));
|
|
279
|
+
console.log(chalk.dim('Run "rankcli logout" to sign out first.'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const spinner = ora();
|
|
283
|
+
try {
|
|
284
|
+
if (options.token) {
|
|
285
|
+
const apiKey = options.token;
|
|
286
|
+
if (!apiKey.startsWith("rankcli_")) {
|
|
287
|
+
console.log(chalk.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
spinner.start("Validating API key...");
|
|
291
|
+
const result = await validateApiKey(apiKey);
|
|
292
|
+
if (!result.valid) {
|
|
293
|
+
spinner.fail("Invalid API key");
|
|
294
|
+
console.log(chalk.red(`
|
|
295
|
+
${result.error}`));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
saveApiKey(apiKey);
|
|
299
|
+
if (result.subscription) {
|
|
300
|
+
saveSubscription({
|
|
301
|
+
planId: result.subscription.planId,
|
|
302
|
+
planName: result.subscription.planName,
|
|
303
|
+
sitesLimit: result.subscription.sitesLimit
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
spinner.succeed(`Authenticated via API key`);
|
|
307
|
+
console.log(chalk.dim(`
|
|
308
|
+
Email: ${result.user?.email}`));
|
|
309
|
+
console.log(chalk.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (options.browser !== false && !options.email) {
|
|
313
|
+
await browserLogin(spinner);
|
|
314
|
+
} else {
|
|
315
|
+
let email = options.email;
|
|
316
|
+
if (!email) {
|
|
317
|
+
const answers = await inquirer.prompt([
|
|
318
|
+
{
|
|
319
|
+
type: "input",
|
|
320
|
+
name: "email",
|
|
321
|
+
message: "Email:",
|
|
322
|
+
validate: (input) => {
|
|
323
|
+
if (!input.includes("@")) return "Please enter a valid email";
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]);
|
|
328
|
+
email = answers.email;
|
|
329
|
+
}
|
|
330
|
+
const { password } = await inquirer.prompt([
|
|
331
|
+
{
|
|
332
|
+
type: "password",
|
|
333
|
+
name: "password",
|
|
334
|
+
message: "Password:",
|
|
335
|
+
mask: "*"
|
|
336
|
+
}
|
|
337
|
+
]);
|
|
338
|
+
spinner.start("Signing in...");
|
|
339
|
+
const supabase = getSupabaseClient();
|
|
340
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
341
|
+
email,
|
|
342
|
+
password
|
|
343
|
+
});
|
|
344
|
+
if (error) {
|
|
345
|
+
spinner.fail("Login failed");
|
|
346
|
+
console.log(chalk.red(`
|
|
347
|
+
${error.message}`));
|
|
348
|
+
if (error.message.includes("Invalid login")) {
|
|
349
|
+
console.log(chalk.dim("\nDon't have an account? Sign up at:"));
|
|
350
|
+
console.log(chalk.cyan(` ${WEB_APP_URL}/signup`));
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!data.session) {
|
|
355
|
+
spinner.fail("No session returned");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
saveSession(data.session);
|
|
359
|
+
await fetchAndSaveSubscription();
|
|
360
|
+
spinner.succeed(`Logged in as ${chalk.green(data.user?.email)}`);
|
|
361
|
+
const { planName, sitesLimit } = getUserInfo();
|
|
362
|
+
console.log(chalk.dim(`
|
|
363
|
+
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
spinner.fail("Login failed");
|
|
367
|
+
console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function logout() {
|
|
371
|
+
if (!isLoggedIn()) {
|
|
372
|
+
console.log(chalk.yellow("Not logged in"));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const { email } = getUserInfo();
|
|
376
|
+
const spinner = ora("Signing out...").start();
|
|
377
|
+
try {
|
|
378
|
+
const supabase = getSupabaseClient();
|
|
379
|
+
await supabase.auth.signOut();
|
|
380
|
+
clearCredentials();
|
|
381
|
+
spinner.succeed(`Logged out from ${chalk.dim(email)}`);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
spinner.fail("Logout failed");
|
|
384
|
+
console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function whoami() {
|
|
388
|
+
if (!isLoggedIn()) {
|
|
389
|
+
console.log(chalk.yellow("Not logged in"));
|
|
390
|
+
console.log(chalk.dim('\nRun "rankcli login" to sign in.'));
|
|
391
|
+
console.log(chalk.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const spinner = ora("Checking account...").start();
|
|
395
|
+
try {
|
|
396
|
+
if (isApiKeyAuth()) {
|
|
397
|
+
const apiKey = getApiKey();
|
|
398
|
+
if (apiKey) {
|
|
399
|
+
const result = await validateApiKey(apiKey);
|
|
400
|
+
if (result.valid && result.subscription) {
|
|
401
|
+
saveSubscription({
|
|
402
|
+
planId: result.subscription.planId,
|
|
403
|
+
planName: result.subscription.planName,
|
|
404
|
+
sitesLimit: result.subscription.sitesLimit
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
await fetchAndSaveSubscription();
|
|
410
|
+
}
|
|
411
|
+
const { email, planName, sitesLimit } = getUserInfo();
|
|
412
|
+
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
413
|
+
spinner.stop();
|
|
414
|
+
console.log(chalk.bold("\nRankCLI Account"));
|
|
415
|
+
console.log("\u2500".repeat(40));
|
|
416
|
+
console.log(` Email: ${chalk.cyan(email || "N/A")}`);
|
|
417
|
+
console.log(` Plan: ${chalk.green(planName)}`);
|
|
418
|
+
console.log(` Sites: ${sitesLimit} allowed`);
|
|
419
|
+
console.log(` Auth: ${chalk.dim(authMethod)}`);
|
|
420
|
+
console.log("\u2500".repeat(40));
|
|
421
|
+
console.log(chalk.dim(`
|
|
422
|
+
Manage at: ${WEB_APP_URL}/account`));
|
|
423
|
+
} catch (err) {
|
|
424
|
+
spinner.fail("Failed to fetch account info");
|
|
425
|
+
console.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function fetchAndSaveSubscription() {
|
|
429
|
+
const supabase = await getAuthenticatedClient();
|
|
430
|
+
if (!supabase) return;
|
|
431
|
+
const { userId } = getUserInfo();
|
|
432
|
+
if (!userId) return;
|
|
433
|
+
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
434
|
+
plan:subscription_plans (
|
|
435
|
+
plan_id,
|
|
436
|
+
name,
|
|
437
|
+
sites_limit
|
|
438
|
+
)
|
|
439
|
+
`).eq("user_id", userId).single();
|
|
440
|
+
if (subscription?.plan) {
|
|
441
|
+
const plan = subscription.plan;
|
|
442
|
+
saveSubscription({
|
|
443
|
+
planId: plan.plan_id,
|
|
444
|
+
planName: plan.name,
|
|
445
|
+
sitesLimit: plan.sites_limit
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
saveSubscription({
|
|
449
|
+
planId: void 0,
|
|
450
|
+
planName: "Free",
|
|
451
|
+
sitesLimit: 1
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/commands/setup.ts
|
|
457
|
+
import * as readline from "readline";
|
|
458
|
+
import * as fs from "fs";
|
|
459
|
+
import * as path from "path";
|
|
460
|
+
import {
|
|
461
|
+
injectGA4,
|
|
462
|
+
injectGSCVerification,
|
|
463
|
+
writeGitHubActionFiles,
|
|
464
|
+
getGSCSetupInstructions,
|
|
465
|
+
generateGA4EnvTemplate
|
|
466
|
+
} from "@rankcli/agent-runtime";
|
|
467
|
+
async function setup(options) {
|
|
468
|
+
console.log("\n\u{1F527} RankCLI - Setup\n");
|
|
469
|
+
const projectPath = process.cwd();
|
|
470
|
+
if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
|
|
471
|
+
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
472
|
+
}
|
|
473
|
+
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
474
|
+
await interactiveSetup(projectPath);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (options.ga4) {
|
|
478
|
+
await setupGA4(projectPath, options.ga4);
|
|
479
|
+
}
|
|
480
|
+
if (options.gsc) {
|
|
481
|
+
await setupGSC(projectPath, options.gsc);
|
|
482
|
+
}
|
|
483
|
+
if (options.githubAction) {
|
|
484
|
+
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function interactiveSetup(projectPath) {
|
|
488
|
+
const rl = readline.createInterface({
|
|
489
|
+
input: process.stdin,
|
|
490
|
+
output: process.stdout
|
|
491
|
+
});
|
|
492
|
+
const ask2 = (question) => {
|
|
493
|
+
return new Promise((resolve) => {
|
|
494
|
+
rl.question(question, (answer) => {
|
|
495
|
+
resolve(answer.trim());
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
try {
|
|
500
|
+
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
501
|
+
console.log("\u2500".repeat(60));
|
|
502
|
+
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
503
|
+
console.log("GA4 tracks visitor behavior on your site.");
|
|
504
|
+
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
505
|
+
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
506
|
+
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
507
|
+
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
508
|
+
await setupGA4(projectPath, ga4Id);
|
|
509
|
+
} else if (ga4Id) {
|
|
510
|
+
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
511
|
+
} else {
|
|
512
|
+
console.log("Skipping GA4 setup.\n");
|
|
513
|
+
}
|
|
514
|
+
console.log("\u2500".repeat(60));
|
|
515
|
+
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
516
|
+
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
517
|
+
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
518
|
+
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
519
|
+
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
520
|
+
if (gscCode && gscCode.length > 10) {
|
|
521
|
+
await setupGSC(projectPath, gscCode);
|
|
522
|
+
} else if (gscCode) {
|
|
523
|
+
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
524
|
+
} else {
|
|
525
|
+
console.log("Skipping GSC verification.\n");
|
|
526
|
+
}
|
|
527
|
+
console.log("\u2500".repeat(60));
|
|
528
|
+
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
529
|
+
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
530
|
+
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
531
|
+
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
532
|
+
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
533
|
+
console.log("\nHow often should the check run?");
|
|
534
|
+
console.log(" 1. Weekly (recommended for most sites)");
|
|
535
|
+
console.log(" 2. Daily (for high-traffic sites)");
|
|
536
|
+
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
537
|
+
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
538
|
+
const scheduleMap = {
|
|
539
|
+
"1": "weekly",
|
|
540
|
+
"2": "daily",
|
|
541
|
+
"3": "monthly"
|
|
542
|
+
};
|
|
543
|
+
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
544
|
+
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
545
|
+
if (siteUrl) {
|
|
546
|
+
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
547
|
+
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
548
|
+
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
console.log("Skipping GitHub Action setup.\n");
|
|
552
|
+
}
|
|
553
|
+
console.log("\u2500".repeat(60));
|
|
554
|
+
console.log("\u2705 Setup complete!\n");
|
|
555
|
+
console.log("Next steps:");
|
|
556
|
+
console.log(" 1. Commit the changes to your repository");
|
|
557
|
+
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
558
|
+
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
559
|
+
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
560
|
+
rl.close();
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.log(`
|
|
563
|
+
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
564
|
+
`);
|
|
565
|
+
rl.close();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function setupGA4(projectPath, measurementId) {
|
|
569
|
+
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
570
|
+
const result = await injectGA4(projectPath, { measurementId });
|
|
571
|
+
if (result.success) {
|
|
572
|
+
console.log(`\u2705 ${result.message}`);
|
|
573
|
+
if (result.file) {
|
|
574
|
+
console.log(` Modified: ${result.file}`);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
578
|
+
if (result.code) {
|
|
579
|
+
console.log("\nManually add this code to your HTML <head>:\n");
|
|
580
|
+
console.log(result.code);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const envExamplePath = path.join(projectPath, ".env.example");
|
|
584
|
+
const envContent = generateGA4EnvTemplate();
|
|
585
|
+
if (fs.existsSync(envExamplePath)) {
|
|
586
|
+
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
587
|
+
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
588
|
+
fs.appendFileSync(envExamplePath, "\n" + envContent);
|
|
589
|
+
console.log(" Updated: .env.example");
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
fs.writeFileSync(envExamplePath, envContent);
|
|
593
|
+
console.log(" Created: .env.example");
|
|
594
|
+
}
|
|
595
|
+
console.log("");
|
|
596
|
+
}
|
|
597
|
+
async function setupGSC(projectPath, verificationCode) {
|
|
598
|
+
console.log("Setting up Google Search Console verification...");
|
|
599
|
+
const result = await injectGSCVerification(projectPath, verificationCode);
|
|
600
|
+
if (result.success) {
|
|
601
|
+
console.log(`\u2705 ${result.message}`);
|
|
602
|
+
if (result.file) {
|
|
603
|
+
console.log(` Modified: ${result.file}`);
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
607
|
+
}
|
|
608
|
+
console.log("");
|
|
609
|
+
}
|
|
610
|
+
async function setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard) {
|
|
611
|
+
console.log(`Setting up GitHub Action (${schedule} schedule)...`);
|
|
612
|
+
const config2 = {
|
|
613
|
+
schedule,
|
|
614
|
+
siteUrl: siteUrl || "https://example.com",
|
|
615
|
+
features: {
|
|
616
|
+
audit: true,
|
|
617
|
+
autoFix: true,
|
|
618
|
+
createIssues: true,
|
|
619
|
+
createPRs: true
|
|
620
|
+
},
|
|
621
|
+
syncToDashboard
|
|
622
|
+
};
|
|
623
|
+
const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
|
|
624
|
+
console.log("\u2705 GitHub Action files created:");
|
|
625
|
+
for (const file of files) {
|
|
626
|
+
console.log(` - ${path.relative(projectPath, file)}`);
|
|
627
|
+
}
|
|
628
|
+
console.log(instructions);
|
|
629
|
+
}
|
|
630
|
+
function showGSCInstructions() {
|
|
631
|
+
console.log(getGSCSetupInstructions());
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/commands/init.ts
|
|
635
|
+
var FRAMEWORKS = [
|
|
636
|
+
{ pattern: "next.config", name: "Next.js" },
|
|
637
|
+
{ pattern: "astro.config", name: "Astro" },
|
|
638
|
+
{ pattern: "nuxt.config", name: "Nuxt" },
|
|
639
|
+
{ pattern: "svelte.config", name: "SvelteKit" },
|
|
640
|
+
{ pattern: "gatsby-config", name: "Gatsby" },
|
|
641
|
+
{ pattern: "vite.config", name: "Vite" },
|
|
642
|
+
{ pattern: "hugo.toml", name: "Hugo" },
|
|
643
|
+
{ pattern: "_config.yml", name: "Jekyll" },
|
|
644
|
+
{ pattern: "angular.json", name: "Angular" },
|
|
645
|
+
{ pattern: "vue.config", name: "Vue CLI" }
|
|
646
|
+
];
|
|
647
|
+
var AI_KEY_ENV_VARS = [
|
|
648
|
+
{ env: "OPENAI_API_KEY", label: "OpenAI" },
|
|
649
|
+
{ env: "ANTHROPIC_API_KEY", label: "Anthropic" },
|
|
650
|
+
{ env: "OPENROUTER_API_KEY", label: "OpenRouter" },
|
|
651
|
+
{ env: "GROQ_API_KEY", label: "Groq" },
|
|
652
|
+
{ env: "GEMINI_API_KEY", label: "Gemini" },
|
|
653
|
+
{ env: "GOOGLE_API_KEY", label: "Gemini" }
|
|
654
|
+
];
|
|
655
|
+
function detectFramework() {
|
|
656
|
+
const cwd = process.cwd();
|
|
657
|
+
for (const fw of FRAMEWORKS) {
|
|
658
|
+
const patterns = [
|
|
659
|
+
`${fw.pattern}.js`,
|
|
660
|
+
`${fw.pattern}.ts`,
|
|
661
|
+
`${fw.pattern}.mjs`,
|
|
662
|
+
`${fw.pattern}.cjs`,
|
|
663
|
+
fw.pattern
|
|
664
|
+
];
|
|
665
|
+
for (const p of patterns) {
|
|
666
|
+
if (existsSync2(join2(cwd, p))) {
|
|
667
|
+
return fw.name;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const pkgPath = join2(cwd, "package.json");
|
|
672
|
+
if (existsSync2(pkgPath)) {
|
|
673
|
+
try {
|
|
674
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
675
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
676
|
+
if (deps["next"]) return "Next.js";
|
|
677
|
+
if (deps["astro"]) return "Astro";
|
|
678
|
+
if (deps["nuxt"]) return "Nuxt";
|
|
679
|
+
if (deps["@sveltejs/kit"]) return "SvelteKit";
|
|
680
|
+
if (deps["gatsby"]) return "Gatsby";
|
|
681
|
+
if (deps["react"]) return "React";
|
|
682
|
+
if (deps["vue"]) return "Vue";
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
function getProjectName() {
|
|
689
|
+
const cwd = process.cwd();
|
|
690
|
+
const pkgPath = join2(cwd, "package.json");
|
|
691
|
+
if (existsSync2(pkgPath)) {
|
|
692
|
+
try {
|
|
693
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
694
|
+
if (pkg.name) return pkg.name;
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return cwd.split("/").pop() || "unknown";
|
|
699
|
+
}
|
|
700
|
+
function runGit(args) {
|
|
701
|
+
try {
|
|
702
|
+
return execSync(`git ${args}`, { cwd: process.cwd(), stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function parseGitHubRepo(remoteUrl) {
|
|
708
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
|
|
709
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
710
|
+
}
|
|
711
|
+
function detectConnectedAiKeys() {
|
|
712
|
+
const found = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const { env, label } of AI_KEY_ENV_VARS) {
|
|
714
|
+
if (process.env[env]) found.add(label);
|
|
715
|
+
}
|
|
716
|
+
return Array.from(found);
|
|
717
|
+
}
|
|
718
|
+
async function init(options) {
|
|
719
|
+
console.log(chalk2.bold("\n\u{1F680} Initializing RankCLI...\n"));
|
|
720
|
+
const configPath = join2(process.cwd(), "rankcli.config.json");
|
|
721
|
+
if (existsSync2(configPath)) {
|
|
722
|
+
console.log(chalk2.yellow("\u26A0\uFE0F Configuration file already exists: rankcli.config.json"));
|
|
723
|
+
console.log(chalk2.dim(' Run "rankcli audit" to analyze your project.\n'));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const framework = detectFramework();
|
|
727
|
+
const projectName = getProjectName();
|
|
728
|
+
const isGitRepo = runGit("rev-parse --is-inside-work-tree") === "true";
|
|
729
|
+
const remoteUrl = isGitRepo ? runGit("remote get-url origin") : null;
|
|
730
|
+
const githubRepo = remoteUrl ? parseGitHubRepo(remoteUrl) : null;
|
|
731
|
+
const connectedAiKeys = detectConnectedAiKeys();
|
|
732
|
+
console.log(`\u{1F4E6} Project: ${chalk2.cyan(projectName)}`);
|
|
733
|
+
console.log(`\u{1F527} Framework: ${chalk2.cyan(framework || "Unknown (static site?)")}`);
|
|
734
|
+
if (githubRepo) {
|
|
735
|
+
console.log(`\u{1F419} GitHub repo: ${chalk2.cyan(githubRepo)}`);
|
|
736
|
+
} else if (isGitRepo) {
|
|
737
|
+
console.log(chalk2.dim("\u{1F419} Git repo detected, but no GitHub remote found"));
|
|
738
|
+
}
|
|
739
|
+
if (connectedAiKeys.length > 0) {
|
|
740
|
+
console.log(`\u{1F511} AI key detected: ${chalk2.cyan(connectedAiKeys.join(", "))} (will be used automatically for --ai audits)`);
|
|
741
|
+
}
|
|
742
|
+
const config2 = {
|
|
743
|
+
name: projectName,
|
|
744
|
+
framework,
|
|
745
|
+
url: null,
|
|
746
|
+
githubRepo,
|
|
747
|
+
initialized: (/* @__PURE__ */ new Date()).toISOString()
|
|
748
|
+
};
|
|
749
|
+
writeFileSync2(configPath, JSON.stringify(config2, null, 2));
|
|
750
|
+
console.log(chalk2.green("\n\u2705 Created rankcli.config.json"));
|
|
751
|
+
if (options.yes) {
|
|
752
|
+
printNextSteps(githubRepo);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (!isLoggedIn()) {
|
|
756
|
+
console.log("");
|
|
757
|
+
const { shouldConnect } = await inquirer2.prompt([
|
|
758
|
+
{
|
|
759
|
+
type: "confirm",
|
|
760
|
+
name: "shouldConnect",
|
|
761
|
+
message: "Connect a RankCLI account? Unlocks a dashboard, audit history, and GitHub auto-fix PRs (opens your browser).",
|
|
762
|
+
default: true
|
|
763
|
+
}
|
|
764
|
+
]);
|
|
765
|
+
if (shouldConnect) {
|
|
766
|
+
await login({});
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
const { email } = getUserInfo();
|
|
770
|
+
console.log(chalk2.dim(`
|
|
771
|
+
Already connected as ${email}.`));
|
|
772
|
+
}
|
|
773
|
+
const workflowPath = join2(process.cwd(), ".github", "workflows", "seo.yml");
|
|
774
|
+
if (githubRepo && !existsSync2(workflowPath)) {
|
|
775
|
+
console.log("");
|
|
776
|
+
console.log(chalk2.bold("\u{1F916} Automate SEO/GEO checks in CI"));
|
|
777
|
+
console.log(chalk2.dim(" Privacy: by default nothing is sent to RankCLI - the audit runs"));
|
|
778
|
+
console.log(chalk2.dim(" entirely inside your own GitHub Actions runner. If you opt into"));
|
|
779
|
+
console.log(chalk2.dim(" dashboard sync below, audit results (the URL and issues found,"));
|
|
780
|
+
console.log(chalk2.dim(" not your source code) are sent to your RankCLI account.\n"));
|
|
781
|
+
const { shouldSetupAction } = await inquirer2.prompt([
|
|
782
|
+
{
|
|
783
|
+
type: "confirm",
|
|
784
|
+
name: "shouldSetupAction",
|
|
785
|
+
message: `Set up a GitHub Action for ${githubRepo}?`,
|
|
786
|
+
default: true
|
|
787
|
+
}
|
|
788
|
+
]);
|
|
789
|
+
if (shouldSetupAction) {
|
|
790
|
+
const { siteUrl } = await inquirer2.prompt([
|
|
791
|
+
{
|
|
792
|
+
type: "input",
|
|
793
|
+
name: "siteUrl",
|
|
794
|
+
message: "Site URL to audit (https://example.com):",
|
|
795
|
+
validate: (input) => {
|
|
796
|
+
try {
|
|
797
|
+
new URL(input);
|
|
798
|
+
return true;
|
|
799
|
+
} catch {
|
|
800
|
+
return "Please enter a valid URL";
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
]);
|
|
805
|
+
const { schedule } = await inquirer2.prompt([
|
|
806
|
+
{
|
|
807
|
+
type: "list",
|
|
808
|
+
name: "schedule",
|
|
809
|
+
message: "How often should it run?",
|
|
810
|
+
choices: [
|
|
811
|
+
{ name: "Weekly (recommended)", value: "weekly" },
|
|
812
|
+
{ name: "Daily", value: "daily" },
|
|
813
|
+
{ name: "Monthly", value: "monthly" }
|
|
814
|
+
],
|
|
815
|
+
default: "weekly"
|
|
816
|
+
}
|
|
817
|
+
]);
|
|
818
|
+
let syncToDashboard = false;
|
|
819
|
+
if (isLoggedIn()) {
|
|
820
|
+
const answer = await inquirer2.prompt([
|
|
821
|
+
{
|
|
822
|
+
type: "confirm",
|
|
823
|
+
name: "syncToDashboard",
|
|
824
|
+
message: "Also send scheduled audits to your RankCLI dashboard?",
|
|
825
|
+
default: true
|
|
826
|
+
}
|
|
827
|
+
]);
|
|
828
|
+
syncToDashboard = answer.syncToDashboard;
|
|
829
|
+
}
|
|
830
|
+
await setupGitHubAction(process.cwd(), schedule, siteUrl, syncToDashboard);
|
|
831
|
+
config2.url = siteUrl;
|
|
832
|
+
writeFileSync2(configPath, JSON.stringify(config2, null, 2));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
printNextSteps(githubRepo);
|
|
836
|
+
}
|
|
837
|
+
function printNextSteps(githubRepo) {
|
|
838
|
+
console.log(chalk2.bold("\n\u{1F4CB} Next steps:"));
|
|
839
|
+
console.log(' 1. Run "rankcli audit --url <your-site>" for a full technical audit');
|
|
840
|
+
console.log(' 2. Run "rankcli apply --auto" to apply fixes locally');
|
|
841
|
+
if (!githubRepo) {
|
|
842
|
+
console.log(' 3. Push to GitHub, then re-run "rankcli init" to set up automated checks');
|
|
843
|
+
}
|
|
844
|
+
console.log("");
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/commands/analyze.ts
|
|
848
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
849
|
+
import { join as join3, extname } from "path";
|
|
850
|
+
var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
|
|
851
|
+
function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
|
|
852
|
+
if (depth > maxDepth) return [];
|
|
853
|
+
const files = [];
|
|
854
|
+
try {
|
|
855
|
+
const items = readdirSync(dir);
|
|
856
|
+
for (const item of items) {
|
|
857
|
+
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const fullPath = join3(dir, item);
|
|
861
|
+
const stat = statSync(fullPath);
|
|
862
|
+
if (stat.isDirectory()) {
|
|
863
|
+
files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
|
|
864
|
+
} else if (extensions.includes(extname(item))) {
|
|
865
|
+
files.push(fullPath);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
return files;
|
|
871
|
+
}
|
|
872
|
+
function extractKeywords(text) {
|
|
873
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
874
|
+
const frequency = {};
|
|
875
|
+
for (const word of words) {
|
|
876
|
+
frequency[word] = (frequency[word] || 0) + 1;
|
|
877
|
+
}
|
|
878
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
879
|
+
"this",
|
|
880
|
+
"that",
|
|
881
|
+
"with",
|
|
882
|
+
"from",
|
|
883
|
+
"have",
|
|
884
|
+
"been",
|
|
885
|
+
"were",
|
|
886
|
+
"will",
|
|
887
|
+
"would",
|
|
888
|
+
"could",
|
|
889
|
+
"should",
|
|
890
|
+
"their",
|
|
891
|
+
"there",
|
|
892
|
+
"about",
|
|
893
|
+
"which",
|
|
894
|
+
"when",
|
|
895
|
+
"what",
|
|
896
|
+
"your",
|
|
897
|
+
"more",
|
|
898
|
+
"some",
|
|
899
|
+
"just",
|
|
900
|
+
"into",
|
|
901
|
+
"over",
|
|
902
|
+
"such",
|
|
903
|
+
"only",
|
|
904
|
+
"than",
|
|
905
|
+
"them",
|
|
906
|
+
"then",
|
|
907
|
+
"these",
|
|
908
|
+
"most",
|
|
909
|
+
"other",
|
|
910
|
+
"also",
|
|
911
|
+
"being",
|
|
912
|
+
"after",
|
|
913
|
+
"const",
|
|
914
|
+
"function",
|
|
915
|
+
"return",
|
|
916
|
+
"import",
|
|
917
|
+
"export",
|
|
918
|
+
"default",
|
|
919
|
+
"true",
|
|
920
|
+
"false"
|
|
921
|
+
]);
|
|
922
|
+
return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
|
|
923
|
+
}
|
|
924
|
+
function loadConfig() {
|
|
925
|
+
const configPath = join3(process.cwd(), "rankcli.config.json");
|
|
926
|
+
if (existsSync3(configPath)) {
|
|
927
|
+
try {
|
|
928
|
+
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
929
|
+
} catch {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
async function analyze(options) {
|
|
936
|
+
console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
|
|
937
|
+
const cwd = process.cwd();
|
|
938
|
+
const config2 = loadConfig();
|
|
939
|
+
const result = {
|
|
940
|
+
projectName: config2?.name || cwd.split("/").pop() || "unknown",
|
|
941
|
+
framework: config2?.framework || null,
|
|
942
|
+
hasReadme: false,
|
|
943
|
+
readmeSummary: null,
|
|
944
|
+
pageCount: 0,
|
|
945
|
+
hasMetaTags: false,
|
|
946
|
+
hasSitemap: false,
|
|
947
|
+
hasRobotsTxt: false,
|
|
948
|
+
keywords: [],
|
|
949
|
+
suggestions: []
|
|
950
|
+
};
|
|
951
|
+
const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
|
|
952
|
+
for (const rp of readmePaths) {
|
|
953
|
+
if (existsSync3(join3(cwd, rp))) {
|
|
954
|
+
result.hasReadme = true;
|
|
955
|
+
const content = readFileSync3(join3(cwd, rp), "utf-8");
|
|
956
|
+
result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
|
|
957
|
+
result.keywords.push(...extractKeywords(content));
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
result.hasSitemap = existsSync3(join3(cwd, "public", "sitemap.xml")) || existsSync3(join3(cwd, "sitemap.xml"));
|
|
962
|
+
result.hasRobotsTxt = existsSync3(join3(cwd, "public", "robots.txt")) || existsSync3(join3(cwd, "robots.txt"));
|
|
963
|
+
const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
|
|
964
|
+
result.pageCount = contentFiles.length;
|
|
965
|
+
const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
|
|
966
|
+
for (const hf of htmlFiles.slice(0, 5)) {
|
|
967
|
+
const content = readFileSync3(hf, "utf-8");
|
|
968
|
+
if (content.includes("<meta") || content.includes("metadata")) {
|
|
969
|
+
result.hasMetaTags = true;
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
for (const cf of contentFiles.slice(0, 10)) {
|
|
974
|
+
const content = readFileSync3(cf, "utf-8");
|
|
975
|
+
result.keywords.push(...extractKeywords(content));
|
|
976
|
+
}
|
|
977
|
+
result.keywords = [...new Set(result.keywords)].slice(0, 20);
|
|
978
|
+
if (!result.hasReadme) {
|
|
979
|
+
result.suggestions.push("Add a README.md with clear project description");
|
|
980
|
+
}
|
|
981
|
+
if (!result.hasSitemap) {
|
|
982
|
+
result.suggestions.push("Generate a sitemap.xml for better crawlability");
|
|
983
|
+
}
|
|
984
|
+
if (!result.hasRobotsTxt) {
|
|
985
|
+
result.suggestions.push("Add robots.txt to guide search engine crawlers");
|
|
986
|
+
}
|
|
987
|
+
if (!result.hasMetaTags) {
|
|
988
|
+
result.suggestions.push("Add meta tags (title, description, og:*) to pages");
|
|
989
|
+
}
|
|
990
|
+
if (result.pageCount < 5) {
|
|
991
|
+
result.suggestions.push("Consider adding more content pages for SEO coverage");
|
|
992
|
+
}
|
|
993
|
+
console.log("\u{1F4CA} Analysis Results\n");
|
|
994
|
+
console.log(`Project: ${result.projectName}`);
|
|
995
|
+
console.log(`Framework: ${result.framework || "Unknown"}`);
|
|
996
|
+
console.log(`Content files: ${result.pageCount}`);
|
|
997
|
+
console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
|
|
998
|
+
console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
|
|
999
|
+
console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
|
|
1000
|
+
console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
|
|
1001
|
+
if (result.keywords.length > 0) {
|
|
1002
|
+
console.log("\n\u{1F511} Detected Keywords:");
|
|
1003
|
+
console.log(` ${result.keywords.join(", ")}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (result.suggestions.length > 0) {
|
|
1006
|
+
console.log("\n\u{1F4A1} Suggestions:");
|
|
1007
|
+
for (const s of result.suggestions) {
|
|
1008
|
+
console.log(` \u2022 ${s}`);
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log("\n\u2705 No critical SEO issues found!");
|
|
1012
|
+
}
|
|
1013
|
+
console.log('\n\u{1F4C8} Next: Run "rankcli audit --url <your-deployed-site>" for a full technical audit\n');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/commands/audit.ts
|
|
1017
|
+
import { runFullAudit, discoverPagesFromLinks, frameworks } from "@rankcli/agent-runtime";
|
|
1018
|
+
import ora2 from "ora";
|
|
1019
|
+
import chalk4 from "chalk";
|
|
1020
|
+
|
|
1021
|
+
// src/lib/upsell.ts
|
|
1022
|
+
import chalk3 from "chalk";
|
|
1023
|
+
var WEB_APP_URL2 = "https://rankcli.dev";
|
|
1024
|
+
var TIER_LIMITS = {
|
|
1025
|
+
free_anon: {
|
|
1026
|
+
checks: 50,
|
|
1027
|
+
sites: 0,
|
|
1028
|
+
domains_per_month: 1,
|
|
1029
|
+
features: { geo: false, auto_fix: false, github: false, ai: false }
|
|
1030
|
+
},
|
|
1031
|
+
free: {
|
|
1032
|
+
checks: 100,
|
|
1033
|
+
sites: 1,
|
|
1034
|
+
domains_per_month: 2,
|
|
1035
|
+
features: { geo: "basic", auto_fix: false, github: false, ai: false }
|
|
1036
|
+
},
|
|
1037
|
+
solo: {
|
|
1038
|
+
checks: 280,
|
|
1039
|
+
sites: 3,
|
|
1040
|
+
domains_per_month: 5,
|
|
1041
|
+
features: { geo: true, auto_fix: true, github: false, ai: true }
|
|
1042
|
+
},
|
|
1043
|
+
pro: {
|
|
1044
|
+
checks: 280,
|
|
1045
|
+
sites: 10,
|
|
1046
|
+
domains_per_month: 15,
|
|
1047
|
+
features: { geo: true, auto_fix: true, github: true, ai: true }
|
|
1048
|
+
},
|
|
495
1049
|
agency: {
|
|
496
1050
|
checks: 280,
|
|
497
1051
|
sites: 50,
|
|
@@ -504,133 +1058,133 @@ var UPSELL_MESSAGES = {
|
|
|
504
1058
|
// After free audit completes
|
|
505
1059
|
free_audit_complete: () => [
|
|
506
1060
|
"",
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1061
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1062
|
+
chalk3.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
|
|
1063
|
+
chalk3.dim(" Create a free account to unlock 100 checks and save your results."),
|
|
510
1064
|
"",
|
|
511
|
-
|
|
512
|
-
|
|
1065
|
+
chalk3.cyan(` \u2192 rankcli login`),
|
|
1066
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
513
1067
|
""
|
|
514
1068
|
],
|
|
515
1069
|
// When checks limit is reached
|
|
516
1070
|
checks_limit: (ctx) => [
|
|
517
1071
|
"",
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1072
|
+
chalk3.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
|
|
1073
|
+
chalk3.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
|
|
1074
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
|
|
521
1075
|
""
|
|
522
1076
|
],
|
|
523
1077
|
// When domain limit is reached
|
|
524
1078
|
domain_limit: (ctx) => [
|
|
525
1079
|
"",
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1080
|
+
chalk3.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"),
|
|
1081
|
+
chalk3.red.bold(" \u2717 Domain limit reached"),
|
|
1082
|
+
chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
|
|
529
1083
|
"",
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1084
|
+
chalk3.dim(" Upgrade to audit more sites:"),
|
|
1085
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
|
|
1086
|
+
chalk3.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"),
|
|
533
1087
|
""
|
|
534
1088
|
],
|
|
535
1089
|
// When GEO features are limited
|
|
536
1090
|
geo_limited: (ctx) => [
|
|
537
1091
|
"",
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1092
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1093
|
+
chalk3.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
|
|
1094
|
+
chalk3.dim(" You're seeing basic AI crawler access checks."),
|
|
1095
|
+
chalk3.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
|
|
542
1096
|
"",
|
|
543
|
-
|
|
544
|
-
|
|
1097
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
|
|
1098
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
545
1099
|
""
|
|
546
1100
|
],
|
|
547
1101
|
// When auto-fix is attempted without access
|
|
548
1102
|
auto_fix_blocked: () => [
|
|
549
1103
|
"",
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1104
|
+
chalk3.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"),
|
|
1105
|
+
chalk3.red.bold(" \u2717 Auto-fix requires Solo plan"),
|
|
1106
|
+
chalk3.dim(" Apply framework-specific SEO fixes directly to your codebase."),
|
|
553
1107
|
"",
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1108
|
+
chalk3.dim(" Solo ($9/mo) includes:"),
|
|
1109
|
+
chalk3.dim(" \u2022 Local auto-fix via CLI"),
|
|
1110
|
+
chalk3.dim(" \u2022 25+ framework support"),
|
|
1111
|
+
chalk3.dim(" \u2022 Full 280+ checks"),
|
|
558
1112
|
"",
|
|
559
|
-
|
|
560
|
-
|
|
1113
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
|
|
1114
|
+
chalk3.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"),
|
|
561
1115
|
""
|
|
562
1116
|
],
|
|
563
1117
|
// When GitHub integration is attempted without access
|
|
564
1118
|
github_blocked: () => [
|
|
565
1119
|
"",
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1120
|
+
chalk3.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"),
|
|
1121
|
+
chalk3.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
|
|
1122
|
+
chalk3.dim(" Automatically create fix PRs when SEO issues are detected."),
|
|
569
1123
|
"",
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1124
|
+
chalk3.dim(" Pro ($29/mo) includes:"),
|
|
1125
|
+
chalk3.dim(" \u2022 GitHub App integration"),
|
|
1126
|
+
chalk3.dim(" \u2022 Auto-fix PRs"),
|
|
1127
|
+
chalk3.dim(" \u2022 Daily scheduled audits"),
|
|
1128
|
+
chalk3.dim(" \u2022 Slack/Discord alerts"),
|
|
575
1129
|
"",
|
|
576
|
-
|
|
577
|
-
|
|
1130
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
|
|
1131
|
+
chalk3.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"),
|
|
578
1132
|
""
|
|
579
1133
|
],
|
|
580
1134
|
// Keyword tracking limit reached
|
|
581
1135
|
keyword_limit: (ctx) => [
|
|
582
1136
|
"",
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1137
|
+
chalk3.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"),
|
|
1138
|
+
chalk3.red.bold(" \u2717 Keyword tracking limit reached"),
|
|
1139
|
+
chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
|
|
1140
|
+
chalk3.dim(` Currently tracking: ${ctx.current}`),
|
|
587
1141
|
"",
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1142
|
+
chalk3.dim(" Upgrade for more keywords:"),
|
|
1143
|
+
chalk3.dim(" \u2022 Solo ($9/mo): 100 keywords"),
|
|
1144
|
+
chalk3.dim(" \u2022 Pro ($29/mo): 500 keywords"),
|
|
1145
|
+
chalk3.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
|
|
592
1146
|
"",
|
|
593
|
-
|
|
594
|
-
|
|
1147
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
|
|
1148
|
+
chalk3.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"),
|
|
595
1149
|
""
|
|
596
1150
|
],
|
|
597
1151
|
// Rate limit exceeded
|
|
598
1152
|
rate_limit: (ctx) => [
|
|
599
1153
|
"",
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1154
|
+
chalk3.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"),
|
|
1155
|
+
chalk3.red.bold(" \u2717 API rate limit exceeded"),
|
|
1156
|
+
chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
|
|
603
1157
|
"",
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1158
|
+
chalk3.dim(" Need more? Upgrade for higher limits:"),
|
|
1159
|
+
chalk3.dim(" \u2022 Solo: 100 req/day"),
|
|
1160
|
+
chalk3.dim(" \u2022 Pro: 1,000 req/day"),
|
|
1161
|
+
chalk3.dim(" \u2022 Agency: 10,000 req/day"),
|
|
608
1162
|
"",
|
|
609
|
-
|
|
610
|
-
|
|
1163
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
|
|
1164
|
+
chalk3.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"),
|
|
611
1165
|
""
|
|
612
1166
|
],
|
|
613
1167
|
// Schedule exceeded (for GitHub Action)
|
|
614
1168
|
schedule_exceeded: (ctx) => [
|
|
615
1169
|
"",
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1170
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1171
|
+
chalk3.yellow.bold(" \u23F0 Scheduled audit skipped"),
|
|
1172
|
+
chalk3.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
|
|
619
1173
|
"",
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1174
|
+
chalk3.dim(" Upgrade for more frequent audits:"),
|
|
1175
|
+
chalk3.dim(" \u2022 Solo: Weekly"),
|
|
1176
|
+
chalk3.dim(" \u2022 Pro: Daily"),
|
|
1177
|
+
chalk3.dim(" \u2022 Agency: Hourly"),
|
|
624
1178
|
"",
|
|
625
|
-
|
|
626
|
-
|
|
1179
|
+
chalk3.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
|
|
1180
|
+
chalk3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
627
1181
|
""
|
|
628
1182
|
],
|
|
629
1183
|
// Login prompt (soft)
|
|
630
1184
|
login_prompt: () => [
|
|
631
1185
|
"",
|
|
632
|
-
|
|
633
|
-
|
|
1186
|
+
chalk3.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
|
|
1187
|
+
chalk3.dim(" rankcli login"),
|
|
634
1188
|
""
|
|
635
1189
|
]
|
|
636
1190
|
};
|
|
@@ -671,9 +1225,16 @@ async function fetchHtml(url, timeout = 1e4) {
|
|
|
671
1225
|
return null;
|
|
672
1226
|
}
|
|
673
1227
|
}
|
|
1228
|
+
var AI_PROVIDER_DEFAULTS = {
|
|
1229
|
+
openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4o-mini" },
|
|
1230
|
+
openrouter: { baseUrl: "https://openrouter.ai/api/v1", model: "deepseek/deepseek-r1:free" },
|
|
1231
|
+
groq: { baseUrl: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile" },
|
|
1232
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", model: "gemini-2.0-flash" }
|
|
1233
|
+
};
|
|
674
1234
|
async function runAIAnalysis(url, issues, provider, apiKey) {
|
|
675
1235
|
const isAnthropic = provider === "anthropic";
|
|
676
|
-
const
|
|
1236
|
+
const providerConfig = AI_PROVIDER_DEFAULTS[provider] || AI_PROVIDER_DEFAULTS.openai;
|
|
1237
|
+
const apiUrl = isAnthropic ? "https://api.anthropic.com/v1/messages" : `${providerConfig.baseUrl}/chat/completions`;
|
|
677
1238
|
const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
|
|
678
1239
|
|
|
679
1240
|
URL: ${url}
|
|
@@ -704,19 +1265,26 @@ Return JSON with:
|
|
|
704
1265
|
});
|
|
705
1266
|
} else {
|
|
706
1267
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1268
|
+
if (provider === "openrouter") {
|
|
1269
|
+
headers["HTTP-Referer"] = "https://rankcli.dev";
|
|
1270
|
+
headers["X-Title"] = "RankCLI";
|
|
1271
|
+
}
|
|
707
1272
|
body = JSON.stringify({
|
|
708
|
-
model:
|
|
1273
|
+
model: providerConfig.model,
|
|
709
1274
|
messages: [
|
|
710
1275
|
{ role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
|
|
711
1276
|
{ role: "user", content: prompt }
|
|
712
1277
|
],
|
|
713
1278
|
max_tokens: 1500,
|
|
714
|
-
|
|
1279
|
+
// json_object mode is an OpenAI-specific extension - Groq supports
|
|
1280
|
+
// it too, but not every OpenAI-compatible server does, so only
|
|
1281
|
+
// request it for providers confirmed to support it.
|
|
1282
|
+
...provider === "openai" || provider === "groq" ? { response_format: { type: "json_object" } } : {}
|
|
715
1283
|
});
|
|
716
1284
|
}
|
|
717
1285
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
718
1286
|
if (!response.ok) {
|
|
719
|
-
console.error(
|
|
1287
|
+
console.error(chalk4.yellow(`AI analysis failed: ${response.status}`));
|
|
720
1288
|
return null;
|
|
721
1289
|
}
|
|
722
1290
|
const data = await response.json();
|
|
@@ -726,7 +1294,7 @@ Return JSON with:
|
|
|
726
1294
|
if (!jsonMatch) return null;
|
|
727
1295
|
return JSON.parse(jsonMatch[0]);
|
|
728
1296
|
} catch (error) {
|
|
729
|
-
console.error(
|
|
1297
|
+
console.error(chalk4.yellow(`AI analysis error: ${error}`));
|
|
730
1298
|
return null;
|
|
731
1299
|
}
|
|
732
1300
|
}
|
|
@@ -761,9 +1329,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
761
1329
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
762
1330
|
const limit = sitesLimit ?? 1;
|
|
763
1331
|
if (count !== null && count >= limit) {
|
|
764
|
-
console.log(
|
|
1332
|
+
console.log(chalk4.yellow(`
|
|
765
1333
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
766
|
-
console.log(
|
|
1334
|
+
console.log(chalk4.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
767
1335
|
return null;
|
|
768
1336
|
}
|
|
769
1337
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -773,7 +1341,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
773
1341
|
domain
|
|
774
1342
|
}).select("id").single();
|
|
775
1343
|
if (projectError || !newProject) {
|
|
776
|
-
console.error(
|
|
1344
|
+
console.error(chalk4.dim("Failed to create project"));
|
|
777
1345
|
return null;
|
|
778
1346
|
}
|
|
779
1347
|
projectId = newProject.id;
|
|
@@ -814,7 +1382,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
814
1382
|
ai_analysis: aiAnalysis
|
|
815
1383
|
}).select("id").single();
|
|
816
1384
|
if (auditError || !audit2) {
|
|
817
|
-
console.error(
|
|
1385
|
+
console.error(chalk4.dim("Failed to save audit"));
|
|
818
1386
|
return null;
|
|
819
1387
|
}
|
|
820
1388
|
if (mergedIssues.length > 0) {
|
|
@@ -842,7 +1410,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
842
1410
|
}).eq("id", projectId);
|
|
843
1411
|
return { projectId, auditId: audit2.id };
|
|
844
1412
|
} catch (error) {
|
|
845
|
-
console.error(
|
|
1413
|
+
console.error(chalk4.dim("Cloud sync failed"));
|
|
846
1414
|
return null;
|
|
847
1415
|
}
|
|
848
1416
|
}
|
|
@@ -850,8 +1418,16 @@ async function audit(options) {
|
|
|
850
1418
|
const url = options.url;
|
|
851
1419
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
852
1420
|
const enableAI = options.ai || false;
|
|
853
|
-
const
|
|
854
|
-
const
|
|
1421
|
+
const detectedProvider = options.aiProvider || (process.env.OPENROUTER_API_KEY ? "openrouter" : process.env.GROQ_API_KEY ? "groq" : process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY ? "gemini" : process.env.ANTHROPIC_API_KEY ? "anthropic" : "openai");
|
|
1422
|
+
const aiProvider = detectedProvider;
|
|
1423
|
+
const providerEnvKey = {
|
|
1424
|
+
openai: process.env.OPENAI_API_KEY,
|
|
1425
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
1426
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
1427
|
+
groq: process.env.GROQ_API_KEY,
|
|
1428
|
+
gemini: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
|
1429
|
+
};
|
|
1430
|
+
const aiKey = options.aiKey || providerEnvKey[aiProvider] || "";
|
|
855
1431
|
const loggedIn = isLoggedIn();
|
|
856
1432
|
const { planId } = getUserInfo();
|
|
857
1433
|
const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
|
|
@@ -864,7 +1440,7 @@ async function audit(options) {
|
|
|
864
1440
|
await initEngine(passphrase);
|
|
865
1441
|
premiumChecks = await getPremiumChecks();
|
|
866
1442
|
} catch (error) {
|
|
867
|
-
console.error(
|
|
1443
|
+
console.error(chalk4.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
|
|
868
1444
|
}
|
|
869
1445
|
}
|
|
870
1446
|
}
|
|
@@ -874,12 +1450,12 @@ async function audit(options) {
|
|
|
874
1450
|
console.log("\nOptions:");
|
|
875
1451
|
console.log(" --max-pages <n> Max pages to crawl (default: 5)");
|
|
876
1452
|
console.log(" --ai Enable AI analysis");
|
|
877
|
-
console.log(" --ai-provider <p> AI provider: openai or
|
|
878
|
-
console.log(" --ai-key <key> API key (or set OPENAI_API_KEY
|
|
1453
|
+
console.log(" --ai-provider <p> AI provider: openai, anthropic, openrouter, groq, or gemini (auto-detected from env if omitted)");
|
|
1454
|
+
console.log(" --ai-key <key> API key (or set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY)");
|
|
879
1455
|
console.log(" --output json Output as JSON");
|
|
880
1456
|
if (!loggedIn) {
|
|
881
|
-
console.log(
|
|
882
|
-
console.log(
|
|
1457
|
+
console.log(chalk4.dim("\nTip: Log in to sync results to the web dashboard:"));
|
|
1458
|
+
console.log(chalk4.dim(" rankcli login"));
|
|
883
1459
|
}
|
|
884
1460
|
console.log("");
|
|
885
1461
|
return;
|
|
@@ -892,7 +1468,7 @@ async function audit(options) {
|
|
|
892
1468
|
}
|
|
893
1469
|
const startTime = Date.now();
|
|
894
1470
|
try {
|
|
895
|
-
const discoverySpinner =
|
|
1471
|
+
const discoverySpinner = ora2("Discovering pages...").start();
|
|
896
1472
|
let pagesToAudit = [url];
|
|
897
1473
|
const homepageHtml = await fetchHtml(url, 1e4);
|
|
898
1474
|
let detectedFramework = detectFramework2({});
|
|
@@ -906,7 +1482,7 @@ async function audit(options) {
|
|
|
906
1482
|
} else {
|
|
907
1483
|
discoverySpinner.warn("Could not fetch homepage, auditing URL only");
|
|
908
1484
|
}
|
|
909
|
-
const auditSpinner =
|
|
1485
|
+
const auditSpinner = ora2(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
|
|
910
1486
|
const auditPage = async (pageUrl, index) => {
|
|
911
1487
|
try {
|
|
912
1488
|
const report = await runFullAudit({
|
|
@@ -953,7 +1529,7 @@ async function audit(options) {
|
|
|
953
1529
|
const mergedIssues = mergeIssues(allIssues);
|
|
954
1530
|
let aiAnalysis = null;
|
|
955
1531
|
if (enableAI && aiKey) {
|
|
956
|
-
const aiSpinner =
|
|
1532
|
+
const aiSpinner = ora2(`Running AI analysis (${aiProvider})...`).start();
|
|
957
1533
|
aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
|
|
958
1534
|
if (aiAnalysis) {
|
|
959
1535
|
aiSpinner.succeed("AI analysis complete");
|
|
@@ -961,8 +1537,8 @@ async function audit(options) {
|
|
|
961
1537
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
962
1538
|
}
|
|
963
1539
|
} else if (enableAI && !aiKey) {
|
|
964
|
-
console.log(
|
|
965
|
-
console.log(
|
|
1540
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
|
|
1541
|
+
console.log(chalk4.dim(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY, or use --ai-key"));
|
|
966
1542
|
}
|
|
967
1543
|
const totalTime = Date.now() - startTime;
|
|
968
1544
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
@@ -990,27 +1566,27 @@ async function audit(options) {
|
|
|
990
1566
|
};
|
|
991
1567
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
992
1568
|
} else {
|
|
993
|
-
console.log("\n" +
|
|
994
|
-
console.log(
|
|
995
|
-
console.log(
|
|
1569
|
+
console.log("\n" + chalk4.bold("\u2550".repeat(60)));
|
|
1570
|
+
console.log(chalk4.bold.cyan(` SEO AUDIT REPORT: ${url}`));
|
|
1571
|
+
console.log(chalk4.bold("\u2550".repeat(60)));
|
|
996
1572
|
const score = primaryReport.healthScore?.overall || 0;
|
|
997
|
-
const scoreColor = score >= 80 ?
|
|
1573
|
+
const scoreColor = score >= 80 ? chalk4.green : score >= 60 ? chalk4.yellow : chalk4.red;
|
|
998
1574
|
console.log(`
|
|
999
|
-
${
|
|
1000
|
-
console.log(` ${
|
|
1001
|
-
console.log(` ${
|
|
1002
|
-
console.log(` ${
|
|
1575
|
+
${chalk4.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
|
|
1576
|
+
console.log(` ${chalk4.bold("Pages audited:")} ${successCount}`);
|
|
1577
|
+
console.log(` ${chalk4.bold("Issues found:")} ${mergedIssues.length}`);
|
|
1578
|
+
console.log(` ${chalk4.bold("Time:")} ${totalTime}ms`);
|
|
1003
1579
|
if (frameworkName) {
|
|
1004
|
-
console.log(` ${
|
|
1580
|
+
console.log(` ${chalk4.bold("Framework:")} ${chalk4.cyan(frameworkName)}`);
|
|
1005
1581
|
}
|
|
1006
1582
|
if (primaryReport.healthScore) {
|
|
1007
|
-
console.log("\n" +
|
|
1583
|
+
console.log("\n" + chalk4.bold(" Health Scores:"));
|
|
1008
1584
|
const hs = primaryReport.healthScore;
|
|
1009
1585
|
const bar = (score2) => {
|
|
1010
1586
|
const filled = Math.round(score2 / 5);
|
|
1011
1587
|
const empty = 20 - filled;
|
|
1012
|
-
const color = score2 >= 80 ?
|
|
1013
|
-
return color("\u2588".repeat(filled)) +
|
|
1588
|
+
const color = score2 >= 80 ? chalk4.green : score2 >= 60 ? chalk4.yellow : chalk4.red;
|
|
1589
|
+
return color("\u2588".repeat(filled)) + chalk4.gray("\u2591".repeat(empty));
|
|
1014
1590
|
};
|
|
1015
1591
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
1016
1592
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
@@ -1023,35 +1599,35 @@ async function audit(options) {
|
|
|
1023
1599
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
1024
1600
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
1025
1601
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
1026
|
-
console.log("\n" +
|
|
1027
|
-
console.log(` ${
|
|
1028
|
-
console.log(` ${
|
|
1029
|
-
console.log(` ${
|
|
1030
|
-
console.log("\n" +
|
|
1602
|
+
console.log("\n" + chalk4.bold(" Issues by Severity:"));
|
|
1603
|
+
console.log(` ${chalk4.red("\u25CF")} Errors: ${errors.length}`);
|
|
1604
|
+
console.log(` ${chalk4.yellow("\u25CF")} Warnings: ${warnings.length}`);
|
|
1605
|
+
console.log(` ${chalk4.blue("\u25CF")} Notices: ${notices.length}`);
|
|
1606
|
+
console.log("\n" + chalk4.bold(" Top Issues:"));
|
|
1031
1607
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
1032
1608
|
topIssues.forEach((issue, i) => {
|
|
1033
|
-
const icon = issue.severity === "error" ?
|
|
1609
|
+
const icon = issue.severity === "error" ? chalk4.red("\u2717") : issue.severity === "warning" ? chalk4.yellow("!") : chalk4.blue("i");
|
|
1034
1610
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
1035
1611
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
1036
|
-
console.log(
|
|
1612
|
+
console.log(chalk4.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
1037
1613
|
}
|
|
1038
1614
|
});
|
|
1039
1615
|
if (aiAnalysis) {
|
|
1040
|
-
console.log("\n" +
|
|
1616
|
+
console.log("\n" + chalk4.bold(" AI Analysis:"));
|
|
1041
1617
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
1042
|
-
console.log("\n " +
|
|
1618
|
+
console.log("\n " + chalk4.bold("Suggestions:"));
|
|
1043
1619
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
1044
1620
|
console.log(` ${i + 1}. ${s}`);
|
|
1045
1621
|
});
|
|
1046
1622
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
1047
|
-
console.log("\n " +
|
|
1623
|
+
console.log("\n " + chalk4.bold("Priority Fixes:"));
|
|
1048
1624
|
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1049
1625
|
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1050
|
-
console.log(
|
|
1626
|
+
console.log(chalk4.dim(` ${fix2.rationale}`));
|
|
1051
1627
|
});
|
|
1052
1628
|
}
|
|
1053
1629
|
}
|
|
1054
|
-
console.log("\n" +
|
|
1630
|
+
console.log("\n" + chalk4.bold("\u2550".repeat(60)));
|
|
1055
1631
|
const userInfo = getUserInfo();
|
|
1056
1632
|
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1057
1633
|
const isFreeTier = !loggedIn || tier === "free";
|
|
@@ -1067,13 +1643,13 @@ async function audit(options) {
|
|
|
1067
1643
|
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1068
1644
|
}
|
|
1069
1645
|
if (loggedIn) {
|
|
1070
|
-
const syncSpinner =
|
|
1646
|
+
const syncSpinner = ora2("Syncing to cloud...").start();
|
|
1071
1647
|
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
1072
1648
|
if (syncResult) {
|
|
1073
1649
|
syncSpinner.succeed("Results synced to cloud");
|
|
1074
|
-
console.log(
|
|
1650
|
+
console.log(chalk4.cyan(`
|
|
1075
1651
|
View and manage issues in the web dashboard:`));
|
|
1076
|
-
console.log(
|
|
1652
|
+
console.log(chalk4.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
1077
1653
|
`));
|
|
1078
1654
|
} else {
|
|
1079
1655
|
syncSpinner.warn("Could not sync to cloud");
|
|
@@ -1090,8 +1666,8 @@ async function audit(options) {
|
|
|
1090
1666
|
}
|
|
1091
1667
|
|
|
1092
1668
|
// src/commands/apply.ts
|
|
1093
|
-
import { existsSync as
|
|
1094
|
-
import { join as
|
|
1669
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1670
|
+
import { join as join4 } from "path";
|
|
1095
1671
|
import {
|
|
1096
1672
|
runAuditWithFixes,
|
|
1097
1673
|
generateFixes,
|
|
@@ -1125,11 +1701,11 @@ function truncate(str, maxLen) {
|
|
|
1125
1701
|
}
|
|
1126
1702
|
async function apply(options) {
|
|
1127
1703
|
const cwd = process.cwd();
|
|
1128
|
-
const configPath =
|
|
1704
|
+
const configPath = join4(cwd, "rankcli.config.json");
|
|
1129
1705
|
let config2 = null;
|
|
1130
|
-
if (
|
|
1706
|
+
if (existsSync4(configPath)) {
|
|
1131
1707
|
try {
|
|
1132
|
-
config2 = JSON.parse(
|
|
1708
|
+
config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
1133
1709
|
} catch {
|
|
1134
1710
|
}
|
|
1135
1711
|
}
|
|
@@ -1139,8 +1715,8 @@ async function apply(options) {
|
|
|
1139
1715
|
}
|
|
1140
1716
|
if (!url) {
|
|
1141
1717
|
console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
|
|
1142
|
-
console.log("
|
|
1143
|
-
console.log("\n Or set a URL in your
|
|
1718
|
+
console.log(" rankcli apply --url https://your-site.com");
|
|
1719
|
+
console.log("\n Or set a URL in your rankcli.config.json\n");
|
|
1144
1720
|
return;
|
|
1145
1721
|
}
|
|
1146
1722
|
try {
|
|
@@ -1149,7 +1725,7 @@ async function apply(options) {
|
|
|
1149
1725
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1150
1726
|
return;
|
|
1151
1727
|
}
|
|
1152
|
-
console.log("\n\u{1F680}
|
|
1728
|
+
console.log("\n\u{1F680} RankCLI - Apply Fixes");
|
|
1153
1729
|
console.log("\u2550".repeat(50));
|
|
1154
1730
|
const loggedIn = isLoggedIn();
|
|
1155
1731
|
const userInfo = getUserInfo();
|
|
@@ -1175,11 +1751,18 @@ async function apply(options) {
|
|
|
1175
1751
|
generateFixes: true
|
|
1176
1752
|
});
|
|
1177
1753
|
issues = auditResult.report.issues.map((issue) => ({
|
|
1178
|
-
severity
|
|
1754
|
+
// Squash the audit engine's 5-level severity into generateFixes's
|
|
1755
|
+
// 3-level SEOIssue scheme - matches how audit.ts already treats
|
|
1756
|
+
// 'error' as critical-weight (red ✗) and 'notice' as info-weight.
|
|
1757
|
+
severity: issue.severity === "error" ? "critical" : issue.severity === "notice" ? "info" : issue.severity,
|
|
1179
1758
|
category: issue.category,
|
|
1180
1759
|
code: issue.code,
|
|
1181
1760
|
message: issue.title,
|
|
1182
1761
|
impact: issue.description || void 0
|
|
1762
|
+
// Cast covers `category`: the audit engine's categories (17 values,
|
|
1763
|
+
// e.g. 'crawlability', 'links') don't map cleanly onto SEOIssue's
|
|
1764
|
+
// narrower set - generateFixes never reads category off its input
|
|
1765
|
+
// (each fixer hardcodes its own), so this is safe, not just silenced.
|
|
1183
1766
|
}));
|
|
1184
1767
|
score = auditResult.score;
|
|
1185
1768
|
} catch (error) {
|
|
@@ -1254,7 +1837,7 @@ async function apply(options) {
|
|
|
1254
1837
|
}
|
|
1255
1838
|
if (config2 && !config2.url) {
|
|
1256
1839
|
config2.url = url;
|
|
1257
|
-
|
|
1840
|
+
writeFileSync3(configPath, JSON.stringify(config2, null, 2));
|
|
1258
1841
|
console.log("\n\u{1F4BE} Updated config with URL.");
|
|
1259
1842
|
}
|
|
1260
1843
|
console.log("\n" + "\u2500".repeat(50));
|
|
@@ -1265,7 +1848,7 @@ async function apply(options) {
|
|
|
1265
1848
|
console.log("\n2. Commit the SEO improvements:");
|
|
1266
1849
|
console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
|
|
1267
1850
|
console.log("\n3. Re-run the audit to verify:");
|
|
1268
|
-
console.log(`
|
|
1851
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1269
1852
|
console.log("");
|
|
1270
1853
|
}
|
|
1271
1854
|
|
|
@@ -1278,8 +1861,8 @@ import {
|
|
|
1278
1861
|
applyFixes as applyFixes2,
|
|
1279
1862
|
detectFramework as detectFramework4
|
|
1280
1863
|
} from "@rankcli/agent-runtime";
|
|
1281
|
-
import
|
|
1282
|
-
import
|
|
1864
|
+
import ora3 from "ora";
|
|
1865
|
+
import chalk5 from "chalk";
|
|
1283
1866
|
var execAsync = promisify(exec);
|
|
1284
1867
|
var SEVERITY_ICON2 = {
|
|
1285
1868
|
critical: "\u274C",
|
|
@@ -1297,8 +1880,7 @@ function generateCommitMessage(fixes, url) {
|
|
|
1297
1880
|
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1298
1881
|
);
|
|
1299
1882
|
const type = isAddingNew ? "feat" : "fix";
|
|
1300
|
-
const
|
|
1301
|
-
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1883
|
+
const categoryStr = "seo";
|
|
1302
1884
|
const issueCount = applied.length;
|
|
1303
1885
|
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1304
1886
|
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
@@ -1347,19 +1929,19 @@ function printFix2(fix2, index) {
|
|
|
1347
1929
|
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1348
1930
|
console.log(`
|
|
1349
1931
|
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1350
|
-
console.log(` Code: ${
|
|
1351
|
-
console.log(` File: ${
|
|
1932
|
+
console.log(` Code: ${chalk5.dim(fix2.issue.code)}`);
|
|
1933
|
+
console.log(` File: ${chalk5.cyan(fix2.file)}`);
|
|
1352
1934
|
if (fix2.skipped) {
|
|
1353
|
-
console.log(` Status: ${
|
|
1935
|
+
console.log(` Status: ${chalk5.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1354
1936
|
} else if (fix2.applied) {
|
|
1355
|
-
console.log(` Status: ${
|
|
1356
|
-
console.log(` ${
|
|
1937
|
+
console.log(` Status: ${chalk5.green("APPLIED")}`);
|
|
1938
|
+
console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
|
|
1357
1939
|
} else {
|
|
1358
|
-
console.log(` ${
|
|
1940
|
+
console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
|
|
1359
1941
|
if (fix2.before) {
|
|
1360
|
-
console.log(` ${
|
|
1942
|
+
console.log(` ${chalk5.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1361
1943
|
}
|
|
1362
|
-
console.log(` ${
|
|
1944
|
+
console.log(` ${chalk5.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1363
1945
|
}
|
|
1364
1946
|
}
|
|
1365
1947
|
async function fix(options) {
|
|
@@ -1403,15 +1985,15 @@ async function fix(options) {
|
|
|
1403
1985
|
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1404
1986
|
console.log("\u2550".repeat(50));
|
|
1405
1987
|
}
|
|
1406
|
-
const frameworkSpinner = options.output !== "json" ?
|
|
1988
|
+
const frameworkSpinner = options.output !== "json" ? ora3("Detecting framework...").start() : null;
|
|
1407
1989
|
const frameworkResult = await detectFramework4({ cwd });
|
|
1408
1990
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1409
1991
|
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1410
|
-
const analyzeSpinner = options.output !== "json" ?
|
|
1992
|
+
const analyzeSpinner = options.output !== "json" ? ora3(`Analyzing ${url}...`).start() : null;
|
|
1411
1993
|
let issues;
|
|
1412
1994
|
let score;
|
|
1413
1995
|
try {
|
|
1414
|
-
const analysis = await runDirectAnalysis2(
|
|
1996
|
+
const analysis = await runDirectAnalysis2(url);
|
|
1415
1997
|
issues = analysis.issues;
|
|
1416
1998
|
score = analysis.score;
|
|
1417
1999
|
} catch (error) {
|
|
@@ -1424,7 +2006,7 @@ async function fix(options) {
|
|
|
1424
2006
|
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1425
2007
|
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1426
2008
|
if (options.output !== "json") {
|
|
1427
|
-
console.log(
|
|
2009
|
+
console.log(chalk5.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1428
2010
|
}
|
|
1429
2011
|
}
|
|
1430
2012
|
if (issues.length === 0) {
|
|
@@ -1441,7 +2023,7 @@ async function fix(options) {
|
|
|
1441
2023
|
durationMs: Date.now() - startTime
|
|
1442
2024
|
};
|
|
1443
2025
|
}
|
|
1444
|
-
const fixSpinner = options.output !== "json" ?
|
|
2026
|
+
const fixSpinner = options.output !== "json" ? ora3("Generating fixes...").start() : null;
|
|
1445
2027
|
const fixes = await generateFixes2(issues, {
|
|
1446
2028
|
cwd,
|
|
1447
2029
|
url,
|
|
@@ -1453,13 +2035,13 @@ async function fix(options) {
|
|
|
1453
2035
|
if (options.dryRun) {
|
|
1454
2036
|
if (options.output !== "json") {
|
|
1455
2037
|
console.log("\n" + "\u2500".repeat(50));
|
|
1456
|
-
console.log(
|
|
2038
|
+
console.log(chalk5.yellow("DRY RUN - No changes will be made"));
|
|
1457
2039
|
console.log("\u2500".repeat(50));
|
|
1458
2040
|
for (let i = 0; i < fixes.length; i++) {
|
|
1459
2041
|
printFix2(fixes[i], i);
|
|
1460
2042
|
}
|
|
1461
2043
|
console.log("\n" + "\u2500".repeat(50));
|
|
1462
|
-
console.log(`\u{1F4CB} ${
|
|
2044
|
+
console.log(`\u{1F4CB} ${chalk5.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1463
2045
|
console.log(` Remove --dry-run to apply these fixes.
|
|
1464
2046
|
`);
|
|
1465
2047
|
}
|
|
@@ -1525,7 +2107,7 @@ async function fix(options) {
|
|
|
1525
2107
|
durationMs: Date.now() - startTime
|
|
1526
2108
|
};
|
|
1527
2109
|
}
|
|
1528
|
-
const applySpinner = options.output !== "json" ?
|
|
2110
|
+
const applySpinner = options.output !== "json" ? ora3("Applying fixes...").start() : null;
|
|
1529
2111
|
const { applied, skipped } = await applyFixes2(fixes, { cwd, dryRun: false });
|
|
1530
2112
|
for (const fix2 of applied) {
|
|
1531
2113
|
fix2.applied = true;
|
|
@@ -1537,7 +2119,7 @@ async function fix(options) {
|
|
|
1537
2119
|
let commitError;
|
|
1538
2120
|
let pushError;
|
|
1539
2121
|
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1540
|
-
const gitSpinner = options.output !== "json" ?
|
|
2122
|
+
const gitSpinner = options.output !== "json" ? ora3("Committing changes...").start() : null;
|
|
1541
2123
|
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1542
2124
|
committed = gitResult.committed;
|
|
1543
2125
|
pushed = gitResult.pushed;
|
|
@@ -1563,23 +2145,23 @@ async function fix(options) {
|
|
|
1563
2145
|
console.log("\u2550".repeat(50));
|
|
1564
2146
|
if (applied.length > 0) {
|
|
1565
2147
|
console.log(`
|
|
1566
|
-
${
|
|
2148
|
+
${chalk5.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1567
2149
|
for (const fix2 of applied) {
|
|
1568
2150
|
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1569
2151
|
}
|
|
1570
2152
|
}
|
|
1571
2153
|
if (skipped.length > 0) {
|
|
1572
2154
|
console.log(`
|
|
1573
|
-
${
|
|
2155
|
+
${chalk5.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1574
2156
|
for (const fix2 of skipped) {
|
|
1575
2157
|
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1576
2158
|
}
|
|
1577
2159
|
}
|
|
1578
2160
|
if (committed) {
|
|
1579
2161
|
console.log(`
|
|
1580
|
-
${
|
|
2162
|
+
${chalk5.green("\u2705")} Changes committed`);
|
|
1581
2163
|
if (pushed) {
|
|
1582
|
-
console.log(`${
|
|
2164
|
+
console.log(`${chalk5.green("\u2705")} Changes pushed`);
|
|
1583
2165
|
}
|
|
1584
2166
|
}
|
|
1585
2167
|
if (!committed && applied.length > 0) {
|
|
@@ -1632,8 +2214,8 @@ ${chalk3.green("\u2705")} Changes committed`);
|
|
|
1632
2214
|
}
|
|
1633
2215
|
|
|
1634
2216
|
// src/commands/track.ts
|
|
1635
|
-
import
|
|
1636
|
-
import
|
|
2217
|
+
import ora4 from "ora";
|
|
2218
|
+
import chalk6 from "chalk";
|
|
1637
2219
|
import { table } from "table";
|
|
1638
2220
|
import { RankTracker, TIER_LIMITS as TIER_LIMITS2 } from "@rankcli/agent-runtime";
|
|
1639
2221
|
async function getProjectForUrl(url) {
|
|
@@ -1672,35 +2254,35 @@ function getTierLimits(tier) {
|
|
|
1672
2254
|
};
|
|
1673
2255
|
}
|
|
1674
2256
|
function formatPosition(pos) {
|
|
1675
|
-
if (pos === null) return
|
|
1676
|
-
if (pos <= 3) return
|
|
1677
|
-
if (pos <= 10) return
|
|
1678
|
-
if (pos <= 20) return
|
|
1679
|
-
if (pos <= 50) return
|
|
1680
|
-
return
|
|
2257
|
+
if (pos === null) return chalk6.dim("N/A");
|
|
2258
|
+
if (pos <= 3) return chalk6.green.bold(`#${pos}`);
|
|
2259
|
+
if (pos <= 10) return chalk6.green(`#${pos}`);
|
|
2260
|
+
if (pos <= 20) return chalk6.yellow(`#${pos}`);
|
|
2261
|
+
if (pos <= 50) return chalk6.red(`#${pos}`);
|
|
2262
|
+
return chalk6.red.dim(`#${pos}`);
|
|
1681
2263
|
}
|
|
1682
2264
|
function formatChange(change) {
|
|
1683
|
-
if (change === 0) return
|
|
1684
|
-
if (change > 0) return
|
|
1685
|
-
return
|
|
2265
|
+
if (change === 0) return chalk6.dim("\u2192");
|
|
2266
|
+
if (change > 0) return chalk6.green(`\u2191${change}`);
|
|
2267
|
+
return chalk6.red(`\u2193${Math.abs(change)}`);
|
|
1686
2268
|
}
|
|
1687
2269
|
async function trackAdd(options) {
|
|
1688
2270
|
if (!options.keywords || options.keywords.length === 0) {
|
|
1689
|
-
console.log(
|
|
2271
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1690
2272
|
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1691
2273
|
return { success: false, error: "No keywords provided" };
|
|
1692
2274
|
}
|
|
1693
2275
|
if (!options.url) {
|
|
1694
|
-
console.log(
|
|
2276
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1695
2277
|
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1696
2278
|
return { success: false, error: "No URL provided" };
|
|
1697
2279
|
}
|
|
1698
2280
|
if (!isLoggedIn()) {
|
|
1699
|
-
console.log(
|
|
2281
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1700
2282
|
console.log(" rankcli login\n");
|
|
1701
2283
|
return { success: false, error: "Not logged in" };
|
|
1702
2284
|
}
|
|
1703
|
-
const spinner =
|
|
2285
|
+
const spinner = ora4("Adding keywords to track...").start();
|
|
1704
2286
|
try {
|
|
1705
2287
|
const project = await getProjectForUrl(options.url);
|
|
1706
2288
|
if (!project) {
|
|
@@ -1720,7 +2302,7 @@ async function trackAdd(options) {
|
|
|
1720
2302
|
const newCount = options.keywords.length;
|
|
1721
2303
|
if (existingCount + newCount > maxKeywords) {
|
|
1722
2304
|
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1723
|
-
showUpsell("keyword_limit", { tier, current: existingCount,
|
|
2305
|
+
showUpsell("keyword_limit", { tier, current: existingCount, max: maxKeywords });
|
|
1724
2306
|
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1725
2307
|
}
|
|
1726
2308
|
const keywordRecords = options.keywords.map((keyword) => ({
|
|
@@ -1741,10 +2323,10 @@ async function trackAdd(options) {
|
|
|
1741
2323
|
}
|
|
1742
2324
|
const addedCount = data?.length || 0;
|
|
1743
2325
|
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1744
|
-
console.log(
|
|
2326
|
+
console.log(chalk6.dim(`
|
|
1745
2327
|
Tracking: ${options.keywords.join(", ")}`));
|
|
1746
|
-
console.log(
|
|
1747
|
-
console.log(
|
|
2328
|
+
console.log(chalk6.dim(`For: ${project.domain}`));
|
|
2329
|
+
console.log(chalk6.cyan(`
|
|
1748
2330
|
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1749
2331
|
`));
|
|
1750
2332
|
return { success: true, addedCount };
|
|
@@ -1755,16 +2337,16 @@ Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
|
1755
2337
|
}
|
|
1756
2338
|
async function trackStatus(options) {
|
|
1757
2339
|
if (!options.url) {
|
|
1758
|
-
console.log(
|
|
2340
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1759
2341
|
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1760
2342
|
return { success: false, error: "No URL provided" };
|
|
1761
2343
|
}
|
|
1762
2344
|
if (!isLoggedIn()) {
|
|
1763
|
-
console.log(
|
|
2345
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1764
2346
|
console.log(" rankcli login\n");
|
|
1765
2347
|
return { success: false, error: "Not logged in" };
|
|
1766
2348
|
}
|
|
1767
|
-
const spinner =
|
|
2349
|
+
const spinner = ora4("Loading keyword rankings...").start();
|
|
1768
2350
|
try {
|
|
1769
2351
|
const project = await getProjectForUrl(options.url);
|
|
1770
2352
|
if (!project) {
|
|
@@ -1783,8 +2365,8 @@ async function trackStatus(options) {
|
|
|
1783
2365
|
}
|
|
1784
2366
|
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1785
2367
|
if (!data || data.length === 0) {
|
|
1786
|
-
console.log(
|
|
1787
|
-
console.log(
|
|
2368
|
+
console.log(chalk6.dim("\nNo keywords tracked yet."));
|
|
2369
|
+
console.log(chalk6.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
|
|
1788
2370
|
`));
|
|
1789
2371
|
return { success: true, keywords: [] };
|
|
1790
2372
|
}
|
|
@@ -1799,7 +2381,7 @@ async function trackStatus(options) {
|
|
|
1799
2381
|
if (options.output === "json") {
|
|
1800
2382
|
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1801
2383
|
} else {
|
|
1802
|
-
console.log("\n" +
|
|
2384
|
+
console.log("\n" + chalk6.bold("\u{1F4CA} Keyword Rankings"));
|
|
1803
2385
|
console.log("\u2550".repeat(60));
|
|
1804
2386
|
const tableData = [
|
|
1805
2387
|
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
@@ -1842,14 +2424,14 @@ async function trackHistory(options) {
|
|
|
1842
2424
|
return { success: false, error: "No URL provided" };
|
|
1843
2425
|
}
|
|
1844
2426
|
if (!options.keyword) {
|
|
1845
|
-
console.log(
|
|
2427
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
1846
2428
|
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
1847
2429
|
return { success: false, error: "No keyword provided" };
|
|
1848
2430
|
}
|
|
1849
2431
|
if (!isLoggedIn()) {
|
|
1850
2432
|
return { success: false, error: "Not logged in" };
|
|
1851
2433
|
}
|
|
1852
|
-
const spinner =
|
|
2434
|
+
const spinner = ora4(`Loading history for "${options.keyword}"...`).start();
|
|
1853
2435
|
try {
|
|
1854
2436
|
const project = await getProjectForUrl(options.url);
|
|
1855
2437
|
if (!project) {
|
|
@@ -1882,21 +2464,21 @@ async function trackHistory(options) {
|
|
|
1882
2464
|
if (options.output === "json") {
|
|
1883
2465
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
1884
2466
|
} else {
|
|
1885
|
-
console.log("\n" +
|
|
2467
|
+
console.log("\n" + chalk6.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
1886
2468
|
console.log("\u2550".repeat(50));
|
|
1887
2469
|
if (history.length === 0) {
|
|
1888
|
-
console.log(
|
|
2470
|
+
console.log(chalk6.dim("\nNo history available yet.\n"));
|
|
1889
2471
|
} else {
|
|
1890
2472
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
1891
2473
|
const sparkline = positions.map((p) => {
|
|
1892
|
-
if (p <= 10) return
|
|
1893
|
-
if (p <= 20) return
|
|
1894
|
-
if (p <= 50) return
|
|
1895
|
-
return
|
|
2474
|
+
if (p <= 10) return chalk6.green("\u2588");
|
|
2475
|
+
if (p <= 20) return chalk6.yellow("\u2593");
|
|
2476
|
+
if (p <= 50) return chalk6.red("\u2592");
|
|
2477
|
+
return chalk6.dim("\u2591");
|
|
1896
2478
|
}).join("");
|
|
1897
2479
|
console.log(`
|
|
1898
2480
|
${sparkline} (last ${positions.length} checks)`);
|
|
1899
|
-
console.log(
|
|
2481
|
+
console.log(chalk6.dim(" \u2190 newer older \u2192\n"));
|
|
1900
2482
|
const recent = history.slice(0, 10);
|
|
1901
2483
|
for (const entry of recent) {
|
|
1902
2484
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -1916,7 +2498,7 @@ async function trackExport(options) {
|
|
|
1916
2498
|
if (!isLoggedIn()) {
|
|
1917
2499
|
return { success: false, error: "Not logged in" };
|
|
1918
2500
|
}
|
|
1919
|
-
const spinner =
|
|
2501
|
+
const spinner = ora4("Exporting data...").start();
|
|
1920
2502
|
try {
|
|
1921
2503
|
const project = await getProjectForUrl(options.url);
|
|
1922
2504
|
if (!project) {
|
|
@@ -1973,12 +2555,12 @@ async function trackExport(options) {
|
|
|
1973
2555
|
}
|
|
1974
2556
|
async function trackCheck(options) {
|
|
1975
2557
|
if (!options.url) {
|
|
1976
|
-
console.log(
|
|
2558
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1977
2559
|
console.log(" rankcli track check --url https://your-site.com\n");
|
|
1978
2560
|
return { success: false, error: "No URL provided" };
|
|
1979
2561
|
}
|
|
1980
2562
|
if (!isLoggedIn()) {
|
|
1981
|
-
console.log(
|
|
2563
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
1982
2564
|
console.log(" rankcli login\n");
|
|
1983
2565
|
return { success: false, error: "Not logged in" };
|
|
1984
2566
|
}
|
|
@@ -1995,14 +2577,14 @@ async function trackCheck(options) {
|
|
|
1995
2577
|
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
1996
2578
|
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
1997
2579
|
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
1998
|
-
console.log(
|
|
2580
|
+
console.log(chalk6.yellow(`
|
|
1999
2581
|
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
2000
|
-
console.log(
|
|
2582
|
+
console.log(chalk6.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
2001
2583
|
`));
|
|
2002
2584
|
return { success: true, rateLimited: true };
|
|
2003
2585
|
}
|
|
2004
2586
|
}
|
|
2005
|
-
const spinner =
|
|
2587
|
+
const spinner = ora4("Checking current rankings...").start();
|
|
2006
2588
|
const supabase = await getAuthenticatedClient();
|
|
2007
2589
|
if (!supabase) {
|
|
2008
2590
|
spinner.fail("Could not connect to database");
|
|
@@ -2016,8 +2598,8 @@ async function trackCheck(options) {
|
|
|
2016
2598
|
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2017
2599
|
if (!serpApiKey) {
|
|
2018
2600
|
spinner.warn("No SERP API key configured");
|
|
2019
|
-
console.log(
|
|
2020
|
-
console.log(
|
|
2601
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
|
|
2602
|
+
console.log(chalk6.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
|
|
2021
2603
|
return { success: false, error: "No SERP API key" };
|
|
2022
2604
|
}
|
|
2023
2605
|
const tracker = new RankTracker({
|
|
@@ -2029,13 +2611,13 @@ async function trackCheck(options) {
|
|
|
2029
2611
|
});
|
|
2030
2612
|
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2031
2613
|
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2032
|
-
console.log("\n" +
|
|
2614
|
+
console.log("\n" + chalk6.bold("\u{1F50D} Rank Check Results"));
|
|
2033
2615
|
console.log("\u2550".repeat(50));
|
|
2034
2616
|
for (const result of results) {
|
|
2035
2617
|
const posStr = formatPosition(result.position);
|
|
2036
2618
|
console.log(` ${result.keyword}: ${posStr}`);
|
|
2037
2619
|
if (result.serpFeatures.length > 0) {
|
|
2038
|
-
console.log(
|
|
2620
|
+
console.log(chalk6.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2039
2621
|
}
|
|
2040
2622
|
}
|
|
2041
2623
|
console.log("");
|
|
@@ -2054,8 +2636,8 @@ async function trackCheck(options) {
|
|
|
2054
2636
|
|
|
2055
2637
|
// src/commands/geo.ts
|
|
2056
2638
|
import { analyzers } from "@rankcli/agent-runtime";
|
|
2057
|
-
import
|
|
2058
|
-
import
|
|
2639
|
+
import ora5 from "ora";
|
|
2640
|
+
import chalk7 from "chalk";
|
|
2059
2641
|
async function fetchResources(url) {
|
|
2060
2642
|
const headers = {};
|
|
2061
2643
|
let html = null;
|
|
@@ -2101,7 +2683,7 @@ async function geo(options) {
|
|
|
2101
2683
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
2102
2684
|
return;
|
|
2103
2685
|
}
|
|
2104
|
-
const spinner =
|
|
2686
|
+
const spinner = ora5("Fetching page and robots.txt...").start();
|
|
2105
2687
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
2106
2688
|
if (!html) {
|
|
2107
2689
|
spinner.fail("Could not fetch page content");
|
|
@@ -2127,55 +2709,55 @@ async function geo(options) {
|
|
|
2127
2709
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2128
2710
|
return;
|
|
2129
2711
|
}
|
|
2130
|
-
console.log("\n" +
|
|
2131
|
-
console.log(
|
|
2132
|
-
console.log(
|
|
2712
|
+
console.log("\n" + chalk7.bold("\u2550".repeat(60)));
|
|
2713
|
+
console.log(chalk7.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2714
|
+
console.log(chalk7.bold("\u2550".repeat(60)));
|
|
2133
2715
|
const geoScore = geoResult.score;
|
|
2134
|
-
const scoreColor = geoScore >= 80 ?
|
|
2716
|
+
const scoreColor = geoScore >= 80 ? chalk7.green : geoScore >= 60 ? chalk7.yellow : chalk7.red;
|
|
2135
2717
|
console.log(`
|
|
2136
|
-
${
|
|
2137
|
-
console.log("\n" +
|
|
2718
|
+
${chalk7.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2719
|
+
console.log("\n" + chalk7.bold(" AI Crawler Access:"));
|
|
2138
2720
|
const { aiCrawlerAccess } = geoResult;
|
|
2139
2721
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
2140
|
-
console.log(
|
|
2722
|
+
console.log(chalk7.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
2141
2723
|
}
|
|
2142
2724
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
2143
2725
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
2144
|
-
console.log(
|
|
2726
|
+
console.log(chalk7.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
2145
2727
|
}
|
|
2146
|
-
console.log("\n" +
|
|
2728
|
+
console.log("\n" + chalk7.bold(" Rendering:"));
|
|
2147
2729
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
2148
|
-
console.log(
|
|
2730
|
+
console.log(chalk7.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
2149
2731
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
2150
|
-
console.log(
|
|
2732
|
+
console.log(chalk7.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
2151
2733
|
}
|
|
2152
|
-
console.log("\n" +
|
|
2734
|
+
console.log("\n" + chalk7.bold(" LLM Friendliness:"));
|
|
2153
2735
|
const { llmSignals } = geoResult;
|
|
2154
2736
|
const bar = (score) => {
|
|
2155
2737
|
const filled = Math.round(score / 5);
|
|
2156
2738
|
const empty = 20 - filled;
|
|
2157
|
-
const color = score >= 70 ?
|
|
2158
|
-
return color("\u2588".repeat(filled)) +
|
|
2739
|
+
const color = score >= 70 ? chalk7.green : score >= 40 ? chalk7.yellow : chalk7.red;
|
|
2740
|
+
return color("\u2588".repeat(filled)) + chalk7.gray("\u2591".repeat(empty));
|
|
2159
2741
|
};
|
|
2160
2742
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
2161
2743
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
2162
2744
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
2163
2745
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
2164
|
-
console.log("\n" +
|
|
2746
|
+
console.log("\n" + chalk7.bold(" Content Structure:"));
|
|
2165
2747
|
const { contentStructure } = geoResult;
|
|
2166
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
2167
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
2168
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
2169
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
2170
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
2171
|
-
console.log("\n" +
|
|
2748
|
+
console.log(` ${contentStructure.hasStructuredData ? chalk7.green("\u2705") : chalk7.red("\u274C")} JSON-LD Structured Data`);
|
|
2749
|
+
console.log(` ${contentStructure.hasFAQSchema ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} FAQ Schema`);
|
|
2750
|
+
console.log(` ${contentStructure.hasArticleSchema ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} Article Schema`);
|
|
2751
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? chalk7.green("\u2705") : chalk7.yellow("\u25CB")} Breadcrumbs`);
|
|
2752
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? chalk7.green("Good") : contentStructure.headingHierarchy === "needs-work" ? chalk7.yellow("Needs Work") : chalk7.red("Poor")}`);
|
|
2753
|
+
console.log("\n" + chalk7.bold(" Citation Readiness:"));
|
|
2172
2754
|
const { citationReadiness } = geoResult;
|
|
2173
2755
|
if (citationReadiness.trustSignals.length > 0) {
|
|
2174
|
-
console.log(
|
|
2756
|
+
console.log(chalk7.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
2175
2757
|
} else {
|
|
2176
|
-
console.log(
|
|
2758
|
+
console.log(chalk7.yellow(" \u25CB No trust signals detected"));
|
|
2177
2759
|
}
|
|
2178
|
-
console.log("\n" +
|
|
2760
|
+
console.log("\n" + chalk7.bold(" Other Scores:"));
|
|
2179
2761
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
2180
2762
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
2181
2763
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -2189,18 +2771,18 @@ async function geo(options) {
|
|
|
2189
2771
|
];
|
|
2190
2772
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
2191
2773
|
if (criticalIssues.length > 0) {
|
|
2192
|
-
console.log("\n" +
|
|
2774
|
+
console.log("\n" + chalk7.bold.red(" Critical Issues:"));
|
|
2193
2775
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
2194
|
-
console.log(
|
|
2776
|
+
console.log(chalk7.red(` ${i + 1}. ${issue.title}`));
|
|
2195
2777
|
});
|
|
2196
2778
|
}
|
|
2197
2779
|
if (geoResult.recommendations.length > 0) {
|
|
2198
|
-
console.log("\n" +
|
|
2780
|
+
console.log("\n" + chalk7.bold(" Recommendations:"));
|
|
2199
2781
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
2200
2782
|
console.log(` ${i + 1}. ${rec}`);
|
|
2201
2783
|
});
|
|
2202
2784
|
}
|
|
2203
|
-
console.log("\n" +
|
|
2785
|
+
console.log("\n" + chalk7.bold("\u2550".repeat(60)));
|
|
2204
2786
|
console.log("");
|
|
2205
2787
|
} catch (error) {
|
|
2206
2788
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -2208,7 +2790,7 @@ async function geo(options) {
|
|
|
2208
2790
|
}
|
|
2209
2791
|
|
|
2210
2792
|
// src/commands/keywords.ts
|
|
2211
|
-
import * as
|
|
2793
|
+
import * as readline2 from "readline";
|
|
2212
2794
|
import {
|
|
2213
2795
|
runKeywordResearch,
|
|
2214
2796
|
formatKeywordReport,
|
|
@@ -2221,10 +2803,10 @@ import {
|
|
|
2221
2803
|
runAIKeywordResearch
|
|
2222
2804
|
} from "@rankcli/agent-runtime";
|
|
2223
2805
|
async function keywords(options) {
|
|
2224
|
-
console.log("\n\u{1F511}
|
|
2806
|
+
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
2225
2807
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
2226
2808
|
console.log("based on your site's current authority and competition level.\n");
|
|
2227
|
-
const rl =
|
|
2809
|
+
const rl = readline2.createInterface({
|
|
2228
2810
|
input: process.stdin,
|
|
2229
2811
|
output: process.stdout
|
|
2230
2812
|
});
|
|
@@ -2590,10 +3172,12 @@ import {
|
|
|
2590
3172
|
formatFeaturedSnippetReport,
|
|
2591
3173
|
analyzeKeywordDensity,
|
|
2592
3174
|
formatKeywordDensityReport,
|
|
2593
|
-
crawlUrl
|
|
3175
|
+
crawlUrl,
|
|
3176
|
+
extractMeta,
|
|
3177
|
+
analyzeHeadings
|
|
2594
3178
|
} from "@rankcli/agent-runtime";
|
|
2595
3179
|
async function analyzeContent(options) {
|
|
2596
|
-
console.log("\n\u{1F4DD}
|
|
3180
|
+
console.log("\n\u{1F4DD} RankCLI - Content Analysis\n");
|
|
2597
3181
|
const mode = options.mode || "full";
|
|
2598
3182
|
if (mode === "headline" || options.headline) {
|
|
2599
3183
|
const headline = options.headline || "";
|
|
@@ -2617,15 +3201,22 @@ async function analyzeContent(options) {
|
|
|
2617
3201
|
if (options.url) {
|
|
2618
3202
|
console.log(`Fetching content from ${options.url}...`);
|
|
2619
3203
|
try {
|
|
2620
|
-
const
|
|
2621
|
-
if (!
|
|
3204
|
+
const crawlResult = await crawlUrl({ url: options.url });
|
|
3205
|
+
if (!crawlResult.success || !crawlResult.data) {
|
|
2622
3206
|
console.log("\u274C Could not fetch content from URL");
|
|
2623
3207
|
return;
|
|
2624
3208
|
}
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
|
|
3209
|
+
const { html } = crawlResult.data;
|
|
3210
|
+
const [metaResult, headingsResult] = await Promise.all([
|
|
3211
|
+
extractMeta({ html, url: options.url }),
|
|
3212
|
+
analyzeHeadings({ html })
|
|
3213
|
+
]);
|
|
3214
|
+
const meta = metaResult.success ? metaResult.data : void 0;
|
|
3215
|
+
const headingStructure = headingsResult.success ? headingsResult.data : [];
|
|
3216
|
+
const title = meta?.title || "";
|
|
3217
|
+
const h1 = headingStructure.find((h) => h.level === 1)?.text || "";
|
|
3218
|
+
const headings = headingStructure.map((h) => h.text);
|
|
3219
|
+
const bodyText = extractBodyText(html);
|
|
2629
3220
|
const paragraphs = bodyText.split(/\n\n+/);
|
|
2630
3221
|
console.log(`
|
|
2631
3222
|
Analyzing: "${title || options.url}"
|
|
@@ -2667,215 +3258,39 @@ Analyzing: "${title || options.url}"
|
|
|
2667
3258
|
console.log(` H1: ${h1 || "(none)"}`);
|
|
2668
3259
|
console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
|
|
2669
3260
|
console.log(` Headings: ${headings.length}`);
|
|
2670
|
-
console.log("");
|
|
2671
|
-
} catch (error) {
|
|
2672
|
-
console.log(`
|
|
2673
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2674
|
-
`);
|
|
2675
|
-
}
|
|
2676
|
-
} else {
|
|
2677
|
-
console.log("Usage:");
|
|
2678
|
-
console.log(" seo content -u <url> Full content analysis");
|
|
2679
|
-
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
2680
|
-
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
2681
|
-
console.log(" seo content -u <url> --mode readability Readability only");
|
|
2682
|
-
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
2683
|
-
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
2684
|
-
console.log("");
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
async function analyzeIntent(keyword) {
|
|
2688
|
-
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
2689
|
-
const analysis = classifyIntent(keyword);
|
|
2690
|
-
console.log(formatIntentReport(keyword, analysis));
|
|
2691
|
-
}
|
|
2692
|
-
function extractBodyText(html) {
|
|
2693
|
-
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
2694
|
-
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2695
|
-
text = text.replace(/<[^>]+>/g, " ");
|
|
2696
|
-
text = text.replace(/ /g, " ");
|
|
2697
|
-
text = text.replace(/&/g, "&");
|
|
2698
|
-
text = text.replace(/</g, "<");
|
|
2699
|
-
text = text.replace(/>/g, ">");
|
|
2700
|
-
text = text.replace(/"/g, '"');
|
|
2701
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
2702
|
-
return text;
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
// src/commands/setup.ts
|
|
2706
|
-
import * as readline2 from "readline";
|
|
2707
|
-
import * as fs from "fs";
|
|
2708
|
-
import * as path from "path";
|
|
2709
|
-
import {
|
|
2710
|
-
injectGA4,
|
|
2711
|
-
injectGSCVerification,
|
|
2712
|
-
writeGitHubActionFiles,
|
|
2713
|
-
getGSCSetupInstructions,
|
|
2714
|
-
generateGA4EnvTemplate
|
|
2715
|
-
} from "@rankcli/agent-runtime";
|
|
2716
|
-
async function setup(options) {
|
|
2717
|
-
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
2718
|
-
const projectPath = process.cwd();
|
|
2719
|
-
if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
|
|
2720
|
-
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
2721
|
-
}
|
|
2722
|
-
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
2723
|
-
await interactiveSetup(projectPath);
|
|
2724
|
-
return;
|
|
2725
|
-
}
|
|
2726
|
-
if (options.ga4) {
|
|
2727
|
-
await setupGA4(projectPath, options.ga4);
|
|
2728
|
-
}
|
|
2729
|
-
if (options.gsc) {
|
|
2730
|
-
await setupGSC(projectPath, options.gsc);
|
|
2731
|
-
}
|
|
2732
|
-
if (options.githubAction) {
|
|
2733
|
-
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
async function interactiveSetup(projectPath) {
|
|
2737
|
-
const rl = readline2.createInterface({
|
|
2738
|
-
input: process.stdin,
|
|
2739
|
-
output: process.stdout
|
|
2740
|
-
});
|
|
2741
|
-
const ask2 = (question) => {
|
|
2742
|
-
return new Promise((resolve) => {
|
|
2743
|
-
rl.question(question, (answer) => {
|
|
2744
|
-
resolve(answer.trim());
|
|
2745
|
-
});
|
|
2746
|
-
});
|
|
2747
|
-
};
|
|
2748
|
-
try {
|
|
2749
|
-
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
2750
|
-
console.log("\u2500".repeat(60));
|
|
2751
|
-
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
2752
|
-
console.log("GA4 tracks visitor behavior on your site.");
|
|
2753
|
-
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
2754
|
-
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
2755
|
-
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
2756
|
-
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
2757
|
-
await setupGA4(projectPath, ga4Id);
|
|
2758
|
-
} else if (ga4Id) {
|
|
2759
|
-
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
2760
|
-
} else {
|
|
2761
|
-
console.log("Skipping GA4 setup.\n");
|
|
2762
|
-
}
|
|
2763
|
-
console.log("\u2500".repeat(60));
|
|
2764
|
-
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
2765
|
-
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
2766
|
-
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
2767
|
-
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
2768
|
-
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
2769
|
-
if (gscCode && gscCode.length > 10) {
|
|
2770
|
-
await setupGSC(projectPath, gscCode);
|
|
2771
|
-
} else if (gscCode) {
|
|
2772
|
-
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
2773
|
-
} else {
|
|
2774
|
-
console.log("Skipping GSC verification.\n");
|
|
2775
|
-
}
|
|
2776
|
-
console.log("\u2500".repeat(60));
|
|
2777
|
-
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
2778
|
-
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
2779
|
-
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
2780
|
-
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
2781
|
-
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
2782
|
-
console.log("\nHow often should the check run?");
|
|
2783
|
-
console.log(" 1. Weekly (recommended for most sites)");
|
|
2784
|
-
console.log(" 2. Daily (for high-traffic sites)");
|
|
2785
|
-
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
2786
|
-
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
2787
|
-
const scheduleMap = {
|
|
2788
|
-
"1": "weekly",
|
|
2789
|
-
"2": "daily",
|
|
2790
|
-
"3": "monthly"
|
|
2791
|
-
};
|
|
2792
|
-
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2793
|
-
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2794
|
-
if (siteUrl) {
|
|
2795
|
-
await setupGitHubAction(projectPath, schedule, siteUrl);
|
|
2796
|
-
}
|
|
2797
|
-
} else {
|
|
2798
|
-
console.log("Skipping GitHub Action setup.\n");
|
|
2799
|
-
}
|
|
2800
|
-
console.log("\u2500".repeat(60));
|
|
2801
|
-
console.log("\u2705 Setup complete!\n");
|
|
2802
|
-
console.log("Next steps:");
|
|
2803
|
-
console.log(" 1. Commit the changes to your repository");
|
|
2804
|
-
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
2805
|
-
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
2806
|
-
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
2807
|
-
rl.close();
|
|
2808
|
-
} catch (error) {
|
|
2809
|
-
console.log(`
|
|
3261
|
+
console.log("");
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
console.log(`
|
|
2810
3264
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2811
3265
|
`);
|
|
2812
|
-
rl.close();
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
async function setupGA4(projectPath, measurementId) {
|
|
2816
|
-
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
2817
|
-
const result = await injectGA4(projectPath, { measurementId });
|
|
2818
|
-
if (result.success) {
|
|
2819
|
-
console.log(`\u2705 ${result.message}`);
|
|
2820
|
-
if (result.file) {
|
|
2821
|
-
console.log(` Modified: ${result.file}`);
|
|
2822
|
-
}
|
|
2823
|
-
} else {
|
|
2824
|
-
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
2825
|
-
if (result.code) {
|
|
2826
|
-
console.log("\nManually add this code to your HTML <head>:\n");
|
|
2827
|
-
console.log(result.code);
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
const envExamplePath = path.join(projectPath, ".env.example");
|
|
2831
|
-
const envContent = generateGA4EnvTemplate();
|
|
2832
|
-
if (fs.existsSync(envExamplePath)) {
|
|
2833
|
-
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
2834
|
-
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
2835
|
-
fs.appendFileSync(envExamplePath, "\n" + envContent);
|
|
2836
|
-
console.log(" Updated: .env.example");
|
|
2837
|
-
}
|
|
2838
|
-
} else {
|
|
2839
|
-
fs.writeFileSync(envExamplePath, envContent);
|
|
2840
|
-
console.log(" Created: .env.example");
|
|
2841
|
-
}
|
|
2842
|
-
console.log("");
|
|
2843
|
-
}
|
|
2844
|
-
async function setupGSC(projectPath, verificationCode) {
|
|
2845
|
-
console.log("Setting up Google Search Console verification...");
|
|
2846
|
-
const result = await injectGSCVerification(projectPath, verificationCode);
|
|
2847
|
-
if (result.success) {
|
|
2848
|
-
console.log(`\u2705 ${result.message}`);
|
|
2849
|
-
if (result.file) {
|
|
2850
|
-
console.log(` Modified: ${result.file}`);
|
|
2851
3266
|
}
|
|
2852
3267
|
} else {
|
|
2853
|
-
console.log(
|
|
3268
|
+
console.log("Usage:");
|
|
3269
|
+
console.log(" seo content -u <url> Full content analysis");
|
|
3270
|
+
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
3271
|
+
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
3272
|
+
console.log(" seo content -u <url> --mode readability Readability only");
|
|
3273
|
+
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
3274
|
+
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
3275
|
+
console.log("");
|
|
2854
3276
|
}
|
|
2855
|
-
console.log("");
|
|
2856
3277
|
}
|
|
2857
|
-
async function
|
|
2858
|
-
console.log(
|
|
2859
|
-
const
|
|
2860
|
-
|
|
2861
|
-
siteUrl: siteUrl || "https://example.com",
|
|
2862
|
-
features: {
|
|
2863
|
-
audit: true,
|
|
2864
|
-
tracking: true,
|
|
2865
|
-
autoFix: true,
|
|
2866
|
-
createIssues: true,
|
|
2867
|
-
createPRs: true
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
|
|
2871
|
-
console.log("\u2705 GitHub Action files created:");
|
|
2872
|
-
for (const file of files) {
|
|
2873
|
-
console.log(` - ${path.relative(projectPath, file)}`);
|
|
2874
|
-
}
|
|
2875
|
-
console.log(instructions);
|
|
3278
|
+
async function analyzeIntent(keyword) {
|
|
3279
|
+
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
3280
|
+
const analysis = classifyIntent(keyword);
|
|
3281
|
+
console.log(formatIntentReport(keyword, analysis));
|
|
2876
3282
|
}
|
|
2877
|
-
function
|
|
2878
|
-
|
|
3283
|
+
function extractBodyText(html) {
|
|
3284
|
+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
3285
|
+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
3286
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
3287
|
+
text = text.replace(/ /g, " ");
|
|
3288
|
+
text = text.replace(/&/g, "&");
|
|
3289
|
+
text = text.replace(/</g, "<");
|
|
3290
|
+
text = text.replace(/>/g, ">");
|
|
3291
|
+
text = text.replace(/"/g, '"');
|
|
3292
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
3293
|
+
return text;
|
|
2879
3294
|
}
|
|
2880
3295
|
|
|
2881
3296
|
// src/commands/alerts.ts
|
|
@@ -2900,7 +3315,7 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
2900
3315
|
return answer.toLowerCase().startsWith("y");
|
|
2901
3316
|
}
|
|
2902
3317
|
async function alerts(options) {
|
|
2903
|
-
console.log("\n\u{1F514}
|
|
3318
|
+
console.log("\n\u{1F514} RankCLI - Alert Configuration\n");
|
|
2904
3319
|
const rl = readline3.createInterface({
|
|
2905
3320
|
input: process.stdin,
|
|
2906
3321
|
output: process.stdout
|
|
@@ -3043,235 +3458,18 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
3043
3458
|
}
|
|
3044
3459
|
}
|
|
3045
3460
|
|
|
3046
|
-
// src/commands/auth.ts
|
|
3047
|
-
import chalk6 from "chalk";
|
|
3048
|
-
import ora5 from "ora";
|
|
3049
|
-
import inquirer from "inquirer";
|
|
3050
|
-
import open from "open";
|
|
3051
|
-
async function login(options) {
|
|
3052
|
-
if (isLoggedIn()) {
|
|
3053
|
-
const { email } = getUserInfo();
|
|
3054
|
-
console.log(chalk6.yellow(`Already logged in as ${email}`));
|
|
3055
|
-
console.log(chalk6.dim('Run "rankcli logout" to sign out first.'));
|
|
3056
|
-
return;
|
|
3057
|
-
}
|
|
3058
|
-
const spinner = ora5();
|
|
3059
|
-
try {
|
|
3060
|
-
if (options.token) {
|
|
3061
|
-
const apiKey = options.token;
|
|
3062
|
-
if (!apiKey.startsWith("rankcli_")) {
|
|
3063
|
-
console.log(chalk6.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
|
-
spinner.start("Validating API key...");
|
|
3067
|
-
const result = await validateApiKey(apiKey);
|
|
3068
|
-
if (!result.valid) {
|
|
3069
|
-
spinner.fail("Invalid API key");
|
|
3070
|
-
console.log(chalk6.red(`
|
|
3071
|
-
${result.error}`));
|
|
3072
|
-
return;
|
|
3073
|
-
}
|
|
3074
|
-
saveApiKey(apiKey);
|
|
3075
|
-
if (result.subscription) {
|
|
3076
|
-
saveSubscription({
|
|
3077
|
-
planId: result.subscription.planId,
|
|
3078
|
-
planName: result.subscription.planName,
|
|
3079
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3080
|
-
});
|
|
3081
|
-
}
|
|
3082
|
-
spinner.succeed(`Authenticated via API key`);
|
|
3083
|
-
console.log(chalk6.dim(`
|
|
3084
|
-
Email: ${result.user?.email}`));
|
|
3085
|
-
console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3086
|
-
return;
|
|
3087
|
-
}
|
|
3088
|
-
if (options.browser) {
|
|
3089
|
-
console.log(chalk6.cyan("\nOpening browser for login..."));
|
|
3090
|
-
console.log(chalk6.dim("Complete the login in your browser, then return here.\n"));
|
|
3091
|
-
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
3092
|
-
await open(loginUrl);
|
|
3093
|
-
const { token } = await inquirer.prompt([
|
|
3094
|
-
{
|
|
3095
|
-
type: "password",
|
|
3096
|
-
name: "token",
|
|
3097
|
-
message: "Paste your CLI token from the browser:",
|
|
3098
|
-
mask: "*"
|
|
3099
|
-
}
|
|
3100
|
-
]);
|
|
3101
|
-
spinner.start("Verifying token...");
|
|
3102
|
-
const supabase = getSupabaseClient();
|
|
3103
|
-
const { data, error } = await supabase.auth.getUser(token);
|
|
3104
|
-
if (error || !data.user) {
|
|
3105
|
-
spinner.fail("Invalid token");
|
|
3106
|
-
console.log(chalk6.red("The token is invalid or expired. Please try again."));
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
3110
|
-
access_token: token,
|
|
3111
|
-
refresh_token: ""
|
|
3112
|
-
// Will need to handle this differently
|
|
3113
|
-
});
|
|
3114
|
-
if (sessionError || !sessionData.session) {
|
|
3115
|
-
spinner.fail("Failed to establish session");
|
|
3116
|
-
return;
|
|
3117
|
-
}
|
|
3118
|
-
saveSession(sessionData.session);
|
|
3119
|
-
await fetchAndSaveSubscription();
|
|
3120
|
-
spinner.succeed(`Logged in as ${chalk6.green(data.user.email)}`);
|
|
3121
|
-
} else {
|
|
3122
|
-
let email = options.email;
|
|
3123
|
-
if (!email) {
|
|
3124
|
-
const answers = await inquirer.prompt([
|
|
3125
|
-
{
|
|
3126
|
-
type: "input",
|
|
3127
|
-
name: "email",
|
|
3128
|
-
message: "Email:",
|
|
3129
|
-
validate: (input) => {
|
|
3130
|
-
if (!input.includes("@")) return "Please enter a valid email";
|
|
3131
|
-
return true;
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
]);
|
|
3135
|
-
email = answers.email;
|
|
3136
|
-
}
|
|
3137
|
-
const { password } = await inquirer.prompt([
|
|
3138
|
-
{
|
|
3139
|
-
type: "password",
|
|
3140
|
-
name: "password",
|
|
3141
|
-
message: "Password:",
|
|
3142
|
-
mask: "*"
|
|
3143
|
-
}
|
|
3144
|
-
]);
|
|
3145
|
-
spinner.start("Signing in...");
|
|
3146
|
-
const supabase = getSupabaseClient();
|
|
3147
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
3148
|
-
email,
|
|
3149
|
-
password
|
|
3150
|
-
});
|
|
3151
|
-
if (error) {
|
|
3152
|
-
spinner.fail("Login failed");
|
|
3153
|
-
console.log(chalk6.red(`
|
|
3154
|
-
${error.message}`));
|
|
3155
|
-
if (error.message.includes("Invalid login")) {
|
|
3156
|
-
console.log(chalk6.dim("\nDon't have an account? Sign up at:"));
|
|
3157
|
-
console.log(chalk6.cyan(` ${WEB_APP_URL}/signup`));
|
|
3158
|
-
}
|
|
3159
|
-
return;
|
|
3160
|
-
}
|
|
3161
|
-
if (!data.session) {
|
|
3162
|
-
spinner.fail("No session returned");
|
|
3163
|
-
return;
|
|
3164
|
-
}
|
|
3165
|
-
saveSession(data.session);
|
|
3166
|
-
await fetchAndSaveSubscription();
|
|
3167
|
-
spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
|
|
3168
|
-
const { planName, sitesLimit } = getUserInfo();
|
|
3169
|
-
console.log(chalk6.dim(`
|
|
3170
|
-
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
3171
|
-
}
|
|
3172
|
-
} catch (err) {
|
|
3173
|
-
spinner.fail("Login failed");
|
|
3174
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
async function logout() {
|
|
3178
|
-
if (!isLoggedIn()) {
|
|
3179
|
-
console.log(chalk6.yellow("Not logged in"));
|
|
3180
|
-
return;
|
|
3181
|
-
}
|
|
3182
|
-
const { email } = getUserInfo();
|
|
3183
|
-
const spinner = ora5("Signing out...").start();
|
|
3184
|
-
try {
|
|
3185
|
-
const supabase = getSupabaseClient();
|
|
3186
|
-
await supabase.auth.signOut();
|
|
3187
|
-
clearCredentials();
|
|
3188
|
-
spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
|
|
3189
|
-
} catch (err) {
|
|
3190
|
-
spinner.fail("Logout failed");
|
|
3191
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3194
|
-
async function whoami() {
|
|
3195
|
-
if (!isLoggedIn()) {
|
|
3196
|
-
console.log(chalk6.yellow("Not logged in"));
|
|
3197
|
-
console.log(chalk6.dim('\nRun "rankcli login" to sign in.'));
|
|
3198
|
-
console.log(chalk6.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
3199
|
-
return;
|
|
3200
|
-
}
|
|
3201
|
-
const spinner = ora5("Checking account...").start();
|
|
3202
|
-
try {
|
|
3203
|
-
if (isApiKeyAuth()) {
|
|
3204
|
-
const apiKey = getApiKey();
|
|
3205
|
-
if (apiKey) {
|
|
3206
|
-
const result = await validateApiKey(apiKey);
|
|
3207
|
-
if (result.valid && result.subscription) {
|
|
3208
|
-
saveSubscription({
|
|
3209
|
-
planId: result.subscription.planId,
|
|
3210
|
-
planName: result.subscription.planName,
|
|
3211
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3212
|
-
});
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
} else {
|
|
3216
|
-
await fetchAndSaveSubscription();
|
|
3217
|
-
}
|
|
3218
|
-
const { email, planName, sitesLimit } = getUserInfo();
|
|
3219
|
-
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
3220
|
-
spinner.stop();
|
|
3221
|
-
console.log(chalk6.bold("\nRankCLI Account"));
|
|
3222
|
-
console.log("\u2500".repeat(40));
|
|
3223
|
-
console.log(` Email: ${chalk6.cyan(email || "N/A")}`);
|
|
3224
|
-
console.log(` Plan: ${chalk6.green(planName)}`);
|
|
3225
|
-
console.log(` Sites: ${sitesLimit} allowed`);
|
|
3226
|
-
console.log(` Auth: ${chalk6.dim(authMethod)}`);
|
|
3227
|
-
console.log("\u2500".repeat(40));
|
|
3228
|
-
console.log(chalk6.dim(`
|
|
3229
|
-
Manage at: ${WEB_APP_URL}/account`));
|
|
3230
|
-
} catch (err) {
|
|
3231
|
-
spinner.fail("Failed to fetch account info");
|
|
3232
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
async function fetchAndSaveSubscription() {
|
|
3236
|
-
const supabase = await getAuthenticatedClient();
|
|
3237
|
-
if (!supabase) return;
|
|
3238
|
-
const { userId } = getUserInfo();
|
|
3239
|
-
if (!userId) return;
|
|
3240
|
-
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
3241
|
-
plan:subscription_plans (
|
|
3242
|
-
plan_id,
|
|
3243
|
-
name,
|
|
3244
|
-
sites_limit
|
|
3245
|
-
)
|
|
3246
|
-
`).eq("user_id", userId).single();
|
|
3247
|
-
if (subscription?.plan) {
|
|
3248
|
-
const plan = subscription.plan;
|
|
3249
|
-
saveSubscription({
|
|
3250
|
-
planId: plan.plan_id,
|
|
3251
|
-
planName: plan.name,
|
|
3252
|
-
sitesLimit: plan.sites_limit
|
|
3253
|
-
});
|
|
3254
|
-
} else {
|
|
3255
|
-
saveSubscription({
|
|
3256
|
-
planId: void 0,
|
|
3257
|
-
planName: "Free",
|
|
3258
|
-
sitesLimit: 1
|
|
3259
|
-
});
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
3461
|
// src/version.ts
|
|
3264
|
-
var VERSION = "0.0.
|
|
3462
|
+
var VERSION = "0.0.25";
|
|
3265
3463
|
|
|
3266
3464
|
// src/index.ts
|
|
3267
3465
|
var program = new Command();
|
|
3268
3466
|
program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version(VERSION);
|
|
3269
|
-
program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "
|
|
3467
|
+
program.command("login").description("Log in to your RankCLI account (opens your browser by default)").option("-e, --email <email>", "Log in with email/password instead of the browser").option("--no-browser", "Skip the browser and log in with email/password interactively").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
|
|
3270
3468
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3271
3469
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3272
|
-
program.command("init").description("
|
|
3470
|
+
program.command("init").description("Set up RankCLI in your project (detects framework, git/GitHub, AI keys)").option("-y, --yes", "Skip prompts and use defaults").action(init);
|
|
3273
3471
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
3274
|
-
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
|
|
3472
|
+
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, anthropic, openrouter, groq, or gemini (auto-detected from env if omitted)").option("--ai-key <key>", "API key (or set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY)").action(audit);
|
|
3275
3473
|
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);
|
|
3276
3474
|
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) => {
|
|
3277
3475
|
const result = await fix({
|