@rankcli/cli 0.0.25 → 0.0.27
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 +1159 -1001
- package/dist/index.mjs +1118 -960
- package/package.json +2 -2
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";
|
|
@@ -467,35 +220,833 @@ async function getAuthenticatedClient() {
|
|
|
467
220
|
return supabase;
|
|
468
221
|
}
|
|
469
222
|
|
|
470
|
-
// src/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
},
|
|
1049
|
+
agency: {
|
|
499
1050
|
checks: 280,
|
|
500
1051
|
sites: 50,
|
|
501
1052
|
domains_per_month: null,
|
|
@@ -507,133 +1058,133 @@ var UPSELL_MESSAGES = {
|
|
|
507
1058
|
// After free audit completes
|
|
508
1059
|
free_audit_complete: () => [
|
|
509
1060
|
"",
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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."),
|
|
513
1064
|
"",
|
|
514
|
-
|
|
515
|
-
|
|
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"),
|
|
516
1067
|
""
|
|
517
1068
|
],
|
|
518
1069
|
// When checks limit is reached
|
|
519
1070
|
checks_limit: (ctx) => [
|
|
520
1071
|
"",
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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`),
|
|
524
1075
|
""
|
|
525
1076
|
],
|
|
526
1077
|
// When domain limit is reached
|
|
527
1078
|
domain_limit: (ctx) => [
|
|
528
1079
|
"",
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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.`),
|
|
532
1083
|
"",
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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"),
|
|
536
1087
|
""
|
|
537
1088
|
],
|
|
538
1089
|
// When GEO features are limited
|
|
539
1090
|
geo_limited: (ctx) => [
|
|
540
1091
|
"",
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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"),
|
|
545
1096
|
"",
|
|
546
|
-
|
|
547
|
-
|
|
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"),
|
|
548
1099
|
""
|
|
549
1100
|
],
|
|
550
1101
|
// When auto-fix is attempted without access
|
|
551
1102
|
auto_fix_blocked: () => [
|
|
552
1103
|
"",
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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."),
|
|
556
1107
|
"",
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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"),
|
|
561
1112
|
"",
|
|
562
|
-
|
|
563
|
-
|
|
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"),
|
|
564
1115
|
""
|
|
565
1116
|
],
|
|
566
1117
|
// When GitHub integration is attempted without access
|
|
567
1118
|
github_blocked: () => [
|
|
568
1119
|
"",
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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."),
|
|
572
1123
|
"",
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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"),
|
|
578
1129
|
"",
|
|
579
|
-
|
|
580
|
-
|
|
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"),
|
|
581
1132
|
""
|
|
582
1133
|
],
|
|
583
1134
|
// Keyword tracking limit reached
|
|
584
1135
|
keyword_limit: (ctx) => [
|
|
585
1136
|
"",
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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}`),
|
|
590
1141
|
"",
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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"),
|
|
595
1146
|
"",
|
|
596
|
-
|
|
597
|
-
|
|
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"),
|
|
598
1149
|
""
|
|
599
1150
|
],
|
|
600
1151
|
// Rate limit exceeded
|
|
601
1152
|
rate_limit: (ctx) => [
|
|
602
1153
|
"",
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.`),
|
|
606
1157
|
"",
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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"),
|
|
611
1162
|
"",
|
|
612
|
-
|
|
613
|
-
|
|
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"),
|
|
614
1165
|
""
|
|
615
1166
|
],
|
|
616
1167
|
// Schedule exceeded (for GitHub Action)
|
|
617
1168
|
schedule_exceeded: (ctx) => [
|
|
618
1169
|
"",
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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.`),
|
|
622
1173
|
"",
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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"),
|
|
627
1178
|
"",
|
|
628
|
-
|
|
629
|
-
|
|
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"),
|
|
630
1181
|
""
|
|
631
1182
|
],
|
|
632
1183
|
// Login prompt (soft)
|
|
633
1184
|
login_prompt: () => [
|
|
634
1185
|
"",
|
|
635
|
-
|
|
636
|
-
|
|
1186
|
+
chalk3.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
|
|
1187
|
+
chalk3.dim(" rankcli login"),
|
|
637
1188
|
""
|
|
638
1189
|
]
|
|
639
1190
|
};
|
|
@@ -674,9 +1225,16 @@ async function fetchHtml(url, timeout = 1e4) {
|
|
|
674
1225
|
return null;
|
|
675
1226
|
}
|
|
676
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
|
+
};
|
|
677
1234
|
async function runAIAnalysis(url, issues, provider, apiKey) {
|
|
678
1235
|
const isAnthropic = provider === "anthropic";
|
|
679
|
-
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`;
|
|
680
1238
|
const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
|
|
681
1239
|
|
|
682
1240
|
URL: ${url}
|
|
@@ -707,19 +1265,26 @@ Return JSON with:
|
|
|
707
1265
|
});
|
|
708
1266
|
} else {
|
|
709
1267
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1268
|
+
if (provider === "openrouter") {
|
|
1269
|
+
headers["HTTP-Referer"] = "https://rankcli.dev";
|
|
1270
|
+
headers["X-Title"] = "RankCLI";
|
|
1271
|
+
}
|
|
710
1272
|
body = JSON.stringify({
|
|
711
|
-
model:
|
|
1273
|
+
model: providerConfig.model,
|
|
712
1274
|
messages: [
|
|
713
1275
|
{ role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
|
|
714
1276
|
{ role: "user", content: prompt }
|
|
715
1277
|
],
|
|
716
1278
|
max_tokens: 1500,
|
|
717
|
-
|
|
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" } } : {}
|
|
718
1283
|
});
|
|
719
1284
|
}
|
|
720
1285
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
721
1286
|
if (!response.ok) {
|
|
722
|
-
console.error(
|
|
1287
|
+
console.error(chalk4.yellow(`AI analysis failed: ${response.status}`));
|
|
723
1288
|
return null;
|
|
724
1289
|
}
|
|
725
1290
|
const data = await response.json();
|
|
@@ -729,7 +1294,7 @@ Return JSON with:
|
|
|
729
1294
|
if (!jsonMatch) return null;
|
|
730
1295
|
return JSON.parse(jsonMatch[0]);
|
|
731
1296
|
} catch (error) {
|
|
732
|
-
console.error(
|
|
1297
|
+
console.error(chalk4.yellow(`AI analysis error: ${error}`));
|
|
733
1298
|
return null;
|
|
734
1299
|
}
|
|
735
1300
|
}
|
|
@@ -755,7 +1320,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
755
1320
|
if (!userId) return null;
|
|
756
1321
|
try {
|
|
757
1322
|
const parsedUrl = new URL(url);
|
|
758
|
-
const domain = parsedUrl.hostname;
|
|
1323
|
+
const domain = parsedUrl.port ? `${parsedUrl.hostname}:${parsedUrl.port}` : parsedUrl.hostname;
|
|
759
1324
|
const { data: existingProject } = await supabase.from("projects").select("id").eq("user_id", userId).eq("domain", domain).eq("is_active", true).single();
|
|
760
1325
|
let projectId;
|
|
761
1326
|
if (existingProject) {
|
|
@@ -764,9 +1329,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
764
1329
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
765
1330
|
const limit = sitesLimit ?? 1;
|
|
766
1331
|
if (count !== null && count >= limit) {
|
|
767
|
-
console.log(
|
|
1332
|
+
console.log(chalk4.yellow(`
|
|
768
1333
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
769
|
-
console.log(
|
|
1334
|
+
console.log(chalk4.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
770
1335
|
return null;
|
|
771
1336
|
}
|
|
772
1337
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -776,7 +1341,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
776
1341
|
domain
|
|
777
1342
|
}).select("id").single();
|
|
778
1343
|
if (projectError || !newProject) {
|
|
779
|
-
console.error(
|
|
1344
|
+
console.error(chalk4.dim("Failed to create project"));
|
|
780
1345
|
return null;
|
|
781
1346
|
}
|
|
782
1347
|
projectId = newProject.id;
|
|
@@ -817,7 +1382,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
817
1382
|
ai_analysis: aiAnalysis
|
|
818
1383
|
}).select("id").single();
|
|
819
1384
|
if (auditError || !audit2) {
|
|
820
|
-
console.error(
|
|
1385
|
+
console.error(chalk4.dim("Failed to save audit"));
|
|
821
1386
|
return null;
|
|
822
1387
|
}
|
|
823
1388
|
if (mergedIssues.length > 0) {
|
|
@@ -845,7 +1410,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
845
1410
|
}).eq("id", projectId);
|
|
846
1411
|
return { projectId, auditId: audit2.id };
|
|
847
1412
|
} catch (error) {
|
|
848
|
-
console.error(
|
|
1413
|
+
console.error(chalk4.dim("Cloud sync failed"));
|
|
849
1414
|
return null;
|
|
850
1415
|
}
|
|
851
1416
|
}
|
|
@@ -853,8 +1418,16 @@ async function audit(options) {
|
|
|
853
1418
|
const url = options.url;
|
|
854
1419
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
855
1420
|
const enableAI = options.ai || false;
|
|
856
|
-
const
|
|
857
|
-
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] || "";
|
|
858
1431
|
const loggedIn = isLoggedIn();
|
|
859
1432
|
const { planId } = getUserInfo();
|
|
860
1433
|
const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
|
|
@@ -867,7 +1440,7 @@ async function audit(options) {
|
|
|
867
1440
|
await initEngine(passphrase);
|
|
868
1441
|
premiumChecks = await getPremiumChecks();
|
|
869
1442
|
} catch (error) {
|
|
870
|
-
console.error(
|
|
1443
|
+
console.error(chalk4.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
|
|
871
1444
|
}
|
|
872
1445
|
}
|
|
873
1446
|
}
|
|
@@ -877,12 +1450,12 @@ async function audit(options) {
|
|
|
877
1450
|
console.log("\nOptions:");
|
|
878
1451
|
console.log(" --max-pages <n> Max pages to crawl (default: 5)");
|
|
879
1452
|
console.log(" --ai Enable AI analysis");
|
|
880
|
-
console.log(" --ai-provider <p> AI provider: openai or
|
|
881
|
-
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)");
|
|
882
1455
|
console.log(" --output json Output as JSON");
|
|
883
1456
|
if (!loggedIn) {
|
|
884
|
-
console.log(
|
|
885
|
-
console.log(
|
|
1457
|
+
console.log(chalk4.dim("\nTip: Log in to sync results to the web dashboard:"));
|
|
1458
|
+
console.log(chalk4.dim(" rankcli login"));
|
|
886
1459
|
}
|
|
887
1460
|
console.log("");
|
|
888
1461
|
return;
|
|
@@ -895,7 +1468,7 @@ async function audit(options) {
|
|
|
895
1468
|
}
|
|
896
1469
|
const startTime = Date.now();
|
|
897
1470
|
try {
|
|
898
|
-
const discoverySpinner =
|
|
1471
|
+
const discoverySpinner = ora2("Discovering pages...").start();
|
|
899
1472
|
let pagesToAudit = [url];
|
|
900
1473
|
const homepageHtml = await fetchHtml(url, 1e4);
|
|
901
1474
|
let detectedFramework = detectFramework2({});
|
|
@@ -909,7 +1482,7 @@ async function audit(options) {
|
|
|
909
1482
|
} else {
|
|
910
1483
|
discoverySpinner.warn("Could not fetch homepage, auditing URL only");
|
|
911
1484
|
}
|
|
912
|
-
const auditSpinner =
|
|
1485
|
+
const auditSpinner = ora2(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
|
|
913
1486
|
const auditPage = async (pageUrl, index) => {
|
|
914
1487
|
try {
|
|
915
1488
|
const report = await runFullAudit({
|
|
@@ -956,7 +1529,7 @@ async function audit(options) {
|
|
|
956
1529
|
const mergedIssues = mergeIssues(allIssues);
|
|
957
1530
|
let aiAnalysis = null;
|
|
958
1531
|
if (enableAI && aiKey) {
|
|
959
|
-
const aiSpinner =
|
|
1532
|
+
const aiSpinner = ora2(`Running AI analysis (${aiProvider})...`).start();
|
|
960
1533
|
aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
|
|
961
1534
|
if (aiAnalysis) {
|
|
962
1535
|
aiSpinner.succeed("AI analysis complete");
|
|
@@ -964,8 +1537,8 @@ async function audit(options) {
|
|
|
964
1537
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
965
1538
|
}
|
|
966
1539
|
} else if (enableAI && !aiKey) {
|
|
967
|
-
console.log(
|
|
968
|
-
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"));
|
|
969
1542
|
}
|
|
970
1543
|
const totalTime = Date.now() - startTime;
|
|
971
1544
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
@@ -993,27 +1566,27 @@ async function audit(options) {
|
|
|
993
1566
|
};
|
|
994
1567
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
995
1568
|
} else {
|
|
996
|
-
console.log("\n" +
|
|
997
|
-
console.log(
|
|
998
|
-
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)));
|
|
999
1572
|
const score = primaryReport.healthScore?.overall || 0;
|
|
1000
|
-
const scoreColor = score >= 80 ?
|
|
1573
|
+
const scoreColor = score >= 80 ? chalk4.green : score >= 60 ? chalk4.yellow : chalk4.red;
|
|
1001
1574
|
console.log(`
|
|
1002
|
-
${
|
|
1003
|
-
console.log(` ${
|
|
1004
|
-
console.log(` ${
|
|
1005
|
-
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`);
|
|
1006
1579
|
if (frameworkName) {
|
|
1007
|
-
console.log(` ${
|
|
1580
|
+
console.log(` ${chalk4.bold("Framework:")} ${chalk4.cyan(frameworkName)}`);
|
|
1008
1581
|
}
|
|
1009
1582
|
if (primaryReport.healthScore) {
|
|
1010
|
-
console.log("\n" +
|
|
1583
|
+
console.log("\n" + chalk4.bold(" Health Scores:"));
|
|
1011
1584
|
const hs = primaryReport.healthScore;
|
|
1012
1585
|
const bar = (score2) => {
|
|
1013
1586
|
const filled = Math.round(score2 / 5);
|
|
1014
1587
|
const empty = 20 - filled;
|
|
1015
|
-
const color = score2 >= 80 ?
|
|
1016
|
-
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));
|
|
1017
1590
|
};
|
|
1018
1591
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
1019
1592
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
@@ -1026,35 +1599,35 @@ async function audit(options) {
|
|
|
1026
1599
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
1027
1600
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
1028
1601
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
1029
|
-
console.log("\n" +
|
|
1030
|
-
console.log(` ${
|
|
1031
|
-
console.log(` ${
|
|
1032
|
-
console.log(` ${
|
|
1033
|
-
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:"));
|
|
1034
1607
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
1035
1608
|
topIssues.forEach((issue, i) => {
|
|
1036
|
-
const icon = issue.severity === "error" ?
|
|
1609
|
+
const icon = issue.severity === "error" ? chalk4.red("\u2717") : issue.severity === "warning" ? chalk4.yellow("!") : chalk4.blue("i");
|
|
1037
1610
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
1038
1611
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
1039
|
-
console.log(
|
|
1612
|
+
console.log(chalk4.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
1040
1613
|
}
|
|
1041
1614
|
});
|
|
1042
1615
|
if (aiAnalysis) {
|
|
1043
|
-
console.log("\n" +
|
|
1616
|
+
console.log("\n" + chalk4.bold(" AI Analysis:"));
|
|
1044
1617
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
1045
|
-
console.log("\n " +
|
|
1618
|
+
console.log("\n " + chalk4.bold("Suggestions:"));
|
|
1046
1619
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
1047
1620
|
console.log(` ${i + 1}. ${s}`);
|
|
1048
1621
|
});
|
|
1049
1622
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
1050
|
-
console.log("\n " +
|
|
1623
|
+
console.log("\n " + chalk4.bold("Priority Fixes:"));
|
|
1051
1624
|
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1052
1625
|
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1053
|
-
console.log(
|
|
1626
|
+
console.log(chalk4.dim(` ${fix2.rationale}`));
|
|
1054
1627
|
});
|
|
1055
1628
|
}
|
|
1056
1629
|
}
|
|
1057
|
-
console.log("\n" +
|
|
1630
|
+
console.log("\n" + chalk4.bold("\u2550".repeat(60)));
|
|
1058
1631
|
const userInfo = getUserInfo();
|
|
1059
1632
|
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1060
1633
|
const isFreeTier = !loggedIn || tier === "free";
|
|
@@ -1070,13 +1643,13 @@ async function audit(options) {
|
|
|
1070
1643
|
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1071
1644
|
}
|
|
1072
1645
|
if (loggedIn) {
|
|
1073
|
-
const syncSpinner =
|
|
1646
|
+
const syncSpinner = ora2("Syncing to cloud...").start();
|
|
1074
1647
|
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
1075
1648
|
if (syncResult) {
|
|
1076
1649
|
syncSpinner.succeed("Results synced to cloud");
|
|
1077
|
-
console.log(
|
|
1650
|
+
console.log(chalk4.cyan(`
|
|
1078
1651
|
View and manage issues in the web dashboard:`));
|
|
1079
|
-
console.log(
|
|
1652
|
+
console.log(chalk4.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
1080
1653
|
`));
|
|
1081
1654
|
} else {
|
|
1082
1655
|
syncSpinner.warn("Could not sync to cloud");
|
|
@@ -1093,8 +1666,8 @@ async function audit(options) {
|
|
|
1093
1666
|
}
|
|
1094
1667
|
|
|
1095
1668
|
// src/commands/apply.ts
|
|
1096
|
-
import { existsSync as
|
|
1097
|
-
import { join as
|
|
1669
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1670
|
+
import { join as join4 } from "path";
|
|
1098
1671
|
import {
|
|
1099
1672
|
runAuditWithFixes,
|
|
1100
1673
|
generateFixes,
|
|
@@ -1128,11 +1701,11 @@ function truncate(str, maxLen) {
|
|
|
1128
1701
|
}
|
|
1129
1702
|
async function apply(options) {
|
|
1130
1703
|
const cwd = process.cwd();
|
|
1131
|
-
const configPath =
|
|
1704
|
+
const configPath = join4(cwd, "rankcli.config.json");
|
|
1132
1705
|
let config2 = null;
|
|
1133
|
-
if (
|
|
1706
|
+
if (existsSync4(configPath)) {
|
|
1134
1707
|
try {
|
|
1135
|
-
config2 = JSON.parse(
|
|
1708
|
+
config2 = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
1136
1709
|
} catch {
|
|
1137
1710
|
}
|
|
1138
1711
|
}
|
|
@@ -1142,8 +1715,8 @@ async function apply(options) {
|
|
|
1142
1715
|
}
|
|
1143
1716
|
if (!url) {
|
|
1144
1717
|
console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
|
|
1145
|
-
console.log("
|
|
1146
|
-
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");
|
|
1147
1720
|
return;
|
|
1148
1721
|
}
|
|
1149
1722
|
try {
|
|
@@ -1152,7 +1725,7 @@ async function apply(options) {
|
|
|
1152
1725
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1153
1726
|
return;
|
|
1154
1727
|
}
|
|
1155
|
-
console.log("\n\u{1F680}
|
|
1728
|
+
console.log("\n\u{1F680} RankCLI - Apply Fixes");
|
|
1156
1729
|
console.log("\u2550".repeat(50));
|
|
1157
1730
|
const loggedIn = isLoggedIn();
|
|
1158
1731
|
const userInfo = getUserInfo();
|
|
@@ -1264,7 +1837,7 @@ async function apply(options) {
|
|
|
1264
1837
|
}
|
|
1265
1838
|
if (config2 && !config2.url) {
|
|
1266
1839
|
config2.url = url;
|
|
1267
|
-
|
|
1840
|
+
writeFileSync3(configPath, JSON.stringify(config2, null, 2));
|
|
1268
1841
|
console.log("\n\u{1F4BE} Updated config with URL.");
|
|
1269
1842
|
}
|
|
1270
1843
|
console.log("\n" + "\u2500".repeat(50));
|
|
@@ -1275,7 +1848,7 @@ async function apply(options) {
|
|
|
1275
1848
|
console.log("\n2. Commit the SEO improvements:");
|
|
1276
1849
|
console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
|
|
1277
1850
|
console.log("\n3. Re-run the audit to verify:");
|
|
1278
|
-
console.log(`
|
|
1851
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1279
1852
|
console.log("");
|
|
1280
1853
|
}
|
|
1281
1854
|
|
|
@@ -1288,8 +1861,8 @@ import {
|
|
|
1288
1861
|
applyFixes as applyFixes2,
|
|
1289
1862
|
detectFramework as detectFramework4
|
|
1290
1863
|
} from "@rankcli/agent-runtime";
|
|
1291
|
-
import
|
|
1292
|
-
import
|
|
1864
|
+
import ora3 from "ora";
|
|
1865
|
+
import chalk5 from "chalk";
|
|
1293
1866
|
var execAsync = promisify(exec);
|
|
1294
1867
|
var SEVERITY_ICON2 = {
|
|
1295
1868
|
critical: "\u274C",
|
|
@@ -1356,19 +1929,19 @@ function printFix2(fix2, index) {
|
|
|
1356
1929
|
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1357
1930
|
console.log(`
|
|
1358
1931
|
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1359
|
-
console.log(` Code: ${
|
|
1360
|
-
console.log(` File: ${
|
|
1932
|
+
console.log(` Code: ${chalk5.dim(fix2.issue.code)}`);
|
|
1933
|
+
console.log(` File: ${chalk5.cyan(fix2.file)}`);
|
|
1361
1934
|
if (fix2.skipped) {
|
|
1362
|
-
console.log(` Status: ${
|
|
1935
|
+
console.log(` Status: ${chalk5.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1363
1936
|
} else if (fix2.applied) {
|
|
1364
|
-
console.log(` Status: ${
|
|
1365
|
-
console.log(` ${
|
|
1937
|
+
console.log(` Status: ${chalk5.green("APPLIED")}`);
|
|
1938
|
+
console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
|
|
1366
1939
|
} else {
|
|
1367
|
-
console.log(` ${
|
|
1940
|
+
console.log(` ${chalk5.dim("Explanation:")} ${fix2.explanation}`);
|
|
1368
1941
|
if (fix2.before) {
|
|
1369
|
-
console.log(` ${
|
|
1942
|
+
console.log(` ${chalk5.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1370
1943
|
}
|
|
1371
|
-
console.log(` ${
|
|
1944
|
+
console.log(` ${chalk5.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1372
1945
|
}
|
|
1373
1946
|
}
|
|
1374
1947
|
async function fix(options) {
|
|
@@ -1412,11 +1985,11 @@ async function fix(options) {
|
|
|
1412
1985
|
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1413
1986
|
console.log("\u2550".repeat(50));
|
|
1414
1987
|
}
|
|
1415
|
-
const frameworkSpinner = options.output !== "json" ?
|
|
1988
|
+
const frameworkSpinner = options.output !== "json" ? ora3("Detecting framework...").start() : null;
|
|
1416
1989
|
const frameworkResult = await detectFramework4({ cwd });
|
|
1417
1990
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1418
1991
|
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1419
|
-
const analyzeSpinner = options.output !== "json" ?
|
|
1992
|
+
const analyzeSpinner = options.output !== "json" ? ora3(`Analyzing ${url}...`).start() : null;
|
|
1420
1993
|
let issues;
|
|
1421
1994
|
let score;
|
|
1422
1995
|
try {
|
|
@@ -1433,7 +2006,7 @@ async function fix(options) {
|
|
|
1433
2006
|
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1434
2007
|
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1435
2008
|
if (options.output !== "json") {
|
|
1436
|
-
console.log(
|
|
2009
|
+
console.log(chalk5.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1437
2010
|
}
|
|
1438
2011
|
}
|
|
1439
2012
|
if (issues.length === 0) {
|
|
@@ -1450,7 +2023,7 @@ async function fix(options) {
|
|
|
1450
2023
|
durationMs: Date.now() - startTime
|
|
1451
2024
|
};
|
|
1452
2025
|
}
|
|
1453
|
-
const fixSpinner = options.output !== "json" ?
|
|
2026
|
+
const fixSpinner = options.output !== "json" ? ora3("Generating fixes...").start() : null;
|
|
1454
2027
|
const fixes = await generateFixes2(issues, {
|
|
1455
2028
|
cwd,
|
|
1456
2029
|
url,
|
|
@@ -1462,13 +2035,13 @@ async function fix(options) {
|
|
|
1462
2035
|
if (options.dryRun) {
|
|
1463
2036
|
if (options.output !== "json") {
|
|
1464
2037
|
console.log("\n" + "\u2500".repeat(50));
|
|
1465
|
-
console.log(
|
|
2038
|
+
console.log(chalk5.yellow("DRY RUN - No changes will be made"));
|
|
1466
2039
|
console.log("\u2500".repeat(50));
|
|
1467
2040
|
for (let i = 0; i < fixes.length; i++) {
|
|
1468
2041
|
printFix2(fixes[i], i);
|
|
1469
2042
|
}
|
|
1470
2043
|
console.log("\n" + "\u2500".repeat(50));
|
|
1471
|
-
console.log(`\u{1F4CB} ${
|
|
2044
|
+
console.log(`\u{1F4CB} ${chalk5.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1472
2045
|
console.log(` Remove --dry-run to apply these fixes.
|
|
1473
2046
|
`);
|
|
1474
2047
|
}
|
|
@@ -1534,7 +2107,7 @@ async function fix(options) {
|
|
|
1534
2107
|
durationMs: Date.now() - startTime
|
|
1535
2108
|
};
|
|
1536
2109
|
}
|
|
1537
|
-
const applySpinner = options.output !== "json" ?
|
|
2110
|
+
const applySpinner = options.output !== "json" ? ora3("Applying fixes...").start() : null;
|
|
1538
2111
|
const { applied, skipped } = await applyFixes2(fixes, { cwd, dryRun: false });
|
|
1539
2112
|
for (const fix2 of applied) {
|
|
1540
2113
|
fix2.applied = true;
|
|
@@ -1546,7 +2119,7 @@ async function fix(options) {
|
|
|
1546
2119
|
let commitError;
|
|
1547
2120
|
let pushError;
|
|
1548
2121
|
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1549
|
-
const gitSpinner = options.output !== "json" ?
|
|
2122
|
+
const gitSpinner = options.output !== "json" ? ora3("Committing changes...").start() : null;
|
|
1550
2123
|
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1551
2124
|
committed = gitResult.committed;
|
|
1552
2125
|
pushed = gitResult.pushed;
|
|
@@ -1572,23 +2145,23 @@ async function fix(options) {
|
|
|
1572
2145
|
console.log("\u2550".repeat(50));
|
|
1573
2146
|
if (applied.length > 0) {
|
|
1574
2147
|
console.log(`
|
|
1575
|
-
${
|
|
2148
|
+
${chalk5.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1576
2149
|
for (const fix2 of applied) {
|
|
1577
2150
|
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1578
2151
|
}
|
|
1579
2152
|
}
|
|
1580
2153
|
if (skipped.length > 0) {
|
|
1581
2154
|
console.log(`
|
|
1582
|
-
${
|
|
2155
|
+
${chalk5.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1583
2156
|
for (const fix2 of skipped) {
|
|
1584
2157
|
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1585
2158
|
}
|
|
1586
2159
|
}
|
|
1587
2160
|
if (committed) {
|
|
1588
2161
|
console.log(`
|
|
1589
|
-
${
|
|
2162
|
+
${chalk5.green("\u2705")} Changes committed`);
|
|
1590
2163
|
if (pushed) {
|
|
1591
|
-
console.log(`${
|
|
2164
|
+
console.log(`${chalk5.green("\u2705")} Changes pushed`);
|
|
1592
2165
|
}
|
|
1593
2166
|
}
|
|
1594
2167
|
if (!committed && applied.length > 0) {
|
|
@@ -1641,8 +2214,8 @@ ${chalk3.green("\u2705")} Changes committed`);
|
|
|
1641
2214
|
}
|
|
1642
2215
|
|
|
1643
2216
|
// src/commands/track.ts
|
|
1644
|
-
import
|
|
1645
|
-
import
|
|
2217
|
+
import ora4 from "ora";
|
|
2218
|
+
import chalk6 from "chalk";
|
|
1646
2219
|
import { table } from "table";
|
|
1647
2220
|
import { RankTracker, TIER_LIMITS as TIER_LIMITS2 } from "@rankcli/agent-runtime";
|
|
1648
2221
|
async function getProjectForUrl(url) {
|
|
@@ -1681,35 +2254,35 @@ function getTierLimits(tier) {
|
|
|
1681
2254
|
};
|
|
1682
2255
|
}
|
|
1683
2256
|
function formatPosition(pos) {
|
|
1684
|
-
if (pos === null) return
|
|
1685
|
-
if (pos <= 3) return
|
|
1686
|
-
if (pos <= 10) return
|
|
1687
|
-
if (pos <= 20) return
|
|
1688
|
-
if (pos <= 50) return
|
|
1689
|
-
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}`);
|
|
1690
2263
|
}
|
|
1691
2264
|
function formatChange(change) {
|
|
1692
|
-
if (change === 0) return
|
|
1693
|
-
if (change > 0) return
|
|
1694
|
-
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)}`);
|
|
1695
2268
|
}
|
|
1696
2269
|
async function trackAdd(options) {
|
|
1697
2270
|
if (!options.keywords || options.keywords.length === 0) {
|
|
1698
|
-
console.log(
|
|
2271
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1699
2272
|
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1700
2273
|
return { success: false, error: "No keywords provided" };
|
|
1701
2274
|
}
|
|
1702
2275
|
if (!options.url) {
|
|
1703
|
-
console.log(
|
|
2276
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1704
2277
|
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1705
2278
|
return { success: false, error: "No URL provided" };
|
|
1706
2279
|
}
|
|
1707
2280
|
if (!isLoggedIn()) {
|
|
1708
|
-
console.log(
|
|
2281
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1709
2282
|
console.log(" rankcli login\n");
|
|
1710
2283
|
return { success: false, error: "Not logged in" };
|
|
1711
2284
|
}
|
|
1712
|
-
const spinner =
|
|
2285
|
+
const spinner = ora4("Adding keywords to track...").start();
|
|
1713
2286
|
try {
|
|
1714
2287
|
const project = await getProjectForUrl(options.url);
|
|
1715
2288
|
if (!project) {
|
|
@@ -1750,10 +2323,10 @@ async function trackAdd(options) {
|
|
|
1750
2323
|
}
|
|
1751
2324
|
const addedCount = data?.length || 0;
|
|
1752
2325
|
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1753
|
-
console.log(
|
|
2326
|
+
console.log(chalk6.dim(`
|
|
1754
2327
|
Tracking: ${options.keywords.join(", ")}`));
|
|
1755
|
-
console.log(
|
|
1756
|
-
console.log(
|
|
2328
|
+
console.log(chalk6.dim(`For: ${project.domain}`));
|
|
2329
|
+
console.log(chalk6.cyan(`
|
|
1757
2330
|
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1758
2331
|
`));
|
|
1759
2332
|
return { success: true, addedCount };
|
|
@@ -1764,16 +2337,16 @@ Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
|
1764
2337
|
}
|
|
1765
2338
|
async function trackStatus(options) {
|
|
1766
2339
|
if (!options.url) {
|
|
1767
|
-
console.log(
|
|
2340
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1768
2341
|
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1769
2342
|
return { success: false, error: "No URL provided" };
|
|
1770
2343
|
}
|
|
1771
2344
|
if (!isLoggedIn()) {
|
|
1772
|
-
console.log(
|
|
2345
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1773
2346
|
console.log(" rankcli login\n");
|
|
1774
2347
|
return { success: false, error: "Not logged in" };
|
|
1775
2348
|
}
|
|
1776
|
-
const spinner =
|
|
2349
|
+
const spinner = ora4("Loading keyword rankings...").start();
|
|
1777
2350
|
try {
|
|
1778
2351
|
const project = await getProjectForUrl(options.url);
|
|
1779
2352
|
if (!project) {
|
|
@@ -1792,8 +2365,8 @@ async function trackStatus(options) {
|
|
|
1792
2365
|
}
|
|
1793
2366
|
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1794
2367
|
if (!data || data.length === 0) {
|
|
1795
|
-
console.log(
|
|
1796
|
-
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.
|
|
1797
2370
|
`));
|
|
1798
2371
|
return { success: true, keywords: [] };
|
|
1799
2372
|
}
|
|
@@ -1808,7 +2381,7 @@ async function trackStatus(options) {
|
|
|
1808
2381
|
if (options.output === "json") {
|
|
1809
2382
|
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1810
2383
|
} else {
|
|
1811
|
-
console.log("\n" +
|
|
2384
|
+
console.log("\n" + chalk6.bold("\u{1F4CA} Keyword Rankings"));
|
|
1812
2385
|
console.log("\u2550".repeat(60));
|
|
1813
2386
|
const tableData = [
|
|
1814
2387
|
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
@@ -1851,14 +2424,14 @@ async function trackHistory(options) {
|
|
|
1851
2424
|
return { success: false, error: "No URL provided" };
|
|
1852
2425
|
}
|
|
1853
2426
|
if (!options.keyword) {
|
|
1854
|
-
console.log(
|
|
2427
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
1855
2428
|
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
1856
2429
|
return { success: false, error: "No keyword provided" };
|
|
1857
2430
|
}
|
|
1858
2431
|
if (!isLoggedIn()) {
|
|
1859
2432
|
return { success: false, error: "Not logged in" };
|
|
1860
2433
|
}
|
|
1861
|
-
const spinner =
|
|
2434
|
+
const spinner = ora4(`Loading history for "${options.keyword}"...`).start();
|
|
1862
2435
|
try {
|
|
1863
2436
|
const project = await getProjectForUrl(options.url);
|
|
1864
2437
|
if (!project) {
|
|
@@ -1891,21 +2464,21 @@ async function trackHistory(options) {
|
|
|
1891
2464
|
if (options.output === "json") {
|
|
1892
2465
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
1893
2466
|
} else {
|
|
1894
|
-
console.log("\n" +
|
|
2467
|
+
console.log("\n" + chalk6.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
1895
2468
|
console.log("\u2550".repeat(50));
|
|
1896
2469
|
if (history.length === 0) {
|
|
1897
|
-
console.log(
|
|
2470
|
+
console.log(chalk6.dim("\nNo history available yet.\n"));
|
|
1898
2471
|
} else {
|
|
1899
2472
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
1900
2473
|
const sparkline = positions.map((p) => {
|
|
1901
|
-
if (p <= 10) return
|
|
1902
|
-
if (p <= 20) return
|
|
1903
|
-
if (p <= 50) return
|
|
1904
|
-
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");
|
|
1905
2478
|
}).join("");
|
|
1906
2479
|
console.log(`
|
|
1907
2480
|
${sparkline} (last ${positions.length} checks)`);
|
|
1908
|
-
console.log(
|
|
2481
|
+
console.log(chalk6.dim(" \u2190 newer older \u2192\n"));
|
|
1909
2482
|
const recent = history.slice(0, 10);
|
|
1910
2483
|
for (const entry of recent) {
|
|
1911
2484
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -1925,7 +2498,7 @@ async function trackExport(options) {
|
|
|
1925
2498
|
if (!isLoggedIn()) {
|
|
1926
2499
|
return { success: false, error: "Not logged in" };
|
|
1927
2500
|
}
|
|
1928
|
-
const spinner =
|
|
2501
|
+
const spinner = ora4("Exporting data...").start();
|
|
1929
2502
|
try {
|
|
1930
2503
|
const project = await getProjectForUrl(options.url);
|
|
1931
2504
|
if (!project) {
|
|
@@ -1982,12 +2555,12 @@ async function trackExport(options) {
|
|
|
1982
2555
|
}
|
|
1983
2556
|
async function trackCheck(options) {
|
|
1984
2557
|
if (!options.url) {
|
|
1985
|
-
console.log(
|
|
2558
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1986
2559
|
console.log(" rankcli track check --url https://your-site.com\n");
|
|
1987
2560
|
return { success: false, error: "No URL provided" };
|
|
1988
2561
|
}
|
|
1989
2562
|
if (!isLoggedIn()) {
|
|
1990
|
-
console.log(
|
|
2563
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
1991
2564
|
console.log(" rankcli login\n");
|
|
1992
2565
|
return { success: false, error: "Not logged in" };
|
|
1993
2566
|
}
|
|
@@ -2004,14 +2577,14 @@ async function trackCheck(options) {
|
|
|
2004
2577
|
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
2005
2578
|
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
2006
2579
|
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
2007
|
-
console.log(
|
|
2580
|
+
console.log(chalk6.yellow(`
|
|
2008
2581
|
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
2009
|
-
console.log(
|
|
2582
|
+
console.log(chalk6.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
2010
2583
|
`));
|
|
2011
2584
|
return { success: true, rateLimited: true };
|
|
2012
2585
|
}
|
|
2013
2586
|
}
|
|
2014
|
-
const spinner =
|
|
2587
|
+
const spinner = ora4("Checking current rankings...").start();
|
|
2015
2588
|
const supabase = await getAuthenticatedClient();
|
|
2016
2589
|
if (!supabase) {
|
|
2017
2590
|
spinner.fail("Could not connect to database");
|
|
@@ -2025,8 +2598,8 @@ async function trackCheck(options) {
|
|
|
2025
2598
|
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2026
2599
|
if (!serpApiKey) {
|
|
2027
2600
|
spinner.warn("No SERP API key configured");
|
|
2028
|
-
console.log(
|
|
2029
|
-
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"));
|
|
2030
2603
|
return { success: false, error: "No SERP API key" };
|
|
2031
2604
|
}
|
|
2032
2605
|
const tracker = new RankTracker({
|
|
@@ -2038,13 +2611,13 @@ async function trackCheck(options) {
|
|
|
2038
2611
|
});
|
|
2039
2612
|
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2040
2613
|
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2041
|
-
console.log("\n" +
|
|
2614
|
+
console.log("\n" + chalk6.bold("\u{1F50D} Rank Check Results"));
|
|
2042
2615
|
console.log("\u2550".repeat(50));
|
|
2043
2616
|
for (const result of results) {
|
|
2044
2617
|
const posStr = formatPosition(result.position);
|
|
2045
2618
|
console.log(` ${result.keyword}: ${posStr}`);
|
|
2046
2619
|
if (result.serpFeatures.length > 0) {
|
|
2047
|
-
console.log(
|
|
2620
|
+
console.log(chalk6.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2048
2621
|
}
|
|
2049
2622
|
}
|
|
2050
2623
|
console.log("");
|
|
@@ -2063,8 +2636,8 @@ async function trackCheck(options) {
|
|
|
2063
2636
|
|
|
2064
2637
|
// src/commands/geo.ts
|
|
2065
2638
|
import { analyzers } from "@rankcli/agent-runtime";
|
|
2066
|
-
import
|
|
2067
|
-
import
|
|
2639
|
+
import ora5 from "ora";
|
|
2640
|
+
import chalk7 from "chalk";
|
|
2068
2641
|
async function fetchResources(url) {
|
|
2069
2642
|
const headers = {};
|
|
2070
2643
|
let html = null;
|
|
@@ -2110,7 +2683,7 @@ async function geo(options) {
|
|
|
2110
2683
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
2111
2684
|
return;
|
|
2112
2685
|
}
|
|
2113
|
-
const spinner =
|
|
2686
|
+
const spinner = ora5("Fetching page and robots.txt...").start();
|
|
2114
2687
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
2115
2688
|
if (!html) {
|
|
2116
2689
|
spinner.fail("Could not fetch page content");
|
|
@@ -2136,55 +2709,55 @@ async function geo(options) {
|
|
|
2136
2709
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2137
2710
|
return;
|
|
2138
2711
|
}
|
|
2139
|
-
console.log("\n" +
|
|
2140
|
-
console.log(
|
|
2141
|
-
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)));
|
|
2142
2715
|
const geoScore = geoResult.score;
|
|
2143
|
-
const scoreColor = geoScore >= 80 ?
|
|
2716
|
+
const scoreColor = geoScore >= 80 ? chalk7.green : geoScore >= 60 ? chalk7.yellow : chalk7.red;
|
|
2144
2717
|
console.log(`
|
|
2145
|
-
${
|
|
2146
|
-
console.log("\n" +
|
|
2718
|
+
${chalk7.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2719
|
+
console.log("\n" + chalk7.bold(" AI Crawler Access:"));
|
|
2147
2720
|
const { aiCrawlerAccess } = geoResult;
|
|
2148
2721
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
2149
|
-
console.log(
|
|
2722
|
+
console.log(chalk7.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
2150
2723
|
}
|
|
2151
2724
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
2152
2725
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
2153
|
-
console.log(
|
|
2726
|
+
console.log(chalk7.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
2154
2727
|
}
|
|
2155
|
-
console.log("\n" +
|
|
2728
|
+
console.log("\n" + chalk7.bold(" Rendering:"));
|
|
2156
2729
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
2157
|
-
console.log(
|
|
2730
|
+
console.log(chalk7.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
2158
2731
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
2159
|
-
console.log(
|
|
2732
|
+
console.log(chalk7.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
2160
2733
|
}
|
|
2161
|
-
console.log("\n" +
|
|
2734
|
+
console.log("\n" + chalk7.bold(" LLM Friendliness:"));
|
|
2162
2735
|
const { llmSignals } = geoResult;
|
|
2163
2736
|
const bar = (score) => {
|
|
2164
2737
|
const filled = Math.round(score / 5);
|
|
2165
2738
|
const empty = 20 - filled;
|
|
2166
|
-
const color = score >= 70 ?
|
|
2167
|
-
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));
|
|
2168
2741
|
};
|
|
2169
2742
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
2170
2743
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
2171
2744
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
2172
2745
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
2173
|
-
console.log("\n" +
|
|
2746
|
+
console.log("\n" + chalk7.bold(" Content Structure:"));
|
|
2174
2747
|
const { contentStructure } = geoResult;
|
|
2175
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
2176
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
2177
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
2178
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
2179
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
2180
|
-
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:"));
|
|
2181
2754
|
const { citationReadiness } = geoResult;
|
|
2182
2755
|
if (citationReadiness.trustSignals.length > 0) {
|
|
2183
|
-
console.log(
|
|
2756
|
+
console.log(chalk7.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
2184
2757
|
} else {
|
|
2185
|
-
console.log(
|
|
2758
|
+
console.log(chalk7.yellow(" \u25CB No trust signals detected"));
|
|
2186
2759
|
}
|
|
2187
|
-
console.log("\n" +
|
|
2760
|
+
console.log("\n" + chalk7.bold(" Other Scores:"));
|
|
2188
2761
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
2189
2762
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
2190
2763
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -2198,18 +2771,18 @@ async function geo(options) {
|
|
|
2198
2771
|
];
|
|
2199
2772
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
2200
2773
|
if (criticalIssues.length > 0) {
|
|
2201
|
-
console.log("\n" +
|
|
2774
|
+
console.log("\n" + chalk7.bold.red(" Critical Issues:"));
|
|
2202
2775
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
2203
|
-
console.log(
|
|
2776
|
+
console.log(chalk7.red(` ${i + 1}. ${issue.title}`));
|
|
2204
2777
|
});
|
|
2205
2778
|
}
|
|
2206
2779
|
if (geoResult.recommendations.length > 0) {
|
|
2207
|
-
console.log("\n" +
|
|
2780
|
+
console.log("\n" + chalk7.bold(" Recommendations:"));
|
|
2208
2781
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
2209
2782
|
console.log(` ${i + 1}. ${rec}`);
|
|
2210
2783
|
});
|
|
2211
2784
|
}
|
|
2212
|
-
console.log("\n" +
|
|
2785
|
+
console.log("\n" + chalk7.bold("\u2550".repeat(60)));
|
|
2213
2786
|
console.log("");
|
|
2214
2787
|
} catch (error) {
|
|
2215
2788
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -2217,7 +2790,7 @@ async function geo(options) {
|
|
|
2217
2790
|
}
|
|
2218
2791
|
|
|
2219
2792
|
// src/commands/keywords.ts
|
|
2220
|
-
import * as
|
|
2793
|
+
import * as readline2 from "readline";
|
|
2221
2794
|
import {
|
|
2222
2795
|
runKeywordResearch,
|
|
2223
2796
|
formatKeywordReport,
|
|
@@ -2230,10 +2803,10 @@ import {
|
|
|
2230
2803
|
runAIKeywordResearch
|
|
2231
2804
|
} from "@rankcli/agent-runtime";
|
|
2232
2805
|
async function keywords(options) {
|
|
2233
|
-
console.log("\n\u{1F511}
|
|
2806
|
+
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
2234
2807
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
2235
2808
|
console.log("based on your site's current authority and competition level.\n");
|
|
2236
|
-
const rl =
|
|
2809
|
+
const rl = readline2.createInterface({
|
|
2237
2810
|
input: process.stdin,
|
|
2238
2811
|
output: process.stdout
|
|
2239
2812
|
});
|
|
@@ -2604,7 +3177,7 @@ import {
|
|
|
2604
3177
|
analyzeHeadings
|
|
2605
3178
|
} from "@rankcli/agent-runtime";
|
|
2606
3179
|
async function analyzeContent(options) {
|
|
2607
|
-
console.log("\n\u{1F4DD}
|
|
3180
|
+
console.log("\n\u{1F4DD} RankCLI - Content Analysis\n");
|
|
2608
3181
|
const mode = options.mode || "full";
|
|
2609
3182
|
if (mode === "headline" || options.headline) {
|
|
2610
3183
|
const headline = options.headline || "";
|
|
@@ -2686,216 +3259,38 @@ Analyzing: "${title || options.url}"
|
|
|
2686
3259
|
console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
|
|
2687
3260
|
console.log(` Headings: ${headings.length}`);
|
|
2688
3261
|
console.log("");
|
|
2689
|
-
} catch (error) {
|
|
2690
|
-
console.log(`
|
|
2691
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2692
|
-
`);
|
|
2693
|
-
}
|
|
2694
|
-
} else {
|
|
2695
|
-
console.log("Usage:");
|
|
2696
|
-
console.log(" seo content -u <url> Full content analysis");
|
|
2697
|
-
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
2698
|
-
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
2699
|
-
console.log(" seo content -u <url> --mode readability Readability only");
|
|
2700
|
-
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
2701
|
-
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
2702
|
-
console.log("");
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
async function analyzeIntent(keyword) {
|
|
2706
|
-
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
2707
|
-
const analysis = classifyIntent(keyword);
|
|
2708
|
-
console.log(formatIntentReport(keyword, analysis));
|
|
2709
|
-
}
|
|
2710
|
-
function extractBodyText(html) {
|
|
2711
|
-
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
2712
|
-
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2713
|
-
text = text.replace(/<[^>]+>/g, " ");
|
|
2714
|
-
text = text.replace(/ /g, " ");
|
|
2715
|
-
text = text.replace(/&/g, "&");
|
|
2716
|
-
text = text.replace(/</g, "<");
|
|
2717
|
-
text = text.replace(/>/g, ">");
|
|
2718
|
-
text = text.replace(/"/g, '"');
|
|
2719
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
2720
|
-
return text;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// src/commands/setup.ts
|
|
2724
|
-
import * as readline2 from "readline";
|
|
2725
|
-
import * as fs from "fs";
|
|
2726
|
-
import * as path from "path";
|
|
2727
|
-
import {
|
|
2728
|
-
injectGA4,
|
|
2729
|
-
injectGSCVerification,
|
|
2730
|
-
writeGitHubActionFiles,
|
|
2731
|
-
getGSCSetupInstructions,
|
|
2732
|
-
generateGA4EnvTemplate
|
|
2733
|
-
} from "@rankcli/agent-runtime";
|
|
2734
|
-
async function setup(options) {
|
|
2735
|
-
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
2736
|
-
const projectPath = process.cwd();
|
|
2737
|
-
if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
|
|
2738
|
-
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
2739
|
-
}
|
|
2740
|
-
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
2741
|
-
await interactiveSetup(projectPath);
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
if (options.ga4) {
|
|
2745
|
-
await setupGA4(projectPath, options.ga4);
|
|
2746
|
-
}
|
|
2747
|
-
if (options.gsc) {
|
|
2748
|
-
await setupGSC(projectPath, options.gsc);
|
|
2749
|
-
}
|
|
2750
|
-
if (options.githubAction) {
|
|
2751
|
-
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
async function interactiveSetup(projectPath) {
|
|
2755
|
-
const rl = readline2.createInterface({
|
|
2756
|
-
input: process.stdin,
|
|
2757
|
-
output: process.stdout
|
|
2758
|
-
});
|
|
2759
|
-
const ask2 = (question) => {
|
|
2760
|
-
return new Promise((resolve) => {
|
|
2761
|
-
rl.question(question, (answer) => {
|
|
2762
|
-
resolve(answer.trim());
|
|
2763
|
-
});
|
|
2764
|
-
});
|
|
2765
|
-
};
|
|
2766
|
-
try {
|
|
2767
|
-
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
2768
|
-
console.log("\u2500".repeat(60));
|
|
2769
|
-
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
2770
|
-
console.log("GA4 tracks visitor behavior on your site.");
|
|
2771
|
-
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
2772
|
-
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
2773
|
-
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
2774
|
-
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
2775
|
-
await setupGA4(projectPath, ga4Id);
|
|
2776
|
-
} else if (ga4Id) {
|
|
2777
|
-
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
2778
|
-
} else {
|
|
2779
|
-
console.log("Skipping GA4 setup.\n");
|
|
2780
|
-
}
|
|
2781
|
-
console.log("\u2500".repeat(60));
|
|
2782
|
-
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
2783
|
-
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
2784
|
-
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
2785
|
-
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
2786
|
-
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
2787
|
-
if (gscCode && gscCode.length > 10) {
|
|
2788
|
-
await setupGSC(projectPath, gscCode);
|
|
2789
|
-
} else if (gscCode) {
|
|
2790
|
-
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
2791
|
-
} else {
|
|
2792
|
-
console.log("Skipping GSC verification.\n");
|
|
2793
|
-
}
|
|
2794
|
-
console.log("\u2500".repeat(60));
|
|
2795
|
-
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
2796
|
-
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
2797
|
-
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
2798
|
-
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
2799
|
-
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
2800
|
-
console.log("\nHow often should the check run?");
|
|
2801
|
-
console.log(" 1. Weekly (recommended for most sites)");
|
|
2802
|
-
console.log(" 2. Daily (for high-traffic sites)");
|
|
2803
|
-
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
2804
|
-
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
2805
|
-
const scheduleMap = {
|
|
2806
|
-
"1": "weekly",
|
|
2807
|
-
"2": "daily",
|
|
2808
|
-
"3": "monthly"
|
|
2809
|
-
};
|
|
2810
|
-
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2811
|
-
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2812
|
-
if (siteUrl) {
|
|
2813
|
-
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
2814
|
-
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
2815
|
-
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
2816
|
-
}
|
|
2817
|
-
} else {
|
|
2818
|
-
console.log("Skipping GitHub Action setup.\n");
|
|
2819
|
-
}
|
|
2820
|
-
console.log("\u2500".repeat(60));
|
|
2821
|
-
console.log("\u2705 Setup complete!\n");
|
|
2822
|
-
console.log("Next steps:");
|
|
2823
|
-
console.log(" 1. Commit the changes to your repository");
|
|
2824
|
-
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
2825
|
-
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
2826
|
-
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
2827
|
-
rl.close();
|
|
2828
|
-
} catch (error) {
|
|
2829
|
-
console.log(`
|
|
2830
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2831
|
-
`);
|
|
2832
|
-
rl.close();
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
async function setupGA4(projectPath, measurementId) {
|
|
2836
|
-
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
2837
|
-
const result = await injectGA4(projectPath, { measurementId });
|
|
2838
|
-
if (result.success) {
|
|
2839
|
-
console.log(`\u2705 ${result.message}`);
|
|
2840
|
-
if (result.file) {
|
|
2841
|
-
console.log(` Modified: ${result.file}`);
|
|
2842
|
-
}
|
|
2843
|
-
} else {
|
|
2844
|
-
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
2845
|
-
if (result.code) {
|
|
2846
|
-
console.log("\nManually add this code to your HTML <head>:\n");
|
|
2847
|
-
console.log(result.code);
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
const envExamplePath = path.join(projectPath, ".env.example");
|
|
2851
|
-
const envContent = generateGA4EnvTemplate();
|
|
2852
|
-
if (fs.existsSync(envExamplePath)) {
|
|
2853
|
-
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
2854
|
-
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
2855
|
-
fs.appendFileSync(envExamplePath, "\n" + envContent);
|
|
2856
|
-
console.log(" Updated: .env.example");
|
|
2857
|
-
}
|
|
2858
|
-
} else {
|
|
2859
|
-
fs.writeFileSync(envExamplePath, envContent);
|
|
2860
|
-
console.log(" Created: .env.example");
|
|
2861
|
-
}
|
|
2862
|
-
console.log("");
|
|
2863
|
-
}
|
|
2864
|
-
async function setupGSC(projectPath, verificationCode) {
|
|
2865
|
-
console.log("Setting up Google Search Console verification...");
|
|
2866
|
-
const result = await injectGSCVerification(projectPath, verificationCode);
|
|
2867
|
-
if (result.success) {
|
|
2868
|
-
console.log(`\u2705 ${result.message}`);
|
|
2869
|
-
if (result.file) {
|
|
2870
|
-
console.log(` Modified: ${result.file}`);
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
console.log(`
|
|
3264
|
+
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3265
|
+
`);
|
|
2871
3266
|
}
|
|
2872
3267
|
} else {
|
|
2873
|
-
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("");
|
|
2874
3276
|
}
|
|
2875
|
-
console.log("");
|
|
2876
3277
|
}
|
|
2877
|
-
async function
|
|
2878
|
-
console.log(
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
siteUrl: siteUrl || "https://example.com",
|
|
2882
|
-
features: {
|
|
2883
|
-
audit: true,
|
|
2884
|
-
autoFix: true,
|
|
2885
|
-
createIssues: true,
|
|
2886
|
-
createPRs: true
|
|
2887
|
-
},
|
|
2888
|
-
syncToDashboard
|
|
2889
|
-
};
|
|
2890
|
-
const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
|
|
2891
|
-
console.log("\u2705 GitHub Action files created:");
|
|
2892
|
-
for (const file of files) {
|
|
2893
|
-
console.log(` - ${path.relative(projectPath, file)}`);
|
|
2894
|
-
}
|
|
2895
|
-
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));
|
|
2896
3282
|
}
|
|
2897
|
-
function
|
|
2898
|
-
|
|
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;
|
|
2899
3294
|
}
|
|
2900
3295
|
|
|
2901
3296
|
// src/commands/alerts.ts
|
|
@@ -2920,7 +3315,7 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
2920
3315
|
return answer.toLowerCase().startsWith("y");
|
|
2921
3316
|
}
|
|
2922
3317
|
async function alerts(options) {
|
|
2923
|
-
console.log("\n\u{1F514}
|
|
3318
|
+
console.log("\n\u{1F514} RankCLI - Alert Configuration\n");
|
|
2924
3319
|
const rl = readline3.createInterface({
|
|
2925
3320
|
input: process.stdin,
|
|
2926
3321
|
output: process.stdout
|
|
@@ -3063,245 +3458,8 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
3063
3458
|
}
|
|
3064
3459
|
}
|
|
3065
3460
|
|
|
3066
|
-
// src/commands/auth.ts
|
|
3067
|
-
import chalk6 from "chalk";
|
|
3068
|
-
import ora5 from "ora";
|
|
3069
|
-
import inquirer from "inquirer";
|
|
3070
|
-
import open from "open";
|
|
3071
|
-
async function browserLogin(spinner) {
|
|
3072
|
-
spinner.start("Starting login...");
|
|
3073
|
-
let start;
|
|
3074
|
-
try {
|
|
3075
|
-
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
3076
|
-
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
3077
|
-
start = await res.json();
|
|
3078
|
-
} catch (err) {
|
|
3079
|
-
spinner.fail("Could not start login");
|
|
3080
|
-
console.log(chalk6.red(err instanceof Error ? err.message : "Network error"));
|
|
3081
|
-
return;
|
|
3082
|
-
}
|
|
3083
|
-
spinner.text = "Opening browser to approve...";
|
|
3084
|
-
try {
|
|
3085
|
-
await open(start.url);
|
|
3086
|
-
} catch {
|
|
3087
|
-
}
|
|
3088
|
-
console.log(chalk6.dim(`
|
|
3089
|
-
If a browser didn't open, visit:
|
|
3090
|
-
${start.url}
|
|
3091
|
-
`));
|
|
3092
|
-
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
3093
|
-
spinner.text = "Waiting for approval in your browser...";
|
|
3094
|
-
while (Date.now() < deadline) {
|
|
3095
|
-
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3096
|
-
let status;
|
|
3097
|
-
try {
|
|
3098
|
-
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
3099
|
-
method: "POST",
|
|
3100
|
-
headers: { "Content-Type": "application/json" },
|
|
3101
|
-
body: JSON.stringify({ connectId: start.connectId })
|
|
3102
|
-
});
|
|
3103
|
-
status = await res.json();
|
|
3104
|
-
} catch {
|
|
3105
|
-
continue;
|
|
3106
|
-
}
|
|
3107
|
-
if (status.status === "claimed" && status.apiKey) {
|
|
3108
|
-
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
3109
|
-
await fetchAndSaveSubscription();
|
|
3110
|
-
spinner.succeed(`Logged in as ${chalk6.green(status.email)}`);
|
|
3111
|
-
return;
|
|
3112
|
-
}
|
|
3113
|
-
if (status.status === "expired" || status.status === "not_found") {
|
|
3114
|
-
spinner.fail("Login link expired");
|
|
3115
|
-
console.log(chalk6.dim('Run "rankcli login" again.'));
|
|
3116
|
-
return;
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
spinner.fail("Timed out waiting for approval");
|
|
3120
|
-
console.log(chalk6.dim(`Run "rankcli login" again when you're ready.`));
|
|
3121
|
-
}
|
|
3122
|
-
async function login(options) {
|
|
3123
|
-
if (isLoggedIn()) {
|
|
3124
|
-
const { email } = getUserInfo();
|
|
3125
|
-
console.log(chalk6.yellow(`Already logged in as ${email}`));
|
|
3126
|
-
console.log(chalk6.dim('Run "rankcli logout" to sign out first.'));
|
|
3127
|
-
return;
|
|
3128
|
-
}
|
|
3129
|
-
const spinner = ora5();
|
|
3130
|
-
try {
|
|
3131
|
-
if (options.token) {
|
|
3132
|
-
const apiKey = options.token;
|
|
3133
|
-
if (!apiKey.startsWith("rankcli_")) {
|
|
3134
|
-
console.log(chalk6.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
3135
|
-
return;
|
|
3136
|
-
}
|
|
3137
|
-
spinner.start("Validating API key...");
|
|
3138
|
-
const result = await validateApiKey(apiKey);
|
|
3139
|
-
if (!result.valid) {
|
|
3140
|
-
spinner.fail("Invalid API key");
|
|
3141
|
-
console.log(chalk6.red(`
|
|
3142
|
-
${result.error}`));
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
saveApiKey(apiKey);
|
|
3146
|
-
if (result.subscription) {
|
|
3147
|
-
saveSubscription({
|
|
3148
|
-
planId: result.subscription.planId,
|
|
3149
|
-
planName: result.subscription.planName,
|
|
3150
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3151
|
-
});
|
|
3152
|
-
}
|
|
3153
|
-
spinner.succeed(`Authenticated via API key`);
|
|
3154
|
-
console.log(chalk6.dim(`
|
|
3155
|
-
Email: ${result.user?.email}`));
|
|
3156
|
-
console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3157
|
-
return;
|
|
3158
|
-
}
|
|
3159
|
-
if (options.browser !== false && !options.email) {
|
|
3160
|
-
await browserLogin(spinner);
|
|
3161
|
-
} else {
|
|
3162
|
-
let email = options.email;
|
|
3163
|
-
if (!email) {
|
|
3164
|
-
const answers = await inquirer.prompt([
|
|
3165
|
-
{
|
|
3166
|
-
type: "input",
|
|
3167
|
-
name: "email",
|
|
3168
|
-
message: "Email:",
|
|
3169
|
-
validate: (input) => {
|
|
3170
|
-
if (!input.includes("@")) return "Please enter a valid email";
|
|
3171
|
-
return true;
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
]);
|
|
3175
|
-
email = answers.email;
|
|
3176
|
-
}
|
|
3177
|
-
const { password } = await inquirer.prompt([
|
|
3178
|
-
{
|
|
3179
|
-
type: "password",
|
|
3180
|
-
name: "password",
|
|
3181
|
-
message: "Password:",
|
|
3182
|
-
mask: "*"
|
|
3183
|
-
}
|
|
3184
|
-
]);
|
|
3185
|
-
spinner.start("Signing in...");
|
|
3186
|
-
const supabase = getSupabaseClient();
|
|
3187
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
3188
|
-
email,
|
|
3189
|
-
password
|
|
3190
|
-
});
|
|
3191
|
-
if (error) {
|
|
3192
|
-
spinner.fail("Login failed");
|
|
3193
|
-
console.log(chalk6.red(`
|
|
3194
|
-
${error.message}`));
|
|
3195
|
-
if (error.message.includes("Invalid login")) {
|
|
3196
|
-
console.log(chalk6.dim("\nDon't have an account? Sign up at:"));
|
|
3197
|
-
console.log(chalk6.cyan(` ${WEB_APP_URL}/signup`));
|
|
3198
|
-
}
|
|
3199
|
-
return;
|
|
3200
|
-
}
|
|
3201
|
-
if (!data.session) {
|
|
3202
|
-
spinner.fail("No session returned");
|
|
3203
|
-
return;
|
|
3204
|
-
}
|
|
3205
|
-
saveSession(data.session);
|
|
3206
|
-
await fetchAndSaveSubscription();
|
|
3207
|
-
spinner.succeed(`Logged in as ${chalk6.green(data.user?.email)}`);
|
|
3208
|
-
const { planName, sitesLimit } = getUserInfo();
|
|
3209
|
-
console.log(chalk6.dim(`
|
|
3210
|
-
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
3211
|
-
}
|
|
3212
|
-
} catch (err) {
|
|
3213
|
-
spinner.fail("Login failed");
|
|
3214
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3215
|
-
}
|
|
3216
|
-
}
|
|
3217
|
-
async function logout() {
|
|
3218
|
-
if (!isLoggedIn()) {
|
|
3219
|
-
console.log(chalk6.yellow("Not logged in"));
|
|
3220
|
-
return;
|
|
3221
|
-
}
|
|
3222
|
-
const { email } = getUserInfo();
|
|
3223
|
-
const spinner = ora5("Signing out...").start();
|
|
3224
|
-
try {
|
|
3225
|
-
const supabase = getSupabaseClient();
|
|
3226
|
-
await supabase.auth.signOut();
|
|
3227
|
-
clearCredentials();
|
|
3228
|
-
spinner.succeed(`Logged out from ${chalk6.dim(email)}`);
|
|
3229
|
-
} catch (err) {
|
|
3230
|
-
spinner.fail("Logout failed");
|
|
3231
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
async function whoami() {
|
|
3235
|
-
if (!isLoggedIn()) {
|
|
3236
|
-
console.log(chalk6.yellow("Not logged in"));
|
|
3237
|
-
console.log(chalk6.dim('\nRun "rankcli login" to sign in.'));
|
|
3238
|
-
console.log(chalk6.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
3239
|
-
return;
|
|
3240
|
-
}
|
|
3241
|
-
const spinner = ora5("Checking account...").start();
|
|
3242
|
-
try {
|
|
3243
|
-
if (isApiKeyAuth()) {
|
|
3244
|
-
const apiKey = getApiKey();
|
|
3245
|
-
if (apiKey) {
|
|
3246
|
-
const result = await validateApiKey(apiKey);
|
|
3247
|
-
if (result.valid && result.subscription) {
|
|
3248
|
-
saveSubscription({
|
|
3249
|
-
planId: result.subscription.planId,
|
|
3250
|
-
planName: result.subscription.planName,
|
|
3251
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3252
|
-
});
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
} else {
|
|
3256
|
-
await fetchAndSaveSubscription();
|
|
3257
|
-
}
|
|
3258
|
-
const { email, planName, sitesLimit } = getUserInfo();
|
|
3259
|
-
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
3260
|
-
spinner.stop();
|
|
3261
|
-
console.log(chalk6.bold("\nRankCLI Account"));
|
|
3262
|
-
console.log("\u2500".repeat(40));
|
|
3263
|
-
console.log(` Email: ${chalk6.cyan(email || "N/A")}`);
|
|
3264
|
-
console.log(` Plan: ${chalk6.green(planName)}`);
|
|
3265
|
-
console.log(` Sites: ${sitesLimit} allowed`);
|
|
3266
|
-
console.log(` Auth: ${chalk6.dim(authMethod)}`);
|
|
3267
|
-
console.log("\u2500".repeat(40));
|
|
3268
|
-
console.log(chalk6.dim(`
|
|
3269
|
-
Manage at: ${WEB_APP_URL}/account`));
|
|
3270
|
-
} catch (err) {
|
|
3271
|
-
spinner.fail("Failed to fetch account info");
|
|
3272
|
-
console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
async function fetchAndSaveSubscription() {
|
|
3276
|
-
const supabase = await getAuthenticatedClient();
|
|
3277
|
-
if (!supabase) return;
|
|
3278
|
-
const { userId } = getUserInfo();
|
|
3279
|
-
if (!userId) return;
|
|
3280
|
-
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
3281
|
-
plan:subscription_plans (
|
|
3282
|
-
plan_id,
|
|
3283
|
-
name,
|
|
3284
|
-
sites_limit
|
|
3285
|
-
)
|
|
3286
|
-
`).eq("user_id", userId).single();
|
|
3287
|
-
if (subscription?.plan) {
|
|
3288
|
-
const plan = subscription.plan;
|
|
3289
|
-
saveSubscription({
|
|
3290
|
-
planId: plan.plan_id,
|
|
3291
|
-
planName: plan.name,
|
|
3292
|
-
sitesLimit: plan.sites_limit
|
|
3293
|
-
});
|
|
3294
|
-
} else {
|
|
3295
|
-
saveSubscription({
|
|
3296
|
-
planId: void 0,
|
|
3297
|
-
planName: "Free",
|
|
3298
|
-
sitesLimit: 1
|
|
3299
|
-
});
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
3461
|
// src/version.ts
|
|
3304
|
-
var VERSION = "0.0.
|
|
3462
|
+
var VERSION = "0.0.26";
|
|
3305
3463
|
|
|
3306
3464
|
// src/index.ts
|
|
3307
3465
|
var program = new Command();
|
|
@@ -3309,9 +3467,9 @@ program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets
|
|
|
3309
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);
|
|
3310
3468
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3311
3469
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3312
|
-
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);
|
|
3313
3471
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
3314
|
-
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);
|
|
3315
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);
|
|
3316
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) => {
|
|
3317
3475
|
const result = await fix({
|