@rankcli/cli 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1158 -1000
- package/dist/index.mjs +1117 -959
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,262 +29,15 @@ var import_commander = require("commander");
|
|
|
29
29
|
// src/commands/init.ts
|
|
30
30
|
var import_fs = require("fs");
|
|
31
31
|
var import_path = require("path");
|
|
32
|
-
var
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{ pattern: "nuxt.config", name: "Nuxt" },
|
|
36
|
-
{ pattern: "svelte.config", name: "SvelteKit" },
|
|
37
|
-
{ pattern: "gatsby-config", name: "Gatsby" },
|
|
38
|
-
{ pattern: "vite.config", name: "Vite" },
|
|
39
|
-
{ pattern: "hugo.toml", name: "Hugo" },
|
|
40
|
-
{ pattern: "_config.yml", name: "Jekyll" },
|
|
41
|
-
{ pattern: "angular.json", name: "Angular" },
|
|
42
|
-
{ pattern: "vue.config", name: "Vue CLI" }
|
|
43
|
-
];
|
|
44
|
-
function detectFramework() {
|
|
45
|
-
const cwd = process.cwd();
|
|
46
|
-
for (const fw of FRAMEWORKS) {
|
|
47
|
-
const patterns = [
|
|
48
|
-
`${fw.pattern}.js`,
|
|
49
|
-
`${fw.pattern}.ts`,
|
|
50
|
-
`${fw.pattern}.mjs`,
|
|
51
|
-
`${fw.pattern}.cjs`,
|
|
52
|
-
fw.pattern
|
|
53
|
-
];
|
|
54
|
-
for (const p of patterns) {
|
|
55
|
-
if ((0, import_fs.existsSync)((0, import_path.join)(cwd, p))) {
|
|
56
|
-
return fw.name;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const pkgPath = (0, import_path.join)(cwd, "package.json");
|
|
61
|
-
if ((0, import_fs.existsSync)(pkgPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
|
|
64
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
65
|
-
if (deps["next"]) return "Next.js";
|
|
66
|
-
if (deps["astro"]) return "Astro";
|
|
67
|
-
if (deps["nuxt"]) return "Nuxt";
|
|
68
|
-
if (deps["@sveltejs/kit"]) return "SvelteKit";
|
|
69
|
-
if (deps["gatsby"]) return "Gatsby";
|
|
70
|
-
if (deps["react"]) return "React";
|
|
71
|
-
if (deps["vue"]) return "Vue";
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
function getProjectName() {
|
|
78
|
-
const cwd = process.cwd();
|
|
79
|
-
const pkgPath = (0, import_path.join)(cwd, "package.json");
|
|
80
|
-
if ((0, import_fs.existsSync)(pkgPath)) {
|
|
81
|
-
try {
|
|
82
|
-
const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
|
|
83
|
-
if (pkg.name) return pkg.name;
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return cwd.split("/").pop() || "unknown";
|
|
88
|
-
}
|
|
89
|
-
async function init(options) {
|
|
90
|
-
console.log("\n\u{1F680} Initializing SEO Autopilot...\n");
|
|
91
|
-
const configPath = (0, import_path.join)(process.cwd(), "seo-autopilot.config.json");
|
|
92
|
-
if ((0, import_fs.existsSync)(configPath)) {
|
|
93
|
-
console.log("\u26A0\uFE0F Configuration file already exists: seo-autopilot.config.json");
|
|
94
|
-
console.log(' Run "seo analyze" to analyze your project.\n');
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const framework = detectFramework();
|
|
98
|
-
const projectName = getProjectName();
|
|
99
|
-
console.log(`\u{1F4E6} Project: ${projectName}`);
|
|
100
|
-
console.log(`\u{1F527} Framework: ${framework || "Unknown (static site?)"}`);
|
|
101
|
-
const config2 = {
|
|
102
|
-
name: projectName,
|
|
103
|
-
framework,
|
|
104
|
-
url: null,
|
|
105
|
-
initialized: (/* @__PURE__ */ new Date()).toISOString()
|
|
106
|
-
};
|
|
107
|
-
(0, import_fs.writeFileSync)(configPath, JSON.stringify(config2, null, 2));
|
|
108
|
-
console.log("\n\u2705 Created seo-autopilot.config.json");
|
|
109
|
-
console.log("\n\u{1F4CB} Next steps:");
|
|
110
|
-
console.log(' 1. Run "seo analyze" to analyze your project');
|
|
111
|
-
console.log(' 2. Run "seo audit --url <your-site>" to run a technical audit');
|
|
112
|
-
console.log(" 3. Connect Google Search Console at https://seo-autopilot.dev/connect\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// src/commands/analyze.ts
|
|
116
|
-
var import_fs2 = require("fs");
|
|
117
|
-
var import_path2 = require("path");
|
|
118
|
-
var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
|
|
119
|
-
function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
|
|
120
|
-
if (depth > maxDepth) return [];
|
|
121
|
-
const files = [];
|
|
122
|
-
try {
|
|
123
|
-
const items = (0, import_fs2.readdirSync)(dir);
|
|
124
|
-
for (const item of items) {
|
|
125
|
-
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const fullPath = (0, import_path2.join)(dir, item);
|
|
129
|
-
const stat = (0, import_fs2.statSync)(fullPath);
|
|
130
|
-
if (stat.isDirectory()) {
|
|
131
|
-
files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
|
|
132
|
-
} else if (extensions.includes((0, import_path2.extname)(item))) {
|
|
133
|
-
files.push(fullPath);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
return files;
|
|
139
|
-
}
|
|
140
|
-
function extractKeywords(text) {
|
|
141
|
-
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
142
|
-
const frequency = {};
|
|
143
|
-
for (const word of words) {
|
|
144
|
-
frequency[word] = (frequency[word] || 0) + 1;
|
|
145
|
-
}
|
|
146
|
-
const stopWords = /* @__PURE__ */ new Set([
|
|
147
|
-
"this",
|
|
148
|
-
"that",
|
|
149
|
-
"with",
|
|
150
|
-
"from",
|
|
151
|
-
"have",
|
|
152
|
-
"been",
|
|
153
|
-
"were",
|
|
154
|
-
"will",
|
|
155
|
-
"would",
|
|
156
|
-
"could",
|
|
157
|
-
"should",
|
|
158
|
-
"their",
|
|
159
|
-
"there",
|
|
160
|
-
"about",
|
|
161
|
-
"which",
|
|
162
|
-
"when",
|
|
163
|
-
"what",
|
|
164
|
-
"your",
|
|
165
|
-
"more",
|
|
166
|
-
"some",
|
|
167
|
-
"just",
|
|
168
|
-
"into",
|
|
169
|
-
"over",
|
|
170
|
-
"such",
|
|
171
|
-
"only",
|
|
172
|
-
"than",
|
|
173
|
-
"them",
|
|
174
|
-
"then",
|
|
175
|
-
"these",
|
|
176
|
-
"most",
|
|
177
|
-
"other",
|
|
178
|
-
"also",
|
|
179
|
-
"being",
|
|
180
|
-
"after",
|
|
181
|
-
"const",
|
|
182
|
-
"function",
|
|
183
|
-
"return",
|
|
184
|
-
"import",
|
|
185
|
-
"export",
|
|
186
|
-
"default",
|
|
187
|
-
"true",
|
|
188
|
-
"false"
|
|
189
|
-
]);
|
|
190
|
-
return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
|
|
191
|
-
}
|
|
192
|
-
function loadConfig() {
|
|
193
|
-
const configPath = (0, import_path2.join)(process.cwd(), "seo-autopilot.config.json");
|
|
194
|
-
if ((0, import_fs2.existsSync)(configPath)) {
|
|
195
|
-
try {
|
|
196
|
-
return JSON.parse((0, import_fs2.readFileSync)(configPath, "utf-8"));
|
|
197
|
-
} catch {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
async function analyze(options) {
|
|
204
|
-
console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
|
|
205
|
-
const cwd = process.cwd();
|
|
206
|
-
const config2 = loadConfig();
|
|
207
|
-
const result = {
|
|
208
|
-
projectName: config2?.name || cwd.split("/").pop() || "unknown",
|
|
209
|
-
framework: config2?.framework || null,
|
|
210
|
-
hasReadme: false,
|
|
211
|
-
readmeSummary: null,
|
|
212
|
-
pageCount: 0,
|
|
213
|
-
hasMetaTags: false,
|
|
214
|
-
hasSitemap: false,
|
|
215
|
-
hasRobotsTxt: false,
|
|
216
|
-
keywords: [],
|
|
217
|
-
suggestions: []
|
|
218
|
-
};
|
|
219
|
-
const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
|
|
220
|
-
for (const rp of readmePaths) {
|
|
221
|
-
if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, rp))) {
|
|
222
|
-
result.hasReadme = true;
|
|
223
|
-
const content = (0, import_fs2.readFileSync)((0, import_path2.join)(cwd, rp), "utf-8");
|
|
224
|
-
result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
|
|
225
|
-
result.keywords.push(...extractKeywords(content));
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
result.hasSitemap = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "sitemap.xml")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "sitemap.xml"));
|
|
230
|
-
result.hasRobotsTxt = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "robots.txt")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "robots.txt"));
|
|
231
|
-
const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
|
|
232
|
-
result.pageCount = contentFiles.length;
|
|
233
|
-
const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
|
|
234
|
-
for (const hf of htmlFiles.slice(0, 5)) {
|
|
235
|
-
const content = (0, import_fs2.readFileSync)(hf, "utf-8");
|
|
236
|
-
if (content.includes("<meta") || content.includes("metadata")) {
|
|
237
|
-
result.hasMetaTags = true;
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
for (const cf of contentFiles.slice(0, 10)) {
|
|
242
|
-
const content = (0, import_fs2.readFileSync)(cf, "utf-8");
|
|
243
|
-
result.keywords.push(...extractKeywords(content));
|
|
244
|
-
}
|
|
245
|
-
result.keywords = [...new Set(result.keywords)].slice(0, 20);
|
|
246
|
-
if (!result.hasReadme) {
|
|
247
|
-
result.suggestions.push("Add a README.md with clear project description");
|
|
248
|
-
}
|
|
249
|
-
if (!result.hasSitemap) {
|
|
250
|
-
result.suggestions.push("Generate a sitemap.xml for better crawlability");
|
|
251
|
-
}
|
|
252
|
-
if (!result.hasRobotsTxt) {
|
|
253
|
-
result.suggestions.push("Add robots.txt to guide search engine crawlers");
|
|
254
|
-
}
|
|
255
|
-
if (!result.hasMetaTags) {
|
|
256
|
-
result.suggestions.push("Add meta tags (title, description, og:*) to pages");
|
|
257
|
-
}
|
|
258
|
-
if (result.pageCount < 5) {
|
|
259
|
-
result.suggestions.push("Consider adding more content pages for SEO coverage");
|
|
260
|
-
}
|
|
261
|
-
console.log("\u{1F4CA} Analysis Results\n");
|
|
262
|
-
console.log(`Project: ${result.projectName}`);
|
|
263
|
-
console.log(`Framework: ${result.framework || "Unknown"}`);
|
|
264
|
-
console.log(`Content files: ${result.pageCount}`);
|
|
265
|
-
console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
|
|
266
|
-
console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
|
|
267
|
-
console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
|
|
268
|
-
console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
|
|
269
|
-
if (result.keywords.length > 0) {
|
|
270
|
-
console.log("\n\u{1F511} Detected Keywords:");
|
|
271
|
-
console.log(` ${result.keywords.join(", ")}`);
|
|
272
|
-
}
|
|
273
|
-
if (result.suggestions.length > 0) {
|
|
274
|
-
console.log("\n\u{1F4A1} Suggestions:");
|
|
275
|
-
for (const s of result.suggestions) {
|
|
276
|
-
console.log(` \u2022 ${s}`);
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
console.log("\n\u2705 No critical SEO issues found!");
|
|
280
|
-
}
|
|
281
|
-
console.log('\n\u{1F4C8} Next: Run "seo audit --url <your-deployed-site>" for a full technical audit\n');
|
|
282
|
-
}
|
|
32
|
+
var import_child_process = require("child_process");
|
|
33
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
34
|
+
var import_inquirer2 = __toESM(require("inquirer"));
|
|
283
35
|
|
|
284
|
-
// src/commands/
|
|
285
|
-
var
|
|
36
|
+
// src/commands/auth.ts
|
|
37
|
+
var import_chalk = __toESM(require("chalk"));
|
|
286
38
|
var import_ora = __toESM(require("ora"));
|
|
287
|
-
var
|
|
39
|
+
var import_inquirer = __toESM(require("inquirer"));
|
|
40
|
+
var import_open = __toESM(require("open"));
|
|
288
41
|
|
|
289
42
|
// src/lib/config.ts
|
|
290
43
|
var import_conf = __toESM(require("conf"));
|
|
@@ -486,15 +239,807 @@ async function getAuthenticatedClient() {
|
|
|
486
239
|
return supabase;
|
|
487
240
|
}
|
|
488
241
|
|
|
489
|
-
// src/
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
242
|
+
// src/commands/auth.ts
|
|
243
|
+
async function browserLogin(spinner) {
|
|
244
|
+
spinner.start("Starting login...");
|
|
245
|
+
let start;
|
|
246
|
+
try {
|
|
247
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
248
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
249
|
+
start = await res.json();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
spinner.fail("Could not start login");
|
|
252
|
+
console.log(import_chalk.default.red(err instanceof Error ? err.message : "Network error"));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
spinner.text = "Opening browser to approve...";
|
|
256
|
+
try {
|
|
257
|
+
await (0, import_open.default)(start.url);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
console.log(import_chalk.default.dim(`
|
|
261
|
+
If a browser didn't open, visit:
|
|
262
|
+
${start.url}
|
|
263
|
+
`));
|
|
264
|
+
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
265
|
+
spinner.text = "Waiting for approval in your browser...";
|
|
266
|
+
while (Date.now() < deadline) {
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
268
|
+
let status;
|
|
269
|
+
try {
|
|
270
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ connectId: start.connectId })
|
|
274
|
+
});
|
|
275
|
+
status = await res.json();
|
|
276
|
+
} catch {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (status.status === "claimed" && status.apiKey) {
|
|
280
|
+
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
281
|
+
await fetchAndSaveSubscription();
|
|
282
|
+
spinner.succeed(`Logged in as ${import_chalk.default.green(status.email)}`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (status.status === "expired" || status.status === "not_found") {
|
|
286
|
+
spinner.fail("Login link expired");
|
|
287
|
+
console.log(import_chalk.default.dim('Run "rankcli login" again.'));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
spinner.fail("Timed out waiting for approval");
|
|
292
|
+
console.log(import_chalk.default.dim(`Run "rankcli login" again when you're ready.`));
|
|
293
|
+
}
|
|
294
|
+
async function login(options) {
|
|
295
|
+
if (isLoggedIn()) {
|
|
296
|
+
const { email } = getUserInfo();
|
|
297
|
+
console.log(import_chalk.default.yellow(`Already logged in as ${email}`));
|
|
298
|
+
console.log(import_chalk.default.dim('Run "rankcli logout" to sign out first.'));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const spinner = (0, import_ora.default)();
|
|
302
|
+
try {
|
|
303
|
+
if (options.token) {
|
|
304
|
+
const apiKey = options.token;
|
|
305
|
+
if (!apiKey.startsWith("rankcli_")) {
|
|
306
|
+
console.log(import_chalk.default.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
spinner.start("Validating API key...");
|
|
310
|
+
const result = await validateApiKey(apiKey);
|
|
311
|
+
if (!result.valid) {
|
|
312
|
+
spinner.fail("Invalid API key");
|
|
313
|
+
console.log(import_chalk.default.red(`
|
|
314
|
+
${result.error}`));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
saveApiKey(apiKey);
|
|
318
|
+
if (result.subscription) {
|
|
319
|
+
saveSubscription({
|
|
320
|
+
planId: result.subscription.planId,
|
|
321
|
+
planName: result.subscription.planName,
|
|
322
|
+
sitesLimit: result.subscription.sitesLimit
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
spinner.succeed(`Authenticated via API key`);
|
|
326
|
+
console.log(import_chalk.default.dim(`
|
|
327
|
+
Email: ${result.user?.email}`));
|
|
328
|
+
console.log(import_chalk.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (options.browser !== false && !options.email) {
|
|
332
|
+
await browserLogin(spinner);
|
|
333
|
+
} else {
|
|
334
|
+
let email = options.email;
|
|
335
|
+
if (!email) {
|
|
336
|
+
const answers = await import_inquirer.default.prompt([
|
|
337
|
+
{
|
|
338
|
+
type: "input",
|
|
339
|
+
name: "email",
|
|
340
|
+
message: "Email:",
|
|
341
|
+
validate: (input) => {
|
|
342
|
+
if (!input.includes("@")) return "Please enter a valid email";
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
]);
|
|
347
|
+
email = answers.email;
|
|
348
|
+
}
|
|
349
|
+
const { password } = await import_inquirer.default.prompt([
|
|
350
|
+
{
|
|
351
|
+
type: "password",
|
|
352
|
+
name: "password",
|
|
353
|
+
message: "Password:",
|
|
354
|
+
mask: "*"
|
|
355
|
+
}
|
|
356
|
+
]);
|
|
357
|
+
spinner.start("Signing in...");
|
|
358
|
+
const supabase = getSupabaseClient();
|
|
359
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
360
|
+
email,
|
|
361
|
+
password
|
|
362
|
+
});
|
|
363
|
+
if (error) {
|
|
364
|
+
spinner.fail("Login failed");
|
|
365
|
+
console.log(import_chalk.default.red(`
|
|
366
|
+
${error.message}`));
|
|
367
|
+
if (error.message.includes("Invalid login")) {
|
|
368
|
+
console.log(import_chalk.default.dim("\nDon't have an account? Sign up at:"));
|
|
369
|
+
console.log(import_chalk.default.cyan(` ${WEB_APP_URL}/signup`));
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!data.session) {
|
|
374
|
+
spinner.fail("No session returned");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
saveSession(data.session);
|
|
378
|
+
await fetchAndSaveSubscription();
|
|
379
|
+
spinner.succeed(`Logged in as ${import_chalk.default.green(data.user?.email)}`);
|
|
380
|
+
const { planName, sitesLimit } = getUserInfo();
|
|
381
|
+
console.log(import_chalk.default.dim(`
|
|
382
|
+
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
spinner.fail("Login failed");
|
|
386
|
+
console.error(import_chalk.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function logout() {
|
|
390
|
+
if (!isLoggedIn()) {
|
|
391
|
+
console.log(import_chalk.default.yellow("Not logged in"));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const { email } = getUserInfo();
|
|
395
|
+
const spinner = (0, import_ora.default)("Signing out...").start();
|
|
396
|
+
try {
|
|
397
|
+
const supabase = getSupabaseClient();
|
|
398
|
+
await supabase.auth.signOut();
|
|
399
|
+
clearCredentials();
|
|
400
|
+
spinner.succeed(`Logged out from ${import_chalk.default.dim(email)}`);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
spinner.fail("Logout failed");
|
|
403
|
+
console.error(import_chalk.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function whoami() {
|
|
407
|
+
if (!isLoggedIn()) {
|
|
408
|
+
console.log(import_chalk.default.yellow("Not logged in"));
|
|
409
|
+
console.log(import_chalk.default.dim('\nRun "rankcli login" to sign in.'));
|
|
410
|
+
console.log(import_chalk.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const spinner = (0, import_ora.default)("Checking account...").start();
|
|
414
|
+
try {
|
|
415
|
+
if (isApiKeyAuth()) {
|
|
416
|
+
const apiKey = getApiKey();
|
|
417
|
+
if (apiKey) {
|
|
418
|
+
const result = await validateApiKey(apiKey);
|
|
419
|
+
if (result.valid && result.subscription) {
|
|
420
|
+
saveSubscription({
|
|
421
|
+
planId: result.subscription.planId,
|
|
422
|
+
planName: result.subscription.planName,
|
|
423
|
+
sitesLimit: result.subscription.sitesLimit
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
await fetchAndSaveSubscription();
|
|
429
|
+
}
|
|
430
|
+
const { email, planName, sitesLimit } = getUserInfo();
|
|
431
|
+
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
432
|
+
spinner.stop();
|
|
433
|
+
console.log(import_chalk.default.bold("\nRankCLI Account"));
|
|
434
|
+
console.log("\u2500".repeat(40));
|
|
435
|
+
console.log(` Email: ${import_chalk.default.cyan(email || "N/A")}`);
|
|
436
|
+
console.log(` Plan: ${import_chalk.default.green(planName)}`);
|
|
437
|
+
console.log(` Sites: ${sitesLimit} allowed`);
|
|
438
|
+
console.log(` Auth: ${import_chalk.default.dim(authMethod)}`);
|
|
439
|
+
console.log("\u2500".repeat(40));
|
|
440
|
+
console.log(import_chalk.default.dim(`
|
|
441
|
+
Manage at: ${WEB_APP_URL}/account`));
|
|
442
|
+
} catch (err) {
|
|
443
|
+
spinner.fail("Failed to fetch account info");
|
|
444
|
+
console.error(import_chalk.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function fetchAndSaveSubscription() {
|
|
448
|
+
const supabase = await getAuthenticatedClient();
|
|
449
|
+
if (!supabase) return;
|
|
450
|
+
const { userId } = getUserInfo();
|
|
451
|
+
if (!userId) return;
|
|
452
|
+
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
453
|
+
plan:subscription_plans (
|
|
454
|
+
plan_id,
|
|
455
|
+
name,
|
|
456
|
+
sites_limit
|
|
457
|
+
)
|
|
458
|
+
`).eq("user_id", userId).single();
|
|
459
|
+
if (subscription?.plan) {
|
|
460
|
+
const plan = subscription.plan;
|
|
461
|
+
saveSubscription({
|
|
462
|
+
planId: plan.plan_id,
|
|
463
|
+
planName: plan.name,
|
|
464
|
+
sitesLimit: plan.sites_limit
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
saveSubscription({
|
|
468
|
+
planId: void 0,
|
|
469
|
+
planName: "Free",
|
|
470
|
+
sitesLimit: 1
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/commands/setup.ts
|
|
476
|
+
var readline = __toESM(require("readline"));
|
|
477
|
+
var fs = __toESM(require("fs"));
|
|
478
|
+
var path = __toESM(require("path"));
|
|
479
|
+
var import_agent_runtime = require("@rankcli/agent-runtime");
|
|
480
|
+
async function setup(options) {
|
|
481
|
+
console.log("\n\u{1F527} RankCLI - Setup\n");
|
|
482
|
+
const projectPath = process.cwd();
|
|
483
|
+
if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
|
|
484
|
+
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
485
|
+
}
|
|
486
|
+
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
487
|
+
await interactiveSetup(projectPath);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (options.ga4) {
|
|
491
|
+
await setupGA4(projectPath, options.ga4);
|
|
492
|
+
}
|
|
493
|
+
if (options.gsc) {
|
|
494
|
+
await setupGSC(projectPath, options.gsc);
|
|
495
|
+
}
|
|
496
|
+
if (options.githubAction) {
|
|
497
|
+
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function interactiveSetup(projectPath) {
|
|
501
|
+
const rl = readline.createInterface({
|
|
502
|
+
input: process.stdin,
|
|
503
|
+
output: process.stdout
|
|
504
|
+
});
|
|
505
|
+
const ask2 = (question) => {
|
|
506
|
+
return new Promise((resolve) => {
|
|
507
|
+
rl.question(question, (answer) => {
|
|
508
|
+
resolve(answer.trim());
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
try {
|
|
513
|
+
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
514
|
+
console.log("\u2500".repeat(60));
|
|
515
|
+
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
516
|
+
console.log("GA4 tracks visitor behavior on your site.");
|
|
517
|
+
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
518
|
+
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
519
|
+
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
520
|
+
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
521
|
+
await setupGA4(projectPath, ga4Id);
|
|
522
|
+
} else if (ga4Id) {
|
|
523
|
+
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
524
|
+
} else {
|
|
525
|
+
console.log("Skipping GA4 setup.\n");
|
|
526
|
+
}
|
|
527
|
+
console.log("\u2500".repeat(60));
|
|
528
|
+
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
529
|
+
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
530
|
+
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
531
|
+
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
532
|
+
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
533
|
+
if (gscCode && gscCode.length > 10) {
|
|
534
|
+
await setupGSC(projectPath, gscCode);
|
|
535
|
+
} else if (gscCode) {
|
|
536
|
+
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
537
|
+
} else {
|
|
538
|
+
console.log("Skipping GSC verification.\n");
|
|
539
|
+
}
|
|
540
|
+
console.log("\u2500".repeat(60));
|
|
541
|
+
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
542
|
+
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
543
|
+
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
544
|
+
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
545
|
+
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
546
|
+
console.log("\nHow often should the check run?");
|
|
547
|
+
console.log(" 1. Weekly (recommended for most sites)");
|
|
548
|
+
console.log(" 2. Daily (for high-traffic sites)");
|
|
549
|
+
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
550
|
+
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
551
|
+
const scheduleMap = {
|
|
552
|
+
"1": "weekly",
|
|
553
|
+
"2": "daily",
|
|
554
|
+
"3": "monthly"
|
|
555
|
+
};
|
|
556
|
+
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
557
|
+
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
558
|
+
if (siteUrl) {
|
|
559
|
+
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
560
|
+
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
561
|
+
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
console.log("Skipping GitHub Action setup.\n");
|
|
565
|
+
}
|
|
566
|
+
console.log("\u2500".repeat(60));
|
|
567
|
+
console.log("\u2705 Setup complete!\n");
|
|
568
|
+
console.log("Next steps:");
|
|
569
|
+
console.log(" 1. Commit the changes to your repository");
|
|
570
|
+
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
571
|
+
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
572
|
+
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
573
|
+
rl.close();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.log(`
|
|
576
|
+
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
577
|
+
`);
|
|
578
|
+
rl.close();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function setupGA4(projectPath, measurementId) {
|
|
582
|
+
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
583
|
+
const result = await (0, import_agent_runtime.injectGA4)(projectPath, { measurementId });
|
|
584
|
+
if (result.success) {
|
|
585
|
+
console.log(`\u2705 ${result.message}`);
|
|
586
|
+
if (result.file) {
|
|
587
|
+
console.log(` Modified: ${result.file}`);
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
591
|
+
if (result.code) {
|
|
592
|
+
console.log("\nManually add this code to your HTML <head>:\n");
|
|
593
|
+
console.log(result.code);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const envExamplePath = path.join(projectPath, ".env.example");
|
|
597
|
+
const envContent = (0, import_agent_runtime.generateGA4EnvTemplate)();
|
|
598
|
+
if (fs.existsSync(envExamplePath)) {
|
|
599
|
+
const existing = fs.readFileSync(envExamplePath, "utf-8");
|
|
600
|
+
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
601
|
+
fs.appendFileSync(envExamplePath, "\n" + envContent);
|
|
602
|
+
console.log(" Updated: .env.example");
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
fs.writeFileSync(envExamplePath, envContent);
|
|
606
|
+
console.log(" Created: .env.example");
|
|
607
|
+
}
|
|
608
|
+
console.log("");
|
|
609
|
+
}
|
|
610
|
+
async function setupGSC(projectPath, verificationCode) {
|
|
611
|
+
console.log("Setting up Google Search Console verification...");
|
|
612
|
+
const result = await (0, import_agent_runtime.injectGSCVerification)(projectPath, verificationCode);
|
|
613
|
+
if (result.success) {
|
|
614
|
+
console.log(`\u2705 ${result.message}`);
|
|
615
|
+
if (result.file) {
|
|
616
|
+
console.log(` Modified: ${result.file}`);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
620
|
+
}
|
|
621
|
+
console.log("");
|
|
622
|
+
}
|
|
623
|
+
async function setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard) {
|
|
624
|
+
console.log(`Setting up GitHub Action (${schedule} schedule)...`);
|
|
625
|
+
const config2 = {
|
|
626
|
+
schedule,
|
|
627
|
+
siteUrl: siteUrl || "https://example.com",
|
|
628
|
+
features: {
|
|
629
|
+
audit: true,
|
|
630
|
+
autoFix: true,
|
|
631
|
+
createIssues: true,
|
|
632
|
+
createPRs: true
|
|
633
|
+
},
|
|
634
|
+
syncToDashboard
|
|
635
|
+
};
|
|
636
|
+
const { files, instructions } = (0, import_agent_runtime.writeGitHubActionFiles)(projectPath, config2);
|
|
637
|
+
console.log("\u2705 GitHub Action files created:");
|
|
638
|
+
for (const file of files) {
|
|
639
|
+
console.log(` - ${path.relative(projectPath, file)}`);
|
|
640
|
+
}
|
|
641
|
+
console.log(instructions);
|
|
642
|
+
}
|
|
643
|
+
function showGSCInstructions() {
|
|
644
|
+
console.log((0, import_agent_runtime.getGSCSetupInstructions)());
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/commands/init.ts
|
|
648
|
+
var FRAMEWORKS = [
|
|
649
|
+
{ pattern: "next.config", name: "Next.js" },
|
|
650
|
+
{ pattern: "astro.config", name: "Astro" },
|
|
651
|
+
{ pattern: "nuxt.config", name: "Nuxt" },
|
|
652
|
+
{ pattern: "svelte.config", name: "SvelteKit" },
|
|
653
|
+
{ pattern: "gatsby-config", name: "Gatsby" },
|
|
654
|
+
{ pattern: "vite.config", name: "Vite" },
|
|
655
|
+
{ pattern: "hugo.toml", name: "Hugo" },
|
|
656
|
+
{ pattern: "_config.yml", name: "Jekyll" },
|
|
657
|
+
{ pattern: "angular.json", name: "Angular" },
|
|
658
|
+
{ pattern: "vue.config", name: "Vue CLI" }
|
|
659
|
+
];
|
|
660
|
+
var AI_KEY_ENV_VARS = [
|
|
661
|
+
{ env: "OPENAI_API_KEY", label: "OpenAI" },
|
|
662
|
+
{ env: "ANTHROPIC_API_KEY", label: "Anthropic" },
|
|
663
|
+
{ env: "OPENROUTER_API_KEY", label: "OpenRouter" },
|
|
664
|
+
{ env: "GROQ_API_KEY", label: "Groq" },
|
|
665
|
+
{ env: "GEMINI_API_KEY", label: "Gemini" },
|
|
666
|
+
{ env: "GOOGLE_API_KEY", label: "Gemini" }
|
|
667
|
+
];
|
|
668
|
+
function detectFramework() {
|
|
669
|
+
const cwd = process.cwd();
|
|
670
|
+
for (const fw of FRAMEWORKS) {
|
|
671
|
+
const patterns = [
|
|
672
|
+
`${fw.pattern}.js`,
|
|
673
|
+
`${fw.pattern}.ts`,
|
|
674
|
+
`${fw.pattern}.mjs`,
|
|
675
|
+
`${fw.pattern}.cjs`,
|
|
676
|
+
fw.pattern
|
|
677
|
+
];
|
|
678
|
+
for (const p of patterns) {
|
|
679
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(cwd, p))) {
|
|
680
|
+
return fw.name;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const pkgPath = (0, import_path.join)(cwd, "package.json");
|
|
685
|
+
if ((0, import_fs.existsSync)(pkgPath)) {
|
|
686
|
+
try {
|
|
687
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
|
|
688
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
689
|
+
if (deps["next"]) return "Next.js";
|
|
690
|
+
if (deps["astro"]) return "Astro";
|
|
691
|
+
if (deps["nuxt"]) return "Nuxt";
|
|
692
|
+
if (deps["@sveltejs/kit"]) return "SvelteKit";
|
|
693
|
+
if (deps["gatsby"]) return "Gatsby";
|
|
694
|
+
if (deps["react"]) return "React";
|
|
695
|
+
if (deps["vue"]) return "Vue";
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
function getProjectName() {
|
|
702
|
+
const cwd = process.cwd();
|
|
703
|
+
const pkgPath = (0, import_path.join)(cwd, "package.json");
|
|
704
|
+
if ((0, import_fs.existsSync)(pkgPath)) {
|
|
705
|
+
try {
|
|
706
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
|
|
707
|
+
if (pkg.name) return pkg.name;
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return cwd.split("/").pop() || "unknown";
|
|
712
|
+
}
|
|
713
|
+
function runGit(args) {
|
|
714
|
+
try {
|
|
715
|
+
return (0, import_child_process.execSync)(`git ${args}`, { cwd: process.cwd(), stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function parseGitHubRepo(remoteUrl) {
|
|
721
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
|
|
722
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
723
|
+
}
|
|
724
|
+
function detectConnectedAiKeys() {
|
|
725
|
+
const found = /* @__PURE__ */ new Set();
|
|
726
|
+
for (const { env, label } of AI_KEY_ENV_VARS) {
|
|
727
|
+
if (process.env[env]) found.add(label);
|
|
728
|
+
}
|
|
729
|
+
return Array.from(found);
|
|
730
|
+
}
|
|
731
|
+
async function init(options) {
|
|
732
|
+
console.log(import_chalk2.default.bold("\n\u{1F680} Initializing RankCLI...\n"));
|
|
733
|
+
const configPath = (0, import_path.join)(process.cwd(), "rankcli.config.json");
|
|
734
|
+
if ((0, import_fs.existsSync)(configPath)) {
|
|
735
|
+
console.log(import_chalk2.default.yellow("\u26A0\uFE0F Configuration file already exists: rankcli.config.json"));
|
|
736
|
+
console.log(import_chalk2.default.dim(' Run "rankcli audit" to analyze your project.\n'));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const framework = detectFramework();
|
|
740
|
+
const projectName = getProjectName();
|
|
741
|
+
const isGitRepo = runGit("rev-parse --is-inside-work-tree") === "true";
|
|
742
|
+
const remoteUrl = isGitRepo ? runGit("remote get-url origin") : null;
|
|
743
|
+
const githubRepo = remoteUrl ? parseGitHubRepo(remoteUrl) : null;
|
|
744
|
+
const connectedAiKeys = detectConnectedAiKeys();
|
|
745
|
+
console.log(`\u{1F4E6} Project: ${import_chalk2.default.cyan(projectName)}`);
|
|
746
|
+
console.log(`\u{1F527} Framework: ${import_chalk2.default.cyan(framework || "Unknown (static site?)")}`);
|
|
747
|
+
if (githubRepo) {
|
|
748
|
+
console.log(`\u{1F419} GitHub repo: ${import_chalk2.default.cyan(githubRepo)}`);
|
|
749
|
+
} else if (isGitRepo) {
|
|
750
|
+
console.log(import_chalk2.default.dim("\u{1F419} Git repo detected, but no GitHub remote found"));
|
|
751
|
+
}
|
|
752
|
+
if (connectedAiKeys.length > 0) {
|
|
753
|
+
console.log(`\u{1F511} AI key detected: ${import_chalk2.default.cyan(connectedAiKeys.join(", "))} (will be used automatically for --ai audits)`);
|
|
754
|
+
}
|
|
755
|
+
const config2 = {
|
|
756
|
+
name: projectName,
|
|
757
|
+
framework,
|
|
758
|
+
url: null,
|
|
759
|
+
githubRepo,
|
|
760
|
+
initialized: (/* @__PURE__ */ new Date()).toISOString()
|
|
761
|
+
};
|
|
762
|
+
(0, import_fs.writeFileSync)(configPath, JSON.stringify(config2, null, 2));
|
|
763
|
+
console.log(import_chalk2.default.green("\n\u2705 Created rankcli.config.json"));
|
|
764
|
+
if (options.yes) {
|
|
765
|
+
printNextSteps(githubRepo);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (!isLoggedIn()) {
|
|
769
|
+
console.log("");
|
|
770
|
+
const { shouldConnect } = await import_inquirer2.default.prompt([
|
|
771
|
+
{
|
|
772
|
+
type: "confirm",
|
|
773
|
+
name: "shouldConnect",
|
|
774
|
+
message: "Connect a RankCLI account? Unlocks a dashboard, audit history, and GitHub auto-fix PRs (opens your browser).",
|
|
775
|
+
default: true
|
|
776
|
+
}
|
|
777
|
+
]);
|
|
778
|
+
if (shouldConnect) {
|
|
779
|
+
await login({});
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
const { email } = getUserInfo();
|
|
783
|
+
console.log(import_chalk2.default.dim(`
|
|
784
|
+
Already connected as ${email}.`));
|
|
785
|
+
}
|
|
786
|
+
const workflowPath = (0, import_path.join)(process.cwd(), ".github", "workflows", "seo.yml");
|
|
787
|
+
if (githubRepo && !(0, import_fs.existsSync)(workflowPath)) {
|
|
788
|
+
console.log("");
|
|
789
|
+
console.log(import_chalk2.default.bold("\u{1F916} Automate SEO/GEO checks in CI"));
|
|
790
|
+
console.log(import_chalk2.default.dim(" Privacy: by default nothing is sent to RankCLI - the audit runs"));
|
|
791
|
+
console.log(import_chalk2.default.dim(" entirely inside your own GitHub Actions runner. If you opt into"));
|
|
792
|
+
console.log(import_chalk2.default.dim(" dashboard sync below, audit results (the URL and issues found,"));
|
|
793
|
+
console.log(import_chalk2.default.dim(" not your source code) are sent to your RankCLI account.\n"));
|
|
794
|
+
const { shouldSetupAction } = await import_inquirer2.default.prompt([
|
|
795
|
+
{
|
|
796
|
+
type: "confirm",
|
|
797
|
+
name: "shouldSetupAction",
|
|
798
|
+
message: `Set up a GitHub Action for ${githubRepo}?`,
|
|
799
|
+
default: true
|
|
800
|
+
}
|
|
801
|
+
]);
|
|
802
|
+
if (shouldSetupAction) {
|
|
803
|
+
const { siteUrl } = await import_inquirer2.default.prompt([
|
|
804
|
+
{
|
|
805
|
+
type: "input",
|
|
806
|
+
name: "siteUrl",
|
|
807
|
+
message: "Site URL to audit (https://example.com):",
|
|
808
|
+
validate: (input) => {
|
|
809
|
+
try {
|
|
810
|
+
new URL(input);
|
|
811
|
+
return true;
|
|
812
|
+
} catch {
|
|
813
|
+
return "Please enter a valid URL";
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
]);
|
|
818
|
+
const { schedule } = await import_inquirer2.default.prompt([
|
|
819
|
+
{
|
|
820
|
+
type: "list",
|
|
821
|
+
name: "schedule",
|
|
822
|
+
message: "How often should it run?",
|
|
823
|
+
choices: [
|
|
824
|
+
{ name: "Weekly (recommended)", value: "weekly" },
|
|
825
|
+
{ name: "Daily", value: "daily" },
|
|
826
|
+
{ name: "Monthly", value: "monthly" }
|
|
827
|
+
],
|
|
828
|
+
default: "weekly"
|
|
829
|
+
}
|
|
830
|
+
]);
|
|
831
|
+
let syncToDashboard = false;
|
|
832
|
+
if (isLoggedIn()) {
|
|
833
|
+
const answer = await import_inquirer2.default.prompt([
|
|
834
|
+
{
|
|
835
|
+
type: "confirm",
|
|
836
|
+
name: "syncToDashboard",
|
|
837
|
+
message: "Also send scheduled audits to your RankCLI dashboard?",
|
|
838
|
+
default: true
|
|
839
|
+
}
|
|
840
|
+
]);
|
|
841
|
+
syncToDashboard = answer.syncToDashboard;
|
|
842
|
+
}
|
|
843
|
+
await setupGitHubAction(process.cwd(), schedule, siteUrl, syncToDashboard);
|
|
844
|
+
config2.url = siteUrl;
|
|
845
|
+
(0, import_fs.writeFileSync)(configPath, JSON.stringify(config2, null, 2));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
printNextSteps(githubRepo);
|
|
849
|
+
}
|
|
850
|
+
function printNextSteps(githubRepo) {
|
|
851
|
+
console.log(import_chalk2.default.bold("\n\u{1F4CB} Next steps:"));
|
|
852
|
+
console.log(' 1. Run "rankcli audit --url <your-site>" for a full technical audit');
|
|
853
|
+
console.log(' 2. Run "rankcli apply --auto" to apply fixes locally');
|
|
854
|
+
if (!githubRepo) {
|
|
855
|
+
console.log(' 3. Push to GitHub, then re-run "rankcli init" to set up automated checks');
|
|
856
|
+
}
|
|
857
|
+
console.log("");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/commands/analyze.ts
|
|
861
|
+
var import_fs2 = require("fs");
|
|
862
|
+
var import_path2 = require("path");
|
|
863
|
+
var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
|
|
864
|
+
function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
|
|
865
|
+
if (depth > maxDepth) return [];
|
|
866
|
+
const files = [];
|
|
867
|
+
try {
|
|
868
|
+
const items = (0, import_fs2.readdirSync)(dir);
|
|
869
|
+
for (const item of items) {
|
|
870
|
+
if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const fullPath = (0, import_path2.join)(dir, item);
|
|
874
|
+
const stat = (0, import_fs2.statSync)(fullPath);
|
|
875
|
+
if (stat.isDirectory()) {
|
|
876
|
+
files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
|
|
877
|
+
} else if (extensions.includes((0, import_path2.extname)(item))) {
|
|
878
|
+
files.push(fullPath);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
return files;
|
|
884
|
+
}
|
|
885
|
+
function extractKeywords(text) {
|
|
886
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
887
|
+
const frequency = {};
|
|
888
|
+
for (const word of words) {
|
|
889
|
+
frequency[word] = (frequency[word] || 0) + 1;
|
|
890
|
+
}
|
|
891
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
892
|
+
"this",
|
|
893
|
+
"that",
|
|
894
|
+
"with",
|
|
895
|
+
"from",
|
|
896
|
+
"have",
|
|
897
|
+
"been",
|
|
898
|
+
"were",
|
|
899
|
+
"will",
|
|
900
|
+
"would",
|
|
901
|
+
"could",
|
|
902
|
+
"should",
|
|
903
|
+
"their",
|
|
904
|
+
"there",
|
|
905
|
+
"about",
|
|
906
|
+
"which",
|
|
907
|
+
"when",
|
|
908
|
+
"what",
|
|
909
|
+
"your",
|
|
910
|
+
"more",
|
|
911
|
+
"some",
|
|
912
|
+
"just",
|
|
913
|
+
"into",
|
|
914
|
+
"over",
|
|
915
|
+
"such",
|
|
916
|
+
"only",
|
|
917
|
+
"than",
|
|
918
|
+
"them",
|
|
919
|
+
"then",
|
|
920
|
+
"these",
|
|
921
|
+
"most",
|
|
922
|
+
"other",
|
|
923
|
+
"also",
|
|
924
|
+
"being",
|
|
925
|
+
"after",
|
|
926
|
+
"const",
|
|
927
|
+
"function",
|
|
928
|
+
"return",
|
|
929
|
+
"import",
|
|
930
|
+
"export",
|
|
931
|
+
"default",
|
|
932
|
+
"true",
|
|
933
|
+
"false"
|
|
934
|
+
]);
|
|
935
|
+
return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
|
|
936
|
+
}
|
|
937
|
+
function loadConfig() {
|
|
938
|
+
const configPath = (0, import_path2.join)(process.cwd(), "rankcli.config.json");
|
|
939
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
940
|
+
try {
|
|
941
|
+
return JSON.parse((0, import_fs2.readFileSync)(configPath, "utf-8"));
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
async function analyze(options) {
|
|
949
|
+
console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
|
|
950
|
+
const cwd = process.cwd();
|
|
951
|
+
const config2 = loadConfig();
|
|
952
|
+
const result = {
|
|
953
|
+
projectName: config2?.name || cwd.split("/").pop() || "unknown",
|
|
954
|
+
framework: config2?.framework || null,
|
|
955
|
+
hasReadme: false,
|
|
956
|
+
readmeSummary: null,
|
|
957
|
+
pageCount: 0,
|
|
958
|
+
hasMetaTags: false,
|
|
959
|
+
hasSitemap: false,
|
|
960
|
+
hasRobotsTxt: false,
|
|
961
|
+
keywords: [],
|
|
962
|
+
suggestions: []
|
|
963
|
+
};
|
|
964
|
+
const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
|
|
965
|
+
for (const rp of readmePaths) {
|
|
966
|
+
if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, rp))) {
|
|
967
|
+
result.hasReadme = true;
|
|
968
|
+
const content = (0, import_fs2.readFileSync)((0, import_path2.join)(cwd, rp), "utf-8");
|
|
969
|
+
result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
|
|
970
|
+
result.keywords.push(...extractKeywords(content));
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
result.hasSitemap = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "sitemap.xml")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "sitemap.xml"));
|
|
975
|
+
result.hasRobotsTxt = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "robots.txt")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "robots.txt"));
|
|
976
|
+
const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
|
|
977
|
+
result.pageCount = contentFiles.length;
|
|
978
|
+
const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
|
|
979
|
+
for (const hf of htmlFiles.slice(0, 5)) {
|
|
980
|
+
const content = (0, import_fs2.readFileSync)(hf, "utf-8");
|
|
981
|
+
if (content.includes("<meta") || content.includes("metadata")) {
|
|
982
|
+
result.hasMetaTags = true;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
for (const cf of contentFiles.slice(0, 10)) {
|
|
987
|
+
const content = (0, import_fs2.readFileSync)(cf, "utf-8");
|
|
988
|
+
result.keywords.push(...extractKeywords(content));
|
|
989
|
+
}
|
|
990
|
+
result.keywords = [...new Set(result.keywords)].slice(0, 20);
|
|
991
|
+
if (!result.hasReadme) {
|
|
992
|
+
result.suggestions.push("Add a README.md with clear project description");
|
|
993
|
+
}
|
|
994
|
+
if (!result.hasSitemap) {
|
|
995
|
+
result.suggestions.push("Generate a sitemap.xml for better crawlability");
|
|
996
|
+
}
|
|
997
|
+
if (!result.hasRobotsTxt) {
|
|
998
|
+
result.suggestions.push("Add robots.txt to guide search engine crawlers");
|
|
999
|
+
}
|
|
1000
|
+
if (!result.hasMetaTags) {
|
|
1001
|
+
result.suggestions.push("Add meta tags (title, description, og:*) to pages");
|
|
1002
|
+
}
|
|
1003
|
+
if (result.pageCount < 5) {
|
|
1004
|
+
result.suggestions.push("Consider adding more content pages for SEO coverage");
|
|
1005
|
+
}
|
|
1006
|
+
console.log("\u{1F4CA} Analysis Results\n");
|
|
1007
|
+
console.log(`Project: ${result.projectName}`);
|
|
1008
|
+
console.log(`Framework: ${result.framework || "Unknown"}`);
|
|
1009
|
+
console.log(`Content files: ${result.pageCount}`);
|
|
1010
|
+
console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
|
|
1011
|
+
console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
|
|
1012
|
+
console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
|
|
1013
|
+
console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
|
|
1014
|
+
if (result.keywords.length > 0) {
|
|
1015
|
+
console.log("\n\u{1F511} Detected Keywords:");
|
|
1016
|
+
console.log(` ${result.keywords.join(", ")}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (result.suggestions.length > 0) {
|
|
1019
|
+
console.log("\n\u{1F4A1} Suggestions:");
|
|
1020
|
+
for (const s of result.suggestions) {
|
|
1021
|
+
console.log(` \u2022 ${s}`);
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
console.log("\n\u2705 No critical SEO issues found!");
|
|
1025
|
+
}
|
|
1026
|
+
console.log('\n\u{1F4C8} Next: Run "rankcli audit --url <your-deployed-site>" for a full technical audit\n');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/commands/audit.ts
|
|
1030
|
+
var import_agent_runtime2 = require("@rankcli/agent-runtime");
|
|
1031
|
+
var import_ora2 = __toESM(require("ora"));
|
|
1032
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
1033
|
+
|
|
1034
|
+
// src/lib/upsell.ts
|
|
1035
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
1036
|
+
var WEB_APP_URL2 = "https://rankcli.dev";
|
|
1037
|
+
var TIER_LIMITS = {
|
|
1038
|
+
free_anon: {
|
|
1039
|
+
checks: 50,
|
|
1040
|
+
sites: 0,
|
|
1041
|
+
domains_per_month: 1,
|
|
1042
|
+
features: { geo: false, auto_fix: false, github: false, ai: false }
|
|
498
1043
|
},
|
|
499
1044
|
free: {
|
|
500
1045
|
checks: 100,
|
|
@@ -526,133 +1071,133 @@ var UPSELL_MESSAGES = {
|
|
|
526
1071
|
// After free audit completes
|
|
527
1072
|
free_audit_complete: () => [
|
|
528
1073
|
"",
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1074
|
+
import_chalk3.default.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"),
|
|
1075
|
+
import_chalk3.default.yellow.bold(" \u{1F680} Get 2x more checks + audit history"),
|
|
1076
|
+
import_chalk3.default.dim(" Create a free account to unlock 100 checks and save your results."),
|
|
532
1077
|
"",
|
|
533
|
-
|
|
534
|
-
|
|
1078
|
+
import_chalk3.default.cyan(` \u2192 rankcli login`),
|
|
1079
|
+
import_chalk3.default.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"),
|
|
535
1080
|
""
|
|
536
1081
|
],
|
|
537
1082
|
// When checks limit is reached
|
|
538
1083
|
checks_limit: (ctx) => [
|
|
539
1084
|
"",
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1085
|
+
import_chalk3.default.yellow(`\u26A0\uFE0F Running ${ctx.current || 50} of ${TIER_LIMITS.solo.checks} available checks`),
|
|
1086
|
+
import_chalk3.default.dim(` Unlock all ${TIER_LIMITS.solo.checks}+ checks with Solo ($9/mo)`),
|
|
1087
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_checks`),
|
|
543
1088
|
""
|
|
544
1089
|
],
|
|
545
1090
|
// When domain limit is reached
|
|
546
1091
|
domain_limit: (ctx) => [
|
|
547
1092
|
"",
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1093
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1094
|
+
import_chalk3.default.red.bold(" \u2717 Domain limit reached"),
|
|
1095
|
+
import_chalk3.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} domains per month.`),
|
|
551
1096
|
"",
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1097
|
+
import_chalk3.default.dim(" Upgrade to audit more sites:"),
|
|
1098
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_domain_limit`),
|
|
1099
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
555
1100
|
""
|
|
556
1101
|
],
|
|
557
1102
|
// When GEO features are limited
|
|
558
1103
|
geo_limited: (ctx) => [
|
|
559
1104
|
"",
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1105
|
+
import_chalk3.default.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"),
|
|
1106
|
+
import_chalk3.default.yellow.bold(" \u{1F916} Full GEO analysis requires Solo"),
|
|
1107
|
+
import_chalk3.default.dim(" You're seeing basic AI crawler access checks."),
|
|
1108
|
+
import_chalk3.default.dim(" Full analysis includes: LLM signals, citation readiness, content structure"),
|
|
564
1109
|
"",
|
|
565
|
-
|
|
566
|
-
|
|
1110
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_geo`),
|
|
1111
|
+
import_chalk3.default.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"),
|
|
567
1112
|
""
|
|
568
1113
|
],
|
|
569
1114
|
// When auto-fix is attempted without access
|
|
570
1115
|
auto_fix_blocked: () => [
|
|
571
1116
|
"",
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1117
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1118
|
+
import_chalk3.default.red.bold(" \u2717 Auto-fix requires Solo plan"),
|
|
1119
|
+
import_chalk3.default.dim(" Apply framework-specific SEO fixes directly to your codebase."),
|
|
575
1120
|
"",
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1121
|
+
import_chalk3.default.dim(" Solo ($9/mo) includes:"),
|
|
1122
|
+
import_chalk3.default.dim(" \u2022 Local auto-fix via CLI"),
|
|
1123
|
+
import_chalk3.default.dim(" \u2022 25+ framework support"),
|
|
1124
|
+
import_chalk3.default.dim(" \u2022 Full 280+ checks"),
|
|
580
1125
|
"",
|
|
581
|
-
|
|
582
|
-
|
|
1126
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_autofix`),
|
|
1127
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
583
1128
|
""
|
|
584
1129
|
],
|
|
585
1130
|
// When GitHub integration is attempted without access
|
|
586
1131
|
github_blocked: () => [
|
|
587
1132
|
"",
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1133
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1134
|
+
import_chalk3.default.red.bold(" \u2717 GitHub auto-fix PRs require Pro"),
|
|
1135
|
+
import_chalk3.default.dim(" Automatically create fix PRs when SEO issues are detected."),
|
|
591
1136
|
"",
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1137
|
+
import_chalk3.default.dim(" Pro ($29/mo) includes:"),
|
|
1138
|
+
import_chalk3.default.dim(" \u2022 GitHub App integration"),
|
|
1139
|
+
import_chalk3.default.dim(" \u2022 Auto-fix PRs"),
|
|
1140
|
+
import_chalk3.default.dim(" \u2022 Daily scheduled audits"),
|
|
1141
|
+
import_chalk3.default.dim(" \u2022 Slack/Discord alerts"),
|
|
597
1142
|
"",
|
|
598
|
-
|
|
599
|
-
|
|
1143
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_github`),
|
|
1144
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
600
1145
|
""
|
|
601
1146
|
],
|
|
602
1147
|
// Keyword tracking limit reached
|
|
603
1148
|
keyword_limit: (ctx) => [
|
|
604
1149
|
"",
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1150
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1151
|
+
import_chalk3.default.red.bold(" \u2717 Keyword tracking limit reached"),
|
|
1152
|
+
import_chalk3.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit || ctx.max} tracked keywords.`),
|
|
1153
|
+
import_chalk3.default.dim(` Currently tracking: ${ctx.current}`),
|
|
609
1154
|
"",
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1155
|
+
import_chalk3.default.dim(" Upgrade for more keywords:"),
|
|
1156
|
+
import_chalk3.default.dim(" \u2022 Solo ($9/mo): 100 keywords"),
|
|
1157
|
+
import_chalk3.default.dim(" \u2022 Pro ($29/mo): 500 keywords"),
|
|
1158
|
+
import_chalk3.default.dim(" \u2022 Agency ($99/mo): 2,000 keywords"),
|
|
614
1159
|
"",
|
|
615
|
-
|
|
616
|
-
|
|
1160
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_keyword_limit`),
|
|
1161
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
617
1162
|
""
|
|
618
1163
|
],
|
|
619
1164
|
// Rate limit exceeded
|
|
620
1165
|
rate_limit: (ctx) => [
|
|
621
1166
|
"",
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1167
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
1168
|
+
import_chalk3.default.red.bold(" \u2717 API rate limit exceeded"),
|
|
1169
|
+
import_chalk3.default.dim(` Your ${ctx.tier} plan allows ${ctx.max} requests/day.`),
|
|
625
1170
|
"",
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1171
|
+
import_chalk3.default.dim(" Need more? Upgrade for higher limits:"),
|
|
1172
|
+
import_chalk3.default.dim(" \u2022 Solo: 100 req/day"),
|
|
1173
|
+
import_chalk3.default.dim(" \u2022 Pro: 1,000 req/day"),
|
|
1174
|
+
import_chalk3.default.dim(" \u2022 Agency: 10,000 req/day"),
|
|
630
1175
|
"",
|
|
631
|
-
|
|
632
|
-
|
|
1176
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=cli_rate_limit`),
|
|
1177
|
+
import_chalk3.default.red("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),
|
|
633
1178
|
""
|
|
634
1179
|
],
|
|
635
1180
|
// Schedule exceeded (for GitHub Action)
|
|
636
1181
|
schedule_exceeded: (ctx) => [
|
|
637
1182
|
"",
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1183
|
+
import_chalk3.default.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"),
|
|
1184
|
+
import_chalk3.default.yellow.bold(" \u23F0 Scheduled audit skipped"),
|
|
1185
|
+
import_chalk3.default.dim(` Your ${ctx.tier} plan allows ${ctx.limit} scheduled audits.`),
|
|
641
1186
|
"",
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1187
|
+
import_chalk3.default.dim(" Upgrade for more frequent audits:"),
|
|
1188
|
+
import_chalk3.default.dim(" \u2022 Solo: Weekly"),
|
|
1189
|
+
import_chalk3.default.dim(" \u2022 Pro: Daily"),
|
|
1190
|
+
import_chalk3.default.dim(" \u2022 Agency: Hourly"),
|
|
646
1191
|
"",
|
|
647
|
-
|
|
648
|
-
|
|
1192
|
+
import_chalk3.default.cyan(` \u2192 ${WEB_APP_URL2}/pricing?from=schedule_exceeded`),
|
|
1193
|
+
import_chalk3.default.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"),
|
|
649
1194
|
""
|
|
650
1195
|
],
|
|
651
1196
|
// Login prompt (soft)
|
|
652
1197
|
login_prompt: () => [
|
|
653
1198
|
"",
|
|
654
|
-
|
|
655
|
-
|
|
1199
|
+
import_chalk3.default.dim(" \u{1F4A1} Tip: Log in to sync results and track progress over time:"),
|
|
1200
|
+
import_chalk3.default.dim(" rankcli login"),
|
|
656
1201
|
""
|
|
657
1202
|
]
|
|
658
1203
|
};
|
|
@@ -673,8 +1218,8 @@ function hasFeature(tier, feature) {
|
|
|
673
1218
|
|
|
674
1219
|
// src/wasm-engine.ts
|
|
675
1220
|
var vm = __toESM(require("vm"));
|
|
676
|
-
var
|
|
677
|
-
var
|
|
1221
|
+
var path2 = __toESM(require("path"));
|
|
1222
|
+
var fs2 = __toESM(require("fs"));
|
|
678
1223
|
var os = __toESM(require("os"));
|
|
679
1224
|
var PREMIUM_CHECK_NAMES = [
|
|
680
1225
|
"analyzeFeaturedSnippet",
|
|
@@ -711,12 +1256,12 @@ async function loadWasm() {
|
|
|
711
1256
|
return wasmModule;
|
|
712
1257
|
}
|
|
713
1258
|
const possiblePaths = [
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1259
|
+
path2.join(__dirname, "../native/pkg/rankcli_audit_engine.js"),
|
|
1260
|
+
path2.join(__dirname, "../wasm/rankcli_audit_engine.js"),
|
|
1261
|
+
path2.join(__dirname, "../../native/pkg/rankcli_audit_engine.js")
|
|
717
1262
|
];
|
|
718
1263
|
for (const wasmPath of possiblePaths) {
|
|
719
|
-
if (
|
|
1264
|
+
if (fs2.existsSync(wasmPath)) {
|
|
720
1265
|
wasmModule = require(wasmPath);
|
|
721
1266
|
return wasmModule;
|
|
722
1267
|
}
|
|
@@ -728,8 +1273,8 @@ Searched: ${possiblePaths.join(", ")}`
|
|
|
728
1273
|
}
|
|
729
1274
|
function extractSeaWasmAssets() {
|
|
730
1275
|
const sea = require("sea");
|
|
731
|
-
const dir =
|
|
732
|
-
|
|
1276
|
+
const dir = path2.join(os.tmpdir(), "rankcli-wasm-sea");
|
|
1277
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
733
1278
|
const glueAsset = sea.getRawAsset("rankcli_audit_engine.js");
|
|
734
1279
|
const wasmAsset = sea.getRawAsset("rankcli_audit_engine_bg.wasm");
|
|
735
1280
|
if (typeof glueAsset === "string" || typeof wasmAsset === "string") {
|
|
@@ -737,12 +1282,12 @@ function extractSeaWasmAssets() {
|
|
|
737
1282
|
}
|
|
738
1283
|
const glueBuffer = Buffer.from(glueAsset);
|
|
739
1284
|
const wasmBuffer = Buffer.from(wasmAsset);
|
|
740
|
-
const gluePath =
|
|
741
|
-
const wasmPath =
|
|
742
|
-
const existingSize =
|
|
1285
|
+
const gluePath = path2.join(dir, "rankcli_audit_engine.js");
|
|
1286
|
+
const wasmPath = path2.join(dir, "rankcli_audit_engine_bg.wasm");
|
|
1287
|
+
const existingSize = fs2.existsSync(wasmPath) ? fs2.statSync(wasmPath).size : -1;
|
|
743
1288
|
if (existingSize !== wasmBuffer.length) {
|
|
744
|
-
|
|
745
|
-
|
|
1289
|
+
fs2.writeFileSync(gluePath, glueBuffer);
|
|
1290
|
+
fs2.writeFileSync(wasmPath, wasmBuffer);
|
|
746
1291
|
}
|
|
747
1292
|
return gluePath;
|
|
748
1293
|
}
|
|
@@ -837,7 +1382,7 @@ async function getPremiumChecks() {
|
|
|
837
1382
|
}
|
|
838
1383
|
|
|
839
1384
|
// src/commands/audit.ts
|
|
840
|
-
var { detectFramework: detectFramework2, getFrameworkDisplayName } =
|
|
1385
|
+
var { detectFramework: detectFramework2, getFrameworkDisplayName } = import_agent_runtime2.frameworks;
|
|
841
1386
|
async function fetchHtml(url, timeout = 1e4) {
|
|
842
1387
|
try {
|
|
843
1388
|
const controller = new AbortController();
|
|
@@ -858,9 +1403,16 @@ async function fetchHtml(url, timeout = 1e4) {
|
|
|
858
1403
|
return null;
|
|
859
1404
|
}
|
|
860
1405
|
}
|
|
1406
|
+
var AI_PROVIDER_DEFAULTS = {
|
|
1407
|
+
openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4o-mini" },
|
|
1408
|
+
openrouter: { baseUrl: "https://openrouter.ai/api/v1", model: "deepseek/deepseek-r1:free" },
|
|
1409
|
+
groq: { baseUrl: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile" },
|
|
1410
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", model: "gemini-2.0-flash" }
|
|
1411
|
+
};
|
|
861
1412
|
async function runAIAnalysis(url, issues, provider, apiKey) {
|
|
862
1413
|
const isAnthropic = provider === "anthropic";
|
|
863
|
-
const
|
|
1414
|
+
const providerConfig = AI_PROVIDER_DEFAULTS[provider] || AI_PROVIDER_DEFAULTS.openai;
|
|
1415
|
+
const apiUrl = isAnthropic ? "https://api.anthropic.com/v1/messages" : `${providerConfig.baseUrl}/chat/completions`;
|
|
864
1416
|
const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
|
|
865
1417
|
|
|
866
1418
|
URL: ${url}
|
|
@@ -891,19 +1443,26 @@ Return JSON with:
|
|
|
891
1443
|
});
|
|
892
1444
|
} else {
|
|
893
1445
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1446
|
+
if (provider === "openrouter") {
|
|
1447
|
+
headers["HTTP-Referer"] = "https://rankcli.dev";
|
|
1448
|
+
headers["X-Title"] = "RankCLI";
|
|
1449
|
+
}
|
|
894
1450
|
body = JSON.stringify({
|
|
895
|
-
model:
|
|
1451
|
+
model: providerConfig.model,
|
|
896
1452
|
messages: [
|
|
897
1453
|
{ role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
|
|
898
1454
|
{ role: "user", content: prompt }
|
|
899
1455
|
],
|
|
900
1456
|
max_tokens: 1500,
|
|
901
|
-
|
|
1457
|
+
// json_object mode is an OpenAI-specific extension - Groq supports
|
|
1458
|
+
// it too, but not every OpenAI-compatible server does, so only
|
|
1459
|
+
// request it for providers confirmed to support it.
|
|
1460
|
+
...provider === "openai" || provider === "groq" ? { response_format: { type: "json_object" } } : {}
|
|
902
1461
|
});
|
|
903
1462
|
}
|
|
904
1463
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
905
1464
|
if (!response.ok) {
|
|
906
|
-
console.error(
|
|
1465
|
+
console.error(import_chalk4.default.yellow(`AI analysis failed: ${response.status}`));
|
|
907
1466
|
return null;
|
|
908
1467
|
}
|
|
909
1468
|
const data = await response.json();
|
|
@@ -913,7 +1472,7 @@ Return JSON with:
|
|
|
913
1472
|
if (!jsonMatch) return null;
|
|
914
1473
|
return JSON.parse(jsonMatch[0]);
|
|
915
1474
|
} catch (error) {
|
|
916
|
-
console.error(
|
|
1475
|
+
console.error(import_chalk4.default.yellow(`AI analysis error: ${error}`));
|
|
917
1476
|
return null;
|
|
918
1477
|
}
|
|
919
1478
|
}
|
|
@@ -948,9 +1507,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
948
1507
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
949
1508
|
const limit = sitesLimit ?? 1;
|
|
950
1509
|
if (count !== null && count >= limit) {
|
|
951
|
-
console.log(
|
|
1510
|
+
console.log(import_chalk4.default.yellow(`
|
|
952
1511
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
953
|
-
console.log(
|
|
1512
|
+
console.log(import_chalk4.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
954
1513
|
return null;
|
|
955
1514
|
}
|
|
956
1515
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -960,7 +1519,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
960
1519
|
domain
|
|
961
1520
|
}).select("id").single();
|
|
962
1521
|
if (projectError || !newProject) {
|
|
963
|
-
console.error(
|
|
1522
|
+
console.error(import_chalk4.default.dim("Failed to create project"));
|
|
964
1523
|
return null;
|
|
965
1524
|
}
|
|
966
1525
|
projectId = newProject.id;
|
|
@@ -1001,7 +1560,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
1001
1560
|
ai_analysis: aiAnalysis
|
|
1002
1561
|
}).select("id").single();
|
|
1003
1562
|
if (auditError || !audit2) {
|
|
1004
|
-
console.error(
|
|
1563
|
+
console.error(import_chalk4.default.dim("Failed to save audit"));
|
|
1005
1564
|
return null;
|
|
1006
1565
|
}
|
|
1007
1566
|
if (mergedIssues.length > 0) {
|
|
@@ -1029,7 +1588,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
1029
1588
|
}).eq("id", projectId);
|
|
1030
1589
|
return { projectId, auditId: audit2.id };
|
|
1031
1590
|
} catch (error) {
|
|
1032
|
-
console.error(
|
|
1591
|
+
console.error(import_chalk4.default.dim("Cloud sync failed"));
|
|
1033
1592
|
return null;
|
|
1034
1593
|
}
|
|
1035
1594
|
}
|
|
@@ -1037,8 +1596,16 @@ async function audit(options) {
|
|
|
1037
1596
|
const url = options.url;
|
|
1038
1597
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
1039
1598
|
const enableAI = options.ai || false;
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1599
|
+
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");
|
|
1600
|
+
const aiProvider = detectedProvider;
|
|
1601
|
+
const providerEnvKey = {
|
|
1602
|
+
openai: process.env.OPENAI_API_KEY,
|
|
1603
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
1604
|
+
openrouter: process.env.OPENROUTER_API_KEY,
|
|
1605
|
+
groq: process.env.GROQ_API_KEY,
|
|
1606
|
+
gemini: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
|
1607
|
+
};
|
|
1608
|
+
const aiKey = options.aiKey || providerEnvKey[aiProvider] || "";
|
|
1042
1609
|
const loggedIn = isLoggedIn();
|
|
1043
1610
|
const { planId } = getUserInfo();
|
|
1044
1611
|
const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
|
|
@@ -1051,7 +1618,7 @@ async function audit(options) {
|
|
|
1051
1618
|
await initEngine(passphrase);
|
|
1052
1619
|
premiumChecks = await getPremiumChecks();
|
|
1053
1620
|
} catch (error) {
|
|
1054
|
-
console.error(
|
|
1621
|
+
console.error(import_chalk4.default.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
|
|
1055
1622
|
}
|
|
1056
1623
|
}
|
|
1057
1624
|
}
|
|
@@ -1061,12 +1628,12 @@ async function audit(options) {
|
|
|
1061
1628
|
console.log("\nOptions:");
|
|
1062
1629
|
console.log(" --max-pages <n> Max pages to crawl (default: 5)");
|
|
1063
1630
|
console.log(" --ai Enable AI analysis");
|
|
1064
|
-
console.log(" --ai-provider <p> AI provider: openai or
|
|
1065
|
-
console.log(" --ai-key <key> API key (or set OPENAI_API_KEY
|
|
1631
|
+
console.log(" --ai-provider <p> AI provider: openai, anthropic, openrouter, groq, or gemini (auto-detected from env if omitted)");
|
|
1632
|
+
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)");
|
|
1066
1633
|
console.log(" --output json Output as JSON");
|
|
1067
1634
|
if (!loggedIn) {
|
|
1068
|
-
console.log(
|
|
1069
|
-
console.log(
|
|
1635
|
+
console.log(import_chalk4.default.dim("\nTip: Log in to sync results to the web dashboard:"));
|
|
1636
|
+
console.log(import_chalk4.default.dim(" rankcli login"));
|
|
1070
1637
|
}
|
|
1071
1638
|
console.log("");
|
|
1072
1639
|
return;
|
|
@@ -1079,13 +1646,13 @@ async function audit(options) {
|
|
|
1079
1646
|
}
|
|
1080
1647
|
const startTime = Date.now();
|
|
1081
1648
|
try {
|
|
1082
|
-
const discoverySpinner = (0,
|
|
1649
|
+
const discoverySpinner = (0, import_ora2.default)("Discovering pages...").start();
|
|
1083
1650
|
let pagesToAudit = [url];
|
|
1084
1651
|
const homepageHtml = await fetchHtml(url, 1e4);
|
|
1085
1652
|
let detectedFramework = detectFramework2({});
|
|
1086
1653
|
if (homepageHtml) {
|
|
1087
1654
|
detectedFramework = detectFramework2({ html: homepageHtml });
|
|
1088
|
-
const discovered = (0,
|
|
1655
|
+
const discovered = (0, import_agent_runtime2.discoverPagesFromLinks)(url, homepageHtml, maxPages - 1);
|
|
1089
1656
|
const additionalPages = discovered.map((p) => p.url).filter((u) => u !== url);
|
|
1090
1657
|
pagesToAudit = [url, ...additionalPages];
|
|
1091
1658
|
const frameworkLabel = detectedFramework.framework !== "unknown" ? ` (${getFrameworkDisplayName(detectedFramework.framework)})` : "";
|
|
@@ -1093,10 +1660,10 @@ async function audit(options) {
|
|
|
1093
1660
|
} else {
|
|
1094
1661
|
discoverySpinner.warn("Could not fetch homepage, auditing URL only");
|
|
1095
1662
|
}
|
|
1096
|
-
const auditSpinner = (0,
|
|
1663
|
+
const auditSpinner = (0, import_ora2.default)(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
|
|
1097
1664
|
const auditPage = async (pageUrl, index) => {
|
|
1098
1665
|
try {
|
|
1099
|
-
const report = await (0,
|
|
1666
|
+
const report = await (0, import_agent_runtime2.runFullAudit)({
|
|
1100
1667
|
url: pageUrl,
|
|
1101
1668
|
maxPages: 1,
|
|
1102
1669
|
checkBrokenLinks: options.checkLinks || false,
|
|
@@ -1140,7 +1707,7 @@ async function audit(options) {
|
|
|
1140
1707
|
const mergedIssues = mergeIssues(allIssues);
|
|
1141
1708
|
let aiAnalysis = null;
|
|
1142
1709
|
if (enableAI && aiKey) {
|
|
1143
|
-
const aiSpinner = (0,
|
|
1710
|
+
const aiSpinner = (0, import_ora2.default)(`Running AI analysis (${aiProvider})...`).start();
|
|
1144
1711
|
aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
|
|
1145
1712
|
if (aiAnalysis) {
|
|
1146
1713
|
aiSpinner.succeed("AI analysis complete");
|
|
@@ -1148,8 +1715,8 @@ async function audit(options) {
|
|
|
1148
1715
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
1149
1716
|
}
|
|
1150
1717
|
} else if (enableAI && !aiKey) {
|
|
1151
|
-
console.log(
|
|
1152
|
-
console.log(
|
|
1718
|
+
console.log(import_chalk4.default.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
|
|
1719
|
+
console.log(import_chalk4.default.dim(" Set OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, GROQ_API_KEY, or GEMINI_API_KEY, or use --ai-key"));
|
|
1153
1720
|
}
|
|
1154
1721
|
const totalTime = Date.now() - startTime;
|
|
1155
1722
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
@@ -1177,27 +1744,27 @@ async function audit(options) {
|
|
|
1177
1744
|
};
|
|
1178
1745
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1179
1746
|
} else {
|
|
1180
|
-
console.log("\n" +
|
|
1181
|
-
console.log(
|
|
1182
|
-
console.log(
|
|
1747
|
+
console.log("\n" + import_chalk4.default.bold("\u2550".repeat(60)));
|
|
1748
|
+
console.log(import_chalk4.default.bold.cyan(` SEO AUDIT REPORT: ${url}`));
|
|
1749
|
+
console.log(import_chalk4.default.bold("\u2550".repeat(60)));
|
|
1183
1750
|
const score = primaryReport.healthScore?.overall || 0;
|
|
1184
|
-
const scoreColor = score >= 80 ?
|
|
1751
|
+
const scoreColor = score >= 80 ? import_chalk4.default.green : score >= 60 ? import_chalk4.default.yellow : import_chalk4.default.red;
|
|
1185
1752
|
console.log(`
|
|
1186
|
-
${
|
|
1187
|
-
console.log(` ${
|
|
1188
|
-
console.log(` ${
|
|
1189
|
-
console.log(` ${
|
|
1753
|
+
${import_chalk4.default.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
|
|
1754
|
+
console.log(` ${import_chalk4.default.bold("Pages audited:")} ${successCount}`);
|
|
1755
|
+
console.log(` ${import_chalk4.default.bold("Issues found:")} ${mergedIssues.length}`);
|
|
1756
|
+
console.log(` ${import_chalk4.default.bold("Time:")} ${totalTime}ms`);
|
|
1190
1757
|
if (frameworkName) {
|
|
1191
|
-
console.log(` ${
|
|
1758
|
+
console.log(` ${import_chalk4.default.bold("Framework:")} ${import_chalk4.default.cyan(frameworkName)}`);
|
|
1192
1759
|
}
|
|
1193
1760
|
if (primaryReport.healthScore) {
|
|
1194
|
-
console.log("\n" +
|
|
1761
|
+
console.log("\n" + import_chalk4.default.bold(" Health Scores:"));
|
|
1195
1762
|
const hs = primaryReport.healthScore;
|
|
1196
1763
|
const bar = (score2) => {
|
|
1197
1764
|
const filled = Math.round(score2 / 5);
|
|
1198
1765
|
const empty = 20 - filled;
|
|
1199
|
-
const color = score2 >= 80 ?
|
|
1200
|
-
return color("\u2588".repeat(filled)) +
|
|
1766
|
+
const color = score2 >= 80 ? import_chalk4.default.green : score2 >= 60 ? import_chalk4.default.yellow : import_chalk4.default.red;
|
|
1767
|
+
return color("\u2588".repeat(filled)) + import_chalk4.default.gray("\u2591".repeat(empty));
|
|
1201
1768
|
};
|
|
1202
1769
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
1203
1770
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
@@ -1210,35 +1777,35 @@ async function audit(options) {
|
|
|
1210
1777
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
1211
1778
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
1212
1779
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
1213
|
-
console.log("\n" +
|
|
1214
|
-
console.log(` ${
|
|
1215
|
-
console.log(` ${
|
|
1216
|
-
console.log(` ${
|
|
1217
|
-
console.log("\n" +
|
|
1780
|
+
console.log("\n" + import_chalk4.default.bold(" Issues by Severity:"));
|
|
1781
|
+
console.log(` ${import_chalk4.default.red("\u25CF")} Errors: ${errors.length}`);
|
|
1782
|
+
console.log(` ${import_chalk4.default.yellow("\u25CF")} Warnings: ${warnings.length}`);
|
|
1783
|
+
console.log(` ${import_chalk4.default.blue("\u25CF")} Notices: ${notices.length}`);
|
|
1784
|
+
console.log("\n" + import_chalk4.default.bold(" Top Issues:"));
|
|
1218
1785
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
1219
1786
|
topIssues.forEach((issue, i) => {
|
|
1220
|
-
const icon = issue.severity === "error" ?
|
|
1787
|
+
const icon = issue.severity === "error" ? import_chalk4.default.red("\u2717") : issue.severity === "warning" ? import_chalk4.default.yellow("!") : import_chalk4.default.blue("i");
|
|
1221
1788
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
1222
1789
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
1223
|
-
console.log(
|
|
1790
|
+
console.log(import_chalk4.default.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
1224
1791
|
}
|
|
1225
1792
|
});
|
|
1226
1793
|
if (aiAnalysis) {
|
|
1227
|
-
console.log("\n" +
|
|
1794
|
+
console.log("\n" + import_chalk4.default.bold(" AI Analysis:"));
|
|
1228
1795
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
1229
|
-
console.log("\n " +
|
|
1796
|
+
console.log("\n " + import_chalk4.default.bold("Suggestions:"));
|
|
1230
1797
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
1231
1798
|
console.log(` ${i + 1}. ${s}`);
|
|
1232
1799
|
});
|
|
1233
1800
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
1234
|
-
console.log("\n " +
|
|
1801
|
+
console.log("\n " + import_chalk4.default.bold("Priority Fixes:"));
|
|
1235
1802
|
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1236
1803
|
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1237
|
-
console.log(
|
|
1804
|
+
console.log(import_chalk4.default.dim(` ${fix2.rationale}`));
|
|
1238
1805
|
});
|
|
1239
1806
|
}
|
|
1240
1807
|
}
|
|
1241
|
-
console.log("\n" +
|
|
1808
|
+
console.log("\n" + import_chalk4.default.bold("\u2550".repeat(60)));
|
|
1242
1809
|
const userInfo = getUserInfo();
|
|
1243
1810
|
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1244
1811
|
const isFreeTier = !loggedIn || tier === "free";
|
|
@@ -1254,13 +1821,13 @@ async function audit(options) {
|
|
|
1254
1821
|
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1255
1822
|
}
|
|
1256
1823
|
if (loggedIn) {
|
|
1257
|
-
const syncSpinner = (0,
|
|
1824
|
+
const syncSpinner = (0, import_ora2.default)("Syncing to cloud...").start();
|
|
1258
1825
|
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
1259
1826
|
if (syncResult) {
|
|
1260
1827
|
syncSpinner.succeed("Results synced to cloud");
|
|
1261
|
-
console.log(
|
|
1828
|
+
console.log(import_chalk4.default.cyan(`
|
|
1262
1829
|
View and manage issues in the web dashboard:`));
|
|
1263
|
-
console.log(
|
|
1830
|
+
console.log(import_chalk4.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
1264
1831
|
`));
|
|
1265
1832
|
} else {
|
|
1266
1833
|
syncSpinner.warn("Could not sync to cloud");
|
|
@@ -1279,7 +1846,7 @@ async function audit(options) {
|
|
|
1279
1846
|
// src/commands/apply.ts
|
|
1280
1847
|
var import_fs3 = require("fs");
|
|
1281
1848
|
var import_path3 = require("path");
|
|
1282
|
-
var
|
|
1849
|
+
var import_agent_runtime3 = require("@rankcli/agent-runtime");
|
|
1283
1850
|
var SEVERITY_ICON = {
|
|
1284
1851
|
critical: "\u274C",
|
|
1285
1852
|
warning: "\u26A0\uFE0F",
|
|
@@ -1307,7 +1874,7 @@ function truncate(str, maxLen) {
|
|
|
1307
1874
|
}
|
|
1308
1875
|
async function apply(options) {
|
|
1309
1876
|
const cwd = process.cwd();
|
|
1310
|
-
const configPath = (0, import_path3.join)(cwd, "
|
|
1877
|
+
const configPath = (0, import_path3.join)(cwd, "rankcli.config.json");
|
|
1311
1878
|
let config2 = null;
|
|
1312
1879
|
if ((0, import_fs3.existsSync)(configPath)) {
|
|
1313
1880
|
try {
|
|
@@ -1321,8 +1888,8 @@ async function apply(options) {
|
|
|
1321
1888
|
}
|
|
1322
1889
|
if (!url) {
|
|
1323
1890
|
console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
|
|
1324
|
-
console.log("
|
|
1325
|
-
console.log("\n Or set a URL in your
|
|
1891
|
+
console.log(" rankcli apply --url https://your-site.com");
|
|
1892
|
+
console.log("\n Or set a URL in your rankcli.config.json\n");
|
|
1326
1893
|
return;
|
|
1327
1894
|
}
|
|
1328
1895
|
try {
|
|
@@ -1331,7 +1898,7 @@ async function apply(options) {
|
|
|
1331
1898
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1332
1899
|
return;
|
|
1333
1900
|
}
|
|
1334
|
-
console.log("\n\u{1F680}
|
|
1901
|
+
console.log("\n\u{1F680} RankCLI - Apply Fixes");
|
|
1335
1902
|
console.log("\u2550".repeat(50));
|
|
1336
1903
|
const loggedIn = isLoggedIn();
|
|
1337
1904
|
const userInfo = getUserInfo();
|
|
@@ -1342,7 +1909,7 @@ async function apply(options) {
|
|
|
1342
1909
|
return;
|
|
1343
1910
|
}
|
|
1344
1911
|
console.log("\n\u{1F50D} Detecting framework...");
|
|
1345
|
-
const frameworkResult = await (0,
|
|
1912
|
+
const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
|
|
1346
1913
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1347
1914
|
console.log(` Framework: ${framework.name}`);
|
|
1348
1915
|
console.log(`
|
|
@@ -1352,7 +1919,7 @@ async function apply(options) {
|
|
|
1352
1919
|
let score;
|
|
1353
1920
|
let auditResult = null;
|
|
1354
1921
|
try {
|
|
1355
|
-
auditResult = await (0,
|
|
1922
|
+
auditResult = await (0, import_agent_runtime3.runAuditWithFixes)({
|
|
1356
1923
|
url,
|
|
1357
1924
|
generateFixes: true
|
|
1358
1925
|
});
|
|
@@ -1384,7 +1951,7 @@ async function apply(options) {
|
|
|
1384
1951
|
return;
|
|
1385
1952
|
}
|
|
1386
1953
|
console.log("\n\u{1F527} Generating fixes...");
|
|
1387
|
-
const fixes = await (0,
|
|
1954
|
+
const fixes = await (0, import_agent_runtime3.generateFixes)(issues, {
|
|
1388
1955
|
cwd,
|
|
1389
1956
|
url,
|
|
1390
1957
|
framework
|
|
@@ -1423,7 +1990,7 @@ async function apply(options) {
|
|
|
1423
1990
|
}
|
|
1424
1991
|
}
|
|
1425
1992
|
console.log("\n\u{1F527} Applying fixes...");
|
|
1426
|
-
const { applied, skipped } = await (0,
|
|
1993
|
+
const { applied, skipped } = await (0, import_agent_runtime3.applyFixes)(fixes, { cwd, dryRun: false });
|
|
1427
1994
|
console.log("\n" + "\u2550".repeat(50));
|
|
1428
1995
|
console.log("RESULTS");
|
|
1429
1996
|
console.log("\u2550".repeat(50));
|
|
@@ -1454,17 +2021,17 @@ async function apply(options) {
|
|
|
1454
2021
|
console.log("\n2. Commit the SEO improvements:");
|
|
1455
2022
|
console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
|
|
1456
2023
|
console.log("\n3. Re-run the audit to verify:");
|
|
1457
|
-
console.log(`
|
|
2024
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1458
2025
|
console.log("");
|
|
1459
2026
|
}
|
|
1460
2027
|
|
|
1461
2028
|
// src/commands/fix.ts
|
|
1462
|
-
var
|
|
2029
|
+
var import_child_process2 = require("child_process");
|
|
1463
2030
|
var import_util = require("util");
|
|
1464
|
-
var
|
|
1465
|
-
var
|
|
1466
|
-
var
|
|
1467
|
-
var execAsync = (0, import_util.promisify)(
|
|
2031
|
+
var import_agent_runtime4 = require("@rankcli/agent-runtime");
|
|
2032
|
+
var import_ora3 = __toESM(require("ora"));
|
|
2033
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
2034
|
+
var execAsync = (0, import_util.promisify)(import_child_process2.exec);
|
|
1468
2035
|
var SEVERITY_ICON2 = {
|
|
1469
2036
|
critical: "\u274C",
|
|
1470
2037
|
error: "\u274C",
|
|
@@ -1530,19 +2097,19 @@ function printFix2(fix2, index) {
|
|
|
1530
2097
|
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1531
2098
|
console.log(`
|
|
1532
2099
|
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1533
|
-
console.log(` Code: ${
|
|
1534
|
-
console.log(` File: ${
|
|
2100
|
+
console.log(` Code: ${import_chalk5.default.dim(fix2.issue.code)}`);
|
|
2101
|
+
console.log(` File: ${import_chalk5.default.cyan(fix2.file)}`);
|
|
1535
2102
|
if (fix2.skipped) {
|
|
1536
|
-
console.log(` Status: ${
|
|
2103
|
+
console.log(` Status: ${import_chalk5.default.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1537
2104
|
} else if (fix2.applied) {
|
|
1538
|
-
console.log(` Status: ${
|
|
1539
|
-
console.log(` ${
|
|
2105
|
+
console.log(` Status: ${import_chalk5.default.green("APPLIED")}`);
|
|
2106
|
+
console.log(` ${import_chalk5.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1540
2107
|
} else {
|
|
1541
|
-
console.log(` ${
|
|
2108
|
+
console.log(` ${import_chalk5.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1542
2109
|
if (fix2.before) {
|
|
1543
|
-
console.log(` ${
|
|
2110
|
+
console.log(` ${import_chalk5.default.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1544
2111
|
}
|
|
1545
|
-
console.log(` ${
|
|
2112
|
+
console.log(` ${import_chalk5.default.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1546
2113
|
}
|
|
1547
2114
|
}
|
|
1548
2115
|
async function fix(options) {
|
|
@@ -1586,15 +2153,15 @@ async function fix(options) {
|
|
|
1586
2153
|
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1587
2154
|
console.log("\u2550".repeat(50));
|
|
1588
2155
|
}
|
|
1589
|
-
const frameworkSpinner = options.output !== "json" ? (0,
|
|
1590
|
-
const frameworkResult = await (0,
|
|
2156
|
+
const frameworkSpinner = options.output !== "json" ? (0, import_ora3.default)("Detecting framework...").start() : null;
|
|
2157
|
+
const frameworkResult = await (0, import_agent_runtime4.detectFramework)({ cwd });
|
|
1591
2158
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1592
2159
|
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1593
|
-
const analyzeSpinner = options.output !== "json" ? (0,
|
|
2160
|
+
const analyzeSpinner = options.output !== "json" ? (0, import_ora3.default)(`Analyzing ${url}...`).start() : null;
|
|
1594
2161
|
let issues;
|
|
1595
2162
|
let score;
|
|
1596
2163
|
try {
|
|
1597
|
-
const analysis = await (0,
|
|
2164
|
+
const analysis = await (0, import_agent_runtime4.runDirectAnalysis)(url);
|
|
1598
2165
|
issues = analysis.issues;
|
|
1599
2166
|
score = analysis.score;
|
|
1600
2167
|
} catch (error) {
|
|
@@ -1607,7 +2174,7 @@ async function fix(options) {
|
|
|
1607
2174
|
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1608
2175
|
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1609
2176
|
if (options.output !== "json") {
|
|
1610
|
-
console.log(
|
|
2177
|
+
console.log(import_chalk5.default.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1611
2178
|
}
|
|
1612
2179
|
}
|
|
1613
2180
|
if (issues.length === 0) {
|
|
@@ -1624,8 +2191,8 @@ async function fix(options) {
|
|
|
1624
2191
|
durationMs: Date.now() - startTime
|
|
1625
2192
|
};
|
|
1626
2193
|
}
|
|
1627
|
-
const fixSpinner = options.output !== "json" ? (0,
|
|
1628
|
-
const fixes = await (0,
|
|
2194
|
+
const fixSpinner = options.output !== "json" ? (0, import_ora3.default)("Generating fixes...").start() : null;
|
|
2195
|
+
const fixes = await (0, import_agent_runtime4.generateFixes)(issues, {
|
|
1629
2196
|
cwd,
|
|
1630
2197
|
url,
|
|
1631
2198
|
framework
|
|
@@ -1636,13 +2203,13 @@ async function fix(options) {
|
|
|
1636
2203
|
if (options.dryRun) {
|
|
1637
2204
|
if (options.output !== "json") {
|
|
1638
2205
|
console.log("\n" + "\u2500".repeat(50));
|
|
1639
|
-
console.log(
|
|
2206
|
+
console.log(import_chalk5.default.yellow("DRY RUN - No changes will be made"));
|
|
1640
2207
|
console.log("\u2500".repeat(50));
|
|
1641
2208
|
for (let i = 0; i < fixes.length; i++) {
|
|
1642
2209
|
printFix2(fixes[i], i);
|
|
1643
2210
|
}
|
|
1644
2211
|
console.log("\n" + "\u2500".repeat(50));
|
|
1645
|
-
console.log(`\u{1F4CB} ${
|
|
2212
|
+
console.log(`\u{1F4CB} ${import_chalk5.default.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1646
2213
|
console.log(` Remove --dry-run to apply these fixes.
|
|
1647
2214
|
`);
|
|
1648
2215
|
}
|
|
@@ -1708,8 +2275,8 @@ async function fix(options) {
|
|
|
1708
2275
|
durationMs: Date.now() - startTime
|
|
1709
2276
|
};
|
|
1710
2277
|
}
|
|
1711
|
-
const applySpinner = options.output !== "json" ? (0,
|
|
1712
|
-
const { applied, skipped } = await (0,
|
|
2278
|
+
const applySpinner = options.output !== "json" ? (0, import_ora3.default)("Applying fixes...").start() : null;
|
|
2279
|
+
const { applied, skipped } = await (0, import_agent_runtime4.applyFixes)(fixes, { cwd, dryRun: false });
|
|
1713
2280
|
for (const fix2 of applied) {
|
|
1714
2281
|
fix2.applied = true;
|
|
1715
2282
|
}
|
|
@@ -1720,7 +2287,7 @@ async function fix(options) {
|
|
|
1720
2287
|
let commitError;
|
|
1721
2288
|
let pushError;
|
|
1722
2289
|
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1723
|
-
const gitSpinner = options.output !== "json" ? (0,
|
|
2290
|
+
const gitSpinner = options.output !== "json" ? (0, import_ora3.default)("Committing changes...").start() : null;
|
|
1724
2291
|
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1725
2292
|
committed = gitResult.committed;
|
|
1726
2293
|
pushed = gitResult.pushed;
|
|
@@ -1746,23 +2313,23 @@ async function fix(options) {
|
|
|
1746
2313
|
console.log("\u2550".repeat(50));
|
|
1747
2314
|
if (applied.length > 0) {
|
|
1748
2315
|
console.log(`
|
|
1749
|
-
${
|
|
2316
|
+
${import_chalk5.default.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1750
2317
|
for (const fix2 of applied) {
|
|
1751
2318
|
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1752
2319
|
}
|
|
1753
2320
|
}
|
|
1754
2321
|
if (skipped.length > 0) {
|
|
1755
2322
|
console.log(`
|
|
1756
|
-
${
|
|
2323
|
+
${import_chalk5.default.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1757
2324
|
for (const fix2 of skipped) {
|
|
1758
2325
|
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1759
2326
|
}
|
|
1760
2327
|
}
|
|
1761
2328
|
if (committed) {
|
|
1762
2329
|
console.log(`
|
|
1763
|
-
${
|
|
2330
|
+
${import_chalk5.default.green("\u2705")} Changes committed`);
|
|
1764
2331
|
if (pushed) {
|
|
1765
|
-
console.log(`${
|
|
2332
|
+
console.log(`${import_chalk5.default.green("\u2705")} Changes pushed`);
|
|
1766
2333
|
}
|
|
1767
2334
|
}
|
|
1768
2335
|
if (!committed && applied.length > 0) {
|
|
@@ -1815,10 +2382,10 @@ ${import_chalk3.default.green("\u2705")} Changes committed`);
|
|
|
1815
2382
|
}
|
|
1816
2383
|
|
|
1817
2384
|
// src/commands/track.ts
|
|
1818
|
-
var
|
|
1819
|
-
var
|
|
2385
|
+
var import_ora4 = __toESM(require("ora"));
|
|
2386
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1820
2387
|
var import_table = require("table");
|
|
1821
|
-
var
|
|
2388
|
+
var import_agent_runtime5 = require("@rankcli/agent-runtime");
|
|
1822
2389
|
async function getProjectForUrl(url) {
|
|
1823
2390
|
const supabase = await getAuthenticatedClient();
|
|
1824
2391
|
if (!supabase) return null;
|
|
@@ -1848,42 +2415,42 @@ async function getProjectForUrl(url) {
|
|
|
1848
2415
|
};
|
|
1849
2416
|
}
|
|
1850
2417
|
function getTierLimits(tier) {
|
|
1851
|
-
const limits =
|
|
2418
|
+
const limits = import_agent_runtime5.TIER_LIMITS[tier] || import_agent_runtime5.TIER_LIMITS.free;
|
|
1852
2419
|
return {
|
|
1853
2420
|
maxKeywords: limits.maxKeywords,
|
|
1854
2421
|
checksPerDay: limits.checksPerDay
|
|
1855
2422
|
};
|
|
1856
2423
|
}
|
|
1857
2424
|
function formatPosition(pos) {
|
|
1858
|
-
if (pos === null) return
|
|
1859
|
-
if (pos <= 3) return
|
|
1860
|
-
if (pos <= 10) return
|
|
1861
|
-
if (pos <= 20) return
|
|
1862
|
-
if (pos <= 50) return
|
|
1863
|
-
return
|
|
2425
|
+
if (pos === null) return import_chalk6.default.dim("N/A");
|
|
2426
|
+
if (pos <= 3) return import_chalk6.default.green.bold(`#${pos}`);
|
|
2427
|
+
if (pos <= 10) return import_chalk6.default.green(`#${pos}`);
|
|
2428
|
+
if (pos <= 20) return import_chalk6.default.yellow(`#${pos}`);
|
|
2429
|
+
if (pos <= 50) return import_chalk6.default.red(`#${pos}`);
|
|
2430
|
+
return import_chalk6.default.red.dim(`#${pos}`);
|
|
1864
2431
|
}
|
|
1865
2432
|
function formatChange(change) {
|
|
1866
|
-
if (change === 0) return
|
|
1867
|
-
if (change > 0) return
|
|
1868
|
-
return
|
|
2433
|
+
if (change === 0) return import_chalk6.default.dim("\u2192");
|
|
2434
|
+
if (change > 0) return import_chalk6.default.green(`\u2191${change}`);
|
|
2435
|
+
return import_chalk6.default.red(`\u2193${Math.abs(change)}`);
|
|
1869
2436
|
}
|
|
1870
2437
|
async function trackAdd(options) {
|
|
1871
2438
|
if (!options.keywords || options.keywords.length === 0) {
|
|
1872
|
-
console.log(
|
|
2439
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1873
2440
|
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1874
2441
|
return { success: false, error: "No keywords provided" };
|
|
1875
2442
|
}
|
|
1876
2443
|
if (!options.url) {
|
|
1877
|
-
console.log(
|
|
2444
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1878
2445
|
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1879
2446
|
return { success: false, error: "No URL provided" };
|
|
1880
2447
|
}
|
|
1881
2448
|
if (!isLoggedIn()) {
|
|
1882
|
-
console.log(
|
|
2449
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1883
2450
|
console.log(" rankcli login\n");
|
|
1884
2451
|
return { success: false, error: "Not logged in" };
|
|
1885
2452
|
}
|
|
1886
|
-
const spinner = (0,
|
|
2453
|
+
const spinner = (0, import_ora4.default)("Adding keywords to track...").start();
|
|
1887
2454
|
try {
|
|
1888
2455
|
const project = await getProjectForUrl(options.url);
|
|
1889
2456
|
if (!project) {
|
|
@@ -1924,10 +2491,10 @@ async function trackAdd(options) {
|
|
|
1924
2491
|
}
|
|
1925
2492
|
const addedCount = data?.length || 0;
|
|
1926
2493
|
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1927
|
-
console.log(
|
|
2494
|
+
console.log(import_chalk6.default.dim(`
|
|
1928
2495
|
Tracking: ${options.keywords.join(", ")}`));
|
|
1929
|
-
console.log(
|
|
1930
|
-
console.log(
|
|
2496
|
+
console.log(import_chalk6.default.dim(`For: ${project.domain}`));
|
|
2497
|
+
console.log(import_chalk6.default.cyan(`
|
|
1931
2498
|
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1932
2499
|
`));
|
|
1933
2500
|
return { success: true, addedCount };
|
|
@@ -1938,16 +2505,16 @@ Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
|
1938
2505
|
}
|
|
1939
2506
|
async function trackStatus(options) {
|
|
1940
2507
|
if (!options.url) {
|
|
1941
|
-
console.log(
|
|
2508
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1942
2509
|
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1943
2510
|
return { success: false, error: "No URL provided" };
|
|
1944
2511
|
}
|
|
1945
2512
|
if (!isLoggedIn()) {
|
|
1946
|
-
console.log(
|
|
2513
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1947
2514
|
console.log(" rankcli login\n");
|
|
1948
2515
|
return { success: false, error: "Not logged in" };
|
|
1949
2516
|
}
|
|
1950
|
-
const spinner = (0,
|
|
2517
|
+
const spinner = (0, import_ora4.default)("Loading keyword rankings...").start();
|
|
1951
2518
|
try {
|
|
1952
2519
|
const project = await getProjectForUrl(options.url);
|
|
1953
2520
|
if (!project) {
|
|
@@ -1966,8 +2533,8 @@ async function trackStatus(options) {
|
|
|
1966
2533
|
}
|
|
1967
2534
|
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1968
2535
|
if (!data || data.length === 0) {
|
|
1969
|
-
console.log(
|
|
1970
|
-
console.log(
|
|
2536
|
+
console.log(import_chalk6.default.dim("\nNo keywords tracked yet."));
|
|
2537
|
+
console.log(import_chalk6.default.cyan(`Run 'rankcli track add "keyword" --url ${options.url}' to start tracking.
|
|
1971
2538
|
`));
|
|
1972
2539
|
return { success: true, keywords: [] };
|
|
1973
2540
|
}
|
|
@@ -1982,7 +2549,7 @@ async function trackStatus(options) {
|
|
|
1982
2549
|
if (options.output === "json") {
|
|
1983
2550
|
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1984
2551
|
} else {
|
|
1985
|
-
console.log("\n" +
|
|
2552
|
+
console.log("\n" + import_chalk6.default.bold("\u{1F4CA} Keyword Rankings"));
|
|
1986
2553
|
console.log("\u2550".repeat(60));
|
|
1987
2554
|
const tableData = [
|
|
1988
2555
|
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
@@ -2025,14 +2592,14 @@ async function trackHistory(options) {
|
|
|
2025
2592
|
return { success: false, error: "No URL provided" };
|
|
2026
2593
|
}
|
|
2027
2594
|
if (!options.keyword) {
|
|
2028
|
-
console.log(
|
|
2595
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
2029
2596
|
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
2030
2597
|
return { success: false, error: "No keyword provided" };
|
|
2031
2598
|
}
|
|
2032
2599
|
if (!isLoggedIn()) {
|
|
2033
2600
|
return { success: false, error: "Not logged in" };
|
|
2034
2601
|
}
|
|
2035
|
-
const spinner = (0,
|
|
2602
|
+
const spinner = (0, import_ora4.default)(`Loading history for "${options.keyword}"...`).start();
|
|
2036
2603
|
try {
|
|
2037
2604
|
const project = await getProjectForUrl(options.url);
|
|
2038
2605
|
if (!project) {
|
|
@@ -2065,21 +2632,21 @@ async function trackHistory(options) {
|
|
|
2065
2632
|
if (options.output === "json") {
|
|
2066
2633
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
2067
2634
|
} else {
|
|
2068
|
-
console.log("\n" +
|
|
2635
|
+
console.log("\n" + import_chalk6.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
2069
2636
|
console.log("\u2550".repeat(50));
|
|
2070
2637
|
if (history.length === 0) {
|
|
2071
|
-
console.log(
|
|
2638
|
+
console.log(import_chalk6.default.dim("\nNo history available yet.\n"));
|
|
2072
2639
|
} else {
|
|
2073
2640
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
2074
2641
|
const sparkline = positions.map((p) => {
|
|
2075
|
-
if (p <= 10) return
|
|
2076
|
-
if (p <= 20) return
|
|
2077
|
-
if (p <= 50) return
|
|
2078
|
-
return
|
|
2642
|
+
if (p <= 10) return import_chalk6.default.green("\u2588");
|
|
2643
|
+
if (p <= 20) return import_chalk6.default.yellow("\u2593");
|
|
2644
|
+
if (p <= 50) return import_chalk6.default.red("\u2592");
|
|
2645
|
+
return import_chalk6.default.dim("\u2591");
|
|
2079
2646
|
}).join("");
|
|
2080
2647
|
console.log(`
|
|
2081
2648
|
${sparkline} (last ${positions.length} checks)`);
|
|
2082
|
-
console.log(
|
|
2649
|
+
console.log(import_chalk6.default.dim(" \u2190 newer older \u2192\n"));
|
|
2083
2650
|
const recent = history.slice(0, 10);
|
|
2084
2651
|
for (const entry of recent) {
|
|
2085
2652
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -2099,7 +2666,7 @@ async function trackExport(options) {
|
|
|
2099
2666
|
if (!isLoggedIn()) {
|
|
2100
2667
|
return { success: false, error: "Not logged in" };
|
|
2101
2668
|
}
|
|
2102
|
-
const spinner = (0,
|
|
2669
|
+
const spinner = (0, import_ora4.default)("Exporting data...").start();
|
|
2103
2670
|
try {
|
|
2104
2671
|
const project = await getProjectForUrl(options.url);
|
|
2105
2672
|
if (!project) {
|
|
@@ -2156,12 +2723,12 @@ async function trackExport(options) {
|
|
|
2156
2723
|
}
|
|
2157
2724
|
async function trackCheck(options) {
|
|
2158
2725
|
if (!options.url) {
|
|
2159
|
-
console.log(
|
|
2726
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
2160
2727
|
console.log(" rankcli track check --url https://your-site.com\n");
|
|
2161
2728
|
return { success: false, error: "No URL provided" };
|
|
2162
2729
|
}
|
|
2163
2730
|
if (!isLoggedIn()) {
|
|
2164
|
-
console.log(
|
|
2731
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
2165
2732
|
console.log(" rankcli login\n");
|
|
2166
2733
|
return { success: false, error: "Not logged in" };
|
|
2167
2734
|
}
|
|
@@ -2178,14 +2745,14 @@ async function trackCheck(options) {
|
|
|
2178
2745
|
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
2179
2746
|
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
2180
2747
|
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
2181
|
-
console.log(
|
|
2748
|
+
console.log(import_chalk6.default.yellow(`
|
|
2182
2749
|
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
2183
|
-
console.log(
|
|
2750
|
+
console.log(import_chalk6.default.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
2184
2751
|
`));
|
|
2185
2752
|
return { success: true, rateLimited: true };
|
|
2186
2753
|
}
|
|
2187
2754
|
}
|
|
2188
|
-
const spinner = (0,
|
|
2755
|
+
const spinner = (0, import_ora4.default)("Checking current rankings...").start();
|
|
2189
2756
|
const supabase = await getAuthenticatedClient();
|
|
2190
2757
|
if (!supabase) {
|
|
2191
2758
|
spinner.fail("Could not connect to database");
|
|
@@ -2199,11 +2766,11 @@ async function trackCheck(options) {
|
|
|
2199
2766
|
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2200
2767
|
if (!serpApiKey) {
|
|
2201
2768
|
spinner.warn("No SERP API key configured");
|
|
2202
|
-
console.log(
|
|
2203
|
-
console.log(
|
|
2769
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Set VALUESERP_API_KEY or SERPAPI_KEY to check live rankings."));
|
|
2770
|
+
console.log(import_chalk6.default.dim(" Get a key at https://www.valueserp.com or https://serpapi.com\n"));
|
|
2204
2771
|
return { success: false, error: "No SERP API key" };
|
|
2205
2772
|
}
|
|
2206
|
-
const tracker = new
|
|
2773
|
+
const tracker = new import_agent_runtime5.RankTracker({
|
|
2207
2774
|
supabase,
|
|
2208
2775
|
serpConfig: {
|
|
2209
2776
|
provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
|
|
@@ -2212,13 +2779,13 @@ async function trackCheck(options) {
|
|
|
2212
2779
|
});
|
|
2213
2780
|
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2214
2781
|
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2215
|
-
console.log("\n" +
|
|
2782
|
+
console.log("\n" + import_chalk6.default.bold("\u{1F50D} Rank Check Results"));
|
|
2216
2783
|
console.log("\u2550".repeat(50));
|
|
2217
2784
|
for (const result of results) {
|
|
2218
2785
|
const posStr = formatPosition(result.position);
|
|
2219
2786
|
console.log(` ${result.keyword}: ${posStr}`);
|
|
2220
2787
|
if (result.serpFeatures.length > 0) {
|
|
2221
|
-
console.log(
|
|
2788
|
+
console.log(import_chalk6.default.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2222
2789
|
}
|
|
2223
2790
|
}
|
|
2224
2791
|
console.log("");
|
|
@@ -2236,9 +2803,9 @@ async function trackCheck(options) {
|
|
|
2236
2803
|
}
|
|
2237
2804
|
|
|
2238
2805
|
// src/commands/geo.ts
|
|
2239
|
-
var
|
|
2240
|
-
var
|
|
2241
|
-
var
|
|
2806
|
+
var import_agent_runtime6 = require("@rankcli/agent-runtime");
|
|
2807
|
+
var import_ora5 = __toESM(require("ora"));
|
|
2808
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
2242
2809
|
async function fetchResources(url) {
|
|
2243
2810
|
const headers = {};
|
|
2244
2811
|
let html = null;
|
|
@@ -2284,7 +2851,7 @@ async function geo(options) {
|
|
|
2284
2851
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
2285
2852
|
return;
|
|
2286
2853
|
}
|
|
2287
|
-
const spinner = (0,
|
|
2854
|
+
const spinner = (0, import_ora5.default)("Fetching page and robots.txt...").start();
|
|
2288
2855
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
2289
2856
|
if (!html) {
|
|
2290
2857
|
spinner.fail("Could not fetch page content");
|
|
@@ -2292,11 +2859,11 @@ async function geo(options) {
|
|
|
2292
2859
|
}
|
|
2293
2860
|
spinner.text = "Running GEO analysis...";
|
|
2294
2861
|
try {
|
|
2295
|
-
const geoResult = await
|
|
2296
|
-
const cwvResult =
|
|
2297
|
-
const securityResult =
|
|
2298
|
-
const schemaResult =
|
|
2299
|
-
const mobileResult =
|
|
2862
|
+
const geoResult = await import_agent_runtime6.analyzers.analyzeGEO(html, url, robotsTxt || void 0);
|
|
2863
|
+
const cwvResult = import_agent_runtime6.analyzers.analyzeCoreWebVitals(html, url, headers);
|
|
2864
|
+
const securityResult = import_agent_runtime6.analyzers.analyzeSecurityHeaders(headers, url);
|
|
2865
|
+
const schemaResult = import_agent_runtime6.analyzers.analyzeStructuredData(html, url);
|
|
2866
|
+
const mobileResult = import_agent_runtime6.analyzers.analyzeMobileSEO(html, url);
|
|
2300
2867
|
spinner.succeed("GEO analysis complete");
|
|
2301
2868
|
if (options.output === "json") {
|
|
2302
2869
|
const jsonOutput = {
|
|
@@ -2310,55 +2877,55 @@ async function geo(options) {
|
|
|
2310
2877
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2311
2878
|
return;
|
|
2312
2879
|
}
|
|
2313
|
-
console.log("\n" +
|
|
2314
|
-
console.log(
|
|
2315
|
-
console.log(
|
|
2880
|
+
console.log("\n" + import_chalk7.default.bold("\u2550".repeat(60)));
|
|
2881
|
+
console.log(import_chalk7.default.bold.cyan(" GEO ANALYSIS: AI Search Optimization"));
|
|
2882
|
+
console.log(import_chalk7.default.bold("\u2550".repeat(60)));
|
|
2316
2883
|
const geoScore = geoResult.score;
|
|
2317
|
-
const scoreColor = geoScore >= 80 ?
|
|
2884
|
+
const scoreColor = geoScore >= 80 ? import_chalk7.default.green : geoScore >= 60 ? import_chalk7.default.yellow : import_chalk7.default.red;
|
|
2318
2885
|
console.log(`
|
|
2319
|
-
${
|
|
2320
|
-
console.log("\n" +
|
|
2886
|
+
${import_chalk7.default.bold("GEO Score:")} ${scoreColor.bold(geoScore + "/100")}`);
|
|
2887
|
+
console.log("\n" + import_chalk7.default.bold(" AI Crawler Access:"));
|
|
2321
2888
|
const { aiCrawlerAccess } = geoResult;
|
|
2322
2889
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
2323
|
-
console.log(
|
|
2890
|
+
console.log(import_chalk7.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
2324
2891
|
}
|
|
2325
2892
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
2326
2893
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
2327
|
-
console.log(
|
|
2894
|
+
console.log(import_chalk7.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
2328
2895
|
}
|
|
2329
|
-
console.log("\n" +
|
|
2896
|
+
console.log("\n" + import_chalk7.default.bold(" Rendering:"));
|
|
2330
2897
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
2331
|
-
console.log(
|
|
2898
|
+
console.log(import_chalk7.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
2332
2899
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
2333
|
-
console.log(
|
|
2900
|
+
console.log(import_chalk7.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
2334
2901
|
}
|
|
2335
|
-
console.log("\n" +
|
|
2902
|
+
console.log("\n" + import_chalk7.default.bold(" LLM Friendliness:"));
|
|
2336
2903
|
const { llmSignals } = geoResult;
|
|
2337
2904
|
const bar = (score) => {
|
|
2338
2905
|
const filled = Math.round(score / 5);
|
|
2339
2906
|
const empty = 20 - filled;
|
|
2340
|
-
const color = score >= 70 ?
|
|
2341
|
-
return color("\u2588".repeat(filled)) +
|
|
2907
|
+
const color = score >= 70 ? import_chalk7.default.green : score >= 40 ? import_chalk7.default.yellow : import_chalk7.default.red;
|
|
2908
|
+
return color("\u2588".repeat(filled)) + import_chalk7.default.gray("\u2591".repeat(empty));
|
|
2342
2909
|
};
|
|
2343
2910
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
2344
2911
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
2345
2912
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
2346
2913
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
2347
|
-
console.log("\n" +
|
|
2914
|
+
console.log("\n" + import_chalk7.default.bold(" Content Structure:"));
|
|
2348
2915
|
const { contentStructure } = geoResult;
|
|
2349
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
2350
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
2351
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
2352
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
2353
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
2354
|
-
console.log("\n" +
|
|
2916
|
+
console.log(` ${contentStructure.hasStructuredData ? import_chalk7.default.green("\u2705") : import_chalk7.default.red("\u274C")} JSON-LD Structured Data`);
|
|
2917
|
+
console.log(` ${contentStructure.hasFAQSchema ? import_chalk7.default.green("\u2705") : import_chalk7.default.yellow("\u25CB")} FAQ Schema`);
|
|
2918
|
+
console.log(` ${contentStructure.hasArticleSchema ? import_chalk7.default.green("\u2705") : import_chalk7.default.yellow("\u25CB")} Article Schema`);
|
|
2919
|
+
console.log(` ${contentStructure.hasBreadcrumbs ? import_chalk7.default.green("\u2705") : import_chalk7.default.yellow("\u25CB")} Breadcrumbs`);
|
|
2920
|
+
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ? import_chalk7.default.green("Good") : contentStructure.headingHierarchy === "needs-work" ? import_chalk7.default.yellow("Needs Work") : import_chalk7.default.red("Poor")}`);
|
|
2921
|
+
console.log("\n" + import_chalk7.default.bold(" Citation Readiness:"));
|
|
2355
2922
|
const { citationReadiness } = geoResult;
|
|
2356
2923
|
if (citationReadiness.trustSignals.length > 0) {
|
|
2357
|
-
console.log(
|
|
2924
|
+
console.log(import_chalk7.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
2358
2925
|
} else {
|
|
2359
|
-
console.log(
|
|
2926
|
+
console.log(import_chalk7.default.yellow(" \u25CB No trust signals detected"));
|
|
2360
2927
|
}
|
|
2361
|
-
console.log("\n" +
|
|
2928
|
+
console.log("\n" + import_chalk7.default.bold(" Other Scores:"));
|
|
2362
2929
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
2363
2930
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
2364
2931
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -2372,18 +2939,18 @@ async function geo(options) {
|
|
|
2372
2939
|
];
|
|
2373
2940
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
2374
2941
|
if (criticalIssues.length > 0) {
|
|
2375
|
-
console.log("\n" +
|
|
2942
|
+
console.log("\n" + import_chalk7.default.bold.red(" Critical Issues:"));
|
|
2376
2943
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
2377
|
-
console.log(
|
|
2944
|
+
console.log(import_chalk7.default.red(` ${i + 1}. ${issue.title}`));
|
|
2378
2945
|
});
|
|
2379
2946
|
}
|
|
2380
2947
|
if (geoResult.recommendations.length > 0) {
|
|
2381
|
-
console.log("\n" +
|
|
2948
|
+
console.log("\n" + import_chalk7.default.bold(" Recommendations:"));
|
|
2382
2949
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
2383
2950
|
console.log(` ${i + 1}. ${rec}`);
|
|
2384
2951
|
});
|
|
2385
2952
|
}
|
|
2386
|
-
console.log("\n" +
|
|
2953
|
+
console.log("\n" + import_chalk7.default.bold("\u2550".repeat(60)));
|
|
2387
2954
|
console.log("");
|
|
2388
2955
|
} catch (error) {
|
|
2389
2956
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -2391,13 +2958,13 @@ async function geo(options) {
|
|
|
2391
2958
|
}
|
|
2392
2959
|
|
|
2393
2960
|
// src/commands/keywords.ts
|
|
2394
|
-
var
|
|
2395
|
-
var
|
|
2961
|
+
var readline2 = __toESM(require("readline"));
|
|
2962
|
+
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
2396
2963
|
async function keywords(options) {
|
|
2397
|
-
console.log("\n\u{1F511}
|
|
2964
|
+
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
2398
2965
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
2399
2966
|
console.log("based on your site's current authority and competition level.\n");
|
|
2400
|
-
const rl =
|
|
2967
|
+
const rl = readline2.createInterface({
|
|
2401
2968
|
input: process.stdin,
|
|
2402
2969
|
output: process.stdout
|
|
2403
2970
|
});
|
|
@@ -2443,7 +3010,7 @@ ${question}`);
|
|
|
2443
3010
|
seedKeywords = options.seed.split(",").map((s) => s.trim());
|
|
2444
3011
|
} else if (options.auto) {
|
|
2445
3012
|
console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
|
|
2446
|
-
seedKeywords = await (0,
|
|
3013
|
+
seedKeywords = await (0, import_agent_runtime7.extractSeedKeywords)(url);
|
|
2447
3014
|
if (seedKeywords.length > 0) {
|
|
2448
3015
|
console.log(`Found: ${seedKeywords.join(", ")}`);
|
|
2449
3016
|
}
|
|
@@ -2468,7 +3035,7 @@ ${question}`);
|
|
|
2468
3035
|
contentCapacity: "medium",
|
|
2469
3036
|
targetGeo: "us"
|
|
2470
3037
|
};
|
|
2471
|
-
for (const q of
|
|
3038
|
+
for (const q of import_agent_runtime7.SITE_PROFILE_QUESTIONS) {
|
|
2472
3039
|
const answer = await askChoice(q.question, q.options);
|
|
2473
3040
|
siteProfile[q.id] = answer;
|
|
2474
3041
|
}
|
|
@@ -2487,13 +3054,13 @@ ${question}`);
|
|
|
2487
3054
|
url,
|
|
2488
3055
|
maxKeywords: 100
|
|
2489
3056
|
};
|
|
2490
|
-
const result = await (0,
|
|
2491
|
-
console.log((0,
|
|
3057
|
+
const result = await (0, import_agent_runtime7.runKeywordResearch)(researchOptions);
|
|
3058
|
+
console.log((0, import_agent_runtime7.formatKeywordReport)(result));
|
|
2492
3059
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
2493
3060
|
if (allKeywords.length > 5) {
|
|
2494
3061
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
2495
|
-
const topicResult = (0,
|
|
2496
|
-
console.log((0,
|
|
3062
|
+
const topicResult = (0, import_agent_runtime7.groupKeywordsByTopic)(allKeywords);
|
|
3063
|
+
console.log((0, import_agent_runtime7.formatTopicReport)(topicResult));
|
|
2497
3064
|
}
|
|
2498
3065
|
if (result.quickWins.length > 0) {
|
|
2499
3066
|
console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
|
|
@@ -2518,7 +3085,7 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
2518
3085
|
`);
|
|
2519
3086
|
} else {
|
|
2520
3087
|
console.log("\u{1F50D} Extracting keywords from your page...");
|
|
2521
|
-
seedKeywords = await (0,
|
|
3088
|
+
seedKeywords = await (0, import_agent_runtime7.extractSeedKeywords)(url);
|
|
2522
3089
|
if (seedKeywords.length === 0) {
|
|
2523
3090
|
console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
|
|
2524
3091
|
return;
|
|
@@ -2534,18 +3101,18 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
2534
3101
|
contentCapacity: "medium",
|
|
2535
3102
|
targetGeo: "us"
|
|
2536
3103
|
};
|
|
2537
|
-
const result = await (0,
|
|
3104
|
+
const result = await (0, import_agent_runtime7.runKeywordResearch)({
|
|
2538
3105
|
seedKeywords,
|
|
2539
3106
|
siteProfile,
|
|
2540
3107
|
url,
|
|
2541
3108
|
maxKeywords: 50
|
|
2542
3109
|
});
|
|
2543
|
-
console.log((0,
|
|
3110
|
+
console.log((0, import_agent_runtime7.formatKeywordReport)(result));
|
|
2544
3111
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
2545
3112
|
if (allKeywords.length > 5) {
|
|
2546
3113
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
2547
|
-
const topicResult = (0,
|
|
2548
|
-
console.log((0,
|
|
3114
|
+
const topicResult = (0, import_agent_runtime7.groupKeywordsByTopic)(allKeywords);
|
|
3115
|
+
console.log((0, import_agent_runtime7.formatTopicReport)(topicResult));
|
|
2549
3116
|
}
|
|
2550
3117
|
} catch (error) {
|
|
2551
3118
|
console.log(`
|
|
@@ -2608,7 +3175,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
2608
3175
|
return;
|
|
2609
3176
|
}
|
|
2610
3177
|
console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
|
|
2611
|
-
const result = await (0,
|
|
3178
|
+
const result = await (0, import_agent_runtime7.runAIKeywordResearch)({
|
|
2612
3179
|
url,
|
|
2613
3180
|
tier: "solo",
|
|
2614
3181
|
openaiApiKey,
|
|
@@ -2727,12 +3294,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
2727
3294
|
}
|
|
2728
3295
|
console.log("");
|
|
2729
3296
|
console.log("Analyzing competitor keywords (this may take a moment)...\n");
|
|
2730
|
-
const result = await (0,
|
|
3297
|
+
const result = await (0, import_agent_runtime7.discoverCompetitorKeywords)(
|
|
2731
3298
|
yourDomain,
|
|
2732
3299
|
seedKeywords,
|
|
2733
3300
|
competitors
|
|
2734
3301
|
);
|
|
2735
|
-
console.log((0,
|
|
3302
|
+
console.log((0, import_agent_runtime7.formatCompetitorReport)(result));
|
|
2736
3303
|
if (result.missingKeywords.length > 0) {
|
|
2737
3304
|
console.log("\n\u{1F3AF} ACTION ITEMS:");
|
|
2738
3305
|
console.log("\u2500".repeat(60));
|
|
@@ -2751,9 +3318,9 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
2751
3318
|
}
|
|
2752
3319
|
|
|
2753
3320
|
// src/commands/content.ts
|
|
2754
|
-
var
|
|
3321
|
+
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
2755
3322
|
async function analyzeContent(options) {
|
|
2756
|
-
console.log("\n\u{1F4DD}
|
|
3323
|
+
console.log("\n\u{1F4DD} RankCLI - Content Analysis\n");
|
|
2757
3324
|
const mode = options.mode || "full";
|
|
2758
3325
|
if (mode === "headline" || options.headline) {
|
|
2759
3326
|
const headline = options.headline || "";
|
|
@@ -2761,12 +3328,12 @@ async function analyzeContent(options) {
|
|
|
2761
3328
|
console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
|
|
2762
3329
|
return;
|
|
2763
3330
|
}
|
|
2764
|
-
const analysis = (0,
|
|
2765
|
-
console.log((0,
|
|
3331
|
+
const analysis = (0, import_agent_runtime8.analyzeHeadline)(headline);
|
|
3332
|
+
console.log((0, import_agent_runtime8.formatHeadlineReport)(analysis));
|
|
2766
3333
|
if (options.keyword) {
|
|
2767
3334
|
console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
|
|
2768
3335
|
console.log("\u2500".repeat(60));
|
|
2769
|
-
const variations = (0,
|
|
3336
|
+
const variations = (0, import_agent_runtime8.generateHeadlineVariations)(options.keyword, [options.keyword]);
|
|
2770
3337
|
for (const v of variations.slice(0, 5)) {
|
|
2771
3338
|
console.log(` \u2022 ${v}`);
|
|
2772
3339
|
}
|
|
@@ -2777,15 +3344,15 @@ async function analyzeContent(options) {
|
|
|
2777
3344
|
if (options.url) {
|
|
2778
3345
|
console.log(`Fetching content from ${options.url}...`);
|
|
2779
3346
|
try {
|
|
2780
|
-
const crawlResult = await (0,
|
|
3347
|
+
const crawlResult = await (0, import_agent_runtime8.crawlUrl)({ url: options.url });
|
|
2781
3348
|
if (!crawlResult.success || !crawlResult.data) {
|
|
2782
3349
|
console.log("\u274C Could not fetch content from URL");
|
|
2783
3350
|
return;
|
|
2784
3351
|
}
|
|
2785
3352
|
const { html } = crawlResult.data;
|
|
2786
3353
|
const [metaResult, headingsResult] = await Promise.all([
|
|
2787
|
-
(0,
|
|
2788
|
-
(0,
|
|
3354
|
+
(0, import_agent_runtime8.extractMeta)({ html, url: options.url }),
|
|
3355
|
+
(0, import_agent_runtime8.analyzeHeadings)({ html })
|
|
2789
3356
|
]);
|
|
2790
3357
|
const meta = metaResult.success ? metaResult.data : void 0;
|
|
2791
3358
|
const headingStructure = headingsResult.success ? headingsResult.data : [];
|
|
@@ -2798,15 +3365,15 @@ async function analyzeContent(options) {
|
|
|
2798
3365
|
Analyzing: "${title || options.url}"
|
|
2799
3366
|
`);
|
|
2800
3367
|
if (mode === "full" || mode === "readability") {
|
|
2801
|
-
const readability = (0,
|
|
2802
|
-
console.log((0,
|
|
3368
|
+
const readability = (0, import_agent_runtime8.analyzeReadability)(bodyText);
|
|
3369
|
+
console.log((0, import_agent_runtime8.formatReadabilityReport)(readability));
|
|
2803
3370
|
}
|
|
2804
3371
|
if (mode === "full" && title) {
|
|
2805
|
-
const headlineAnalysis = (0,
|
|
2806
|
-
console.log((0,
|
|
3372
|
+
const headlineAnalysis = (0, import_agent_runtime8.analyzeHeadline)(title);
|
|
3373
|
+
console.log((0, import_agent_runtime8.formatHeadlineReport)(headlineAnalysis));
|
|
2807
3374
|
}
|
|
2808
3375
|
if (options.keyword && (mode === "full" || mode === "density")) {
|
|
2809
|
-
const densityAnalysis = (0,
|
|
3376
|
+
const densityAnalysis = (0, import_agent_runtime8.analyzeKeywordDensity)(options.keyword, {
|
|
2810
3377
|
title,
|
|
2811
3378
|
h1,
|
|
2812
3379
|
headings,
|
|
@@ -2814,231 +3381,59 @@ Analyzing: "${title || options.url}"
|
|
|
2814
3381
|
firstParagraph: paragraphs[0],
|
|
2815
3382
|
lastParagraph: paragraphs[paragraphs.length - 1]
|
|
2816
3383
|
});
|
|
2817
|
-
console.log((0,
|
|
3384
|
+
console.log((0, import_agent_runtime8.formatKeywordDensityReport)(densityAnalysis));
|
|
2818
3385
|
}
|
|
2819
3386
|
if (options.keyword && (mode === "full" || mode === "snippet")) {
|
|
2820
|
-
const snippetAnalysis = (0,
|
|
3387
|
+
const snippetAnalysis = (0, import_agent_runtime8.analyzeFeaturedSnippetPotential)(
|
|
2821
3388
|
options.keyword,
|
|
2822
3389
|
bodyText,
|
|
2823
3390
|
headings
|
|
2824
3391
|
);
|
|
2825
|
-
console.log((0,
|
|
3392
|
+
console.log((0, import_agent_runtime8.formatFeaturedSnippetReport)(snippetAnalysis));
|
|
2826
3393
|
}
|
|
2827
3394
|
if (options.keyword) {
|
|
2828
|
-
const intentAnalysis = (0,
|
|
2829
|
-
console.log((0,
|
|
3395
|
+
const intentAnalysis = (0, import_agent_runtime8.classifyIntent)(options.keyword);
|
|
3396
|
+
console.log((0, import_agent_runtime8.formatIntentReport)(options.keyword, intentAnalysis));
|
|
2830
3397
|
}
|
|
2831
3398
|
console.log("\n\u{1F4CA} CONTENT SUMMARY");
|
|
2832
3399
|
console.log("\u2500".repeat(60));
|
|
2833
|
-
console.log(` Title: ${title || "(none)"}`);
|
|
2834
|
-
console.log(` H1: ${h1 || "(none)"}`);
|
|
2835
|
-
console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
|
|
2836
|
-
console.log(` Headings: ${headings.length}`);
|
|
2837
|
-
console.log("");
|
|
2838
|
-
} catch (error) {
|
|
2839
|
-
console.log(`
|
|
2840
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2841
|
-
`);
|
|
2842
|
-
}
|
|
2843
|
-
} else {
|
|
2844
|
-
console.log("Usage:");
|
|
2845
|
-
console.log(" seo content -u <url> Full content analysis");
|
|
2846
|
-
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
2847
|
-
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
2848
|
-
console.log(" seo content -u <url> --mode readability Readability only");
|
|
2849
|
-
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
2850
|
-
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
2851
|
-
console.log("");
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
async function analyzeIntent(keyword) {
|
|
2855
|
-
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
2856
|
-
const analysis = (0, import_agent_runtime7.classifyIntent)(keyword);
|
|
2857
|
-
console.log((0, import_agent_runtime7.formatIntentReport)(keyword, analysis));
|
|
2858
|
-
}
|
|
2859
|
-
function extractBodyText(html) {
|
|
2860
|
-
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
2861
|
-
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2862
|
-
text = text.replace(/<[^>]+>/g, " ");
|
|
2863
|
-
text = text.replace(/ /g, " ");
|
|
2864
|
-
text = text.replace(/&/g, "&");
|
|
2865
|
-
text = text.replace(/</g, "<");
|
|
2866
|
-
text = text.replace(/>/g, ">");
|
|
2867
|
-
text = text.replace(/"/g, '"');
|
|
2868
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
2869
|
-
return text;
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
// src/commands/setup.ts
|
|
2873
|
-
var readline2 = __toESM(require("readline"));
|
|
2874
|
-
var fs2 = __toESM(require("fs"));
|
|
2875
|
-
var path2 = __toESM(require("path"));
|
|
2876
|
-
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
2877
|
-
async function setup(options) {
|
|
2878
|
-
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
2879
|
-
const projectPath = process.cwd();
|
|
2880
|
-
if (!fs2.existsSync(path2.join(projectPath, "package.json")) && !fs2.existsSync(path2.join(projectPath, "index.html"))) {
|
|
2881
|
-
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
2882
|
-
}
|
|
2883
|
-
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
2884
|
-
await interactiveSetup(projectPath);
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
if (options.ga4) {
|
|
2888
|
-
await setupGA4(projectPath, options.ga4);
|
|
2889
|
-
}
|
|
2890
|
-
if (options.gsc) {
|
|
2891
|
-
await setupGSC(projectPath, options.gsc);
|
|
2892
|
-
}
|
|
2893
|
-
if (options.githubAction) {
|
|
2894
|
-
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
async function interactiveSetup(projectPath) {
|
|
2898
|
-
const rl = readline2.createInterface({
|
|
2899
|
-
input: process.stdin,
|
|
2900
|
-
output: process.stdout
|
|
2901
|
-
});
|
|
2902
|
-
const ask2 = (question) => {
|
|
2903
|
-
return new Promise((resolve) => {
|
|
2904
|
-
rl.question(question, (answer) => {
|
|
2905
|
-
resolve(answer.trim());
|
|
2906
|
-
});
|
|
2907
|
-
});
|
|
2908
|
-
};
|
|
2909
|
-
try {
|
|
2910
|
-
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
2911
|
-
console.log("\u2500".repeat(60));
|
|
2912
|
-
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
2913
|
-
console.log("GA4 tracks visitor behavior on your site.");
|
|
2914
|
-
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
2915
|
-
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
2916
|
-
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
2917
|
-
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
2918
|
-
await setupGA4(projectPath, ga4Id);
|
|
2919
|
-
} else if (ga4Id) {
|
|
2920
|
-
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
2921
|
-
} else {
|
|
2922
|
-
console.log("Skipping GA4 setup.\n");
|
|
2923
|
-
}
|
|
2924
|
-
console.log("\u2500".repeat(60));
|
|
2925
|
-
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
2926
|
-
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
2927
|
-
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
2928
|
-
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
2929
|
-
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
2930
|
-
if (gscCode && gscCode.length > 10) {
|
|
2931
|
-
await setupGSC(projectPath, gscCode);
|
|
2932
|
-
} else if (gscCode) {
|
|
2933
|
-
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
2934
|
-
} else {
|
|
2935
|
-
console.log("Skipping GSC verification.\n");
|
|
2936
|
-
}
|
|
2937
|
-
console.log("\u2500".repeat(60));
|
|
2938
|
-
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
2939
|
-
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
2940
|
-
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
2941
|
-
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
2942
|
-
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
2943
|
-
console.log("\nHow often should the check run?");
|
|
2944
|
-
console.log(" 1. Weekly (recommended for most sites)");
|
|
2945
|
-
console.log(" 2. Daily (for high-traffic sites)");
|
|
2946
|
-
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
2947
|
-
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
2948
|
-
const scheduleMap = {
|
|
2949
|
-
"1": "weekly",
|
|
2950
|
-
"2": "daily",
|
|
2951
|
-
"3": "monthly"
|
|
2952
|
-
};
|
|
2953
|
-
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2954
|
-
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2955
|
-
if (siteUrl) {
|
|
2956
|
-
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
2957
|
-
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
2958
|
-
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
2959
|
-
}
|
|
2960
|
-
} else {
|
|
2961
|
-
console.log("Skipping GitHub Action setup.\n");
|
|
2962
|
-
}
|
|
2963
|
-
console.log("\u2500".repeat(60));
|
|
2964
|
-
console.log("\u2705 Setup complete!\n");
|
|
2965
|
-
console.log("Next steps:");
|
|
2966
|
-
console.log(" 1. Commit the changes to your repository");
|
|
2967
|
-
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
2968
|
-
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
2969
|
-
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
2970
|
-
rl.close();
|
|
2971
|
-
} catch (error) {
|
|
2972
|
-
console.log(`
|
|
2973
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2974
|
-
`);
|
|
2975
|
-
rl.close();
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
async function setupGA4(projectPath, measurementId) {
|
|
2979
|
-
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
2980
|
-
const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
|
|
2981
|
-
if (result.success) {
|
|
2982
|
-
console.log(`\u2705 ${result.message}`);
|
|
2983
|
-
if (result.file) {
|
|
2984
|
-
console.log(` Modified: ${result.file}`);
|
|
2985
|
-
}
|
|
2986
|
-
} else {
|
|
2987
|
-
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
2988
|
-
if (result.code) {
|
|
2989
|
-
console.log("\nManually add this code to your HTML <head>:\n");
|
|
2990
|
-
console.log(result.code);
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
const envExamplePath = path2.join(projectPath, ".env.example");
|
|
2994
|
-
const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
|
|
2995
|
-
if (fs2.existsSync(envExamplePath)) {
|
|
2996
|
-
const existing = fs2.readFileSync(envExamplePath, "utf-8");
|
|
2997
|
-
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
2998
|
-
fs2.appendFileSync(envExamplePath, "\n" + envContent);
|
|
2999
|
-
console.log(" Updated: .env.example");
|
|
3000
|
-
}
|
|
3001
|
-
} else {
|
|
3002
|
-
fs2.writeFileSync(envExamplePath, envContent);
|
|
3003
|
-
console.log(" Created: .env.example");
|
|
3004
|
-
}
|
|
3005
|
-
console.log("");
|
|
3006
|
-
}
|
|
3007
|
-
async function setupGSC(projectPath, verificationCode) {
|
|
3008
|
-
console.log("Setting up Google Search Console verification...");
|
|
3009
|
-
const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
|
|
3010
|
-
if (result.success) {
|
|
3011
|
-
console.log(`\u2705 ${result.message}`);
|
|
3012
|
-
if (result.file) {
|
|
3013
|
-
console.log(` Modified: ${result.file}`);
|
|
3400
|
+
console.log(` Title: ${title || "(none)"}`);
|
|
3401
|
+
console.log(` H1: ${h1 || "(none)"}`);
|
|
3402
|
+
console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
|
|
3403
|
+
console.log(` Headings: ${headings.length}`);
|
|
3404
|
+
console.log("");
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
console.log(`
|
|
3407
|
+
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
3408
|
+
`);
|
|
3014
3409
|
}
|
|
3015
3410
|
} else {
|
|
3016
|
-
console.log(
|
|
3411
|
+
console.log("Usage:");
|
|
3412
|
+
console.log(" seo content -u <url> Full content analysis");
|
|
3413
|
+
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
3414
|
+
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
3415
|
+
console.log(" seo content -u <url> --mode readability Readability only");
|
|
3416
|
+
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
3417
|
+
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
3418
|
+
console.log("");
|
|
3017
3419
|
}
|
|
3018
|
-
console.log("");
|
|
3019
3420
|
}
|
|
3020
|
-
async function
|
|
3021
|
-
console.log(
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
siteUrl: siteUrl || "https://example.com",
|
|
3025
|
-
features: {
|
|
3026
|
-
audit: true,
|
|
3027
|
-
autoFix: true,
|
|
3028
|
-
createIssues: true,
|
|
3029
|
-
createPRs: true
|
|
3030
|
-
},
|
|
3031
|
-
syncToDashboard
|
|
3032
|
-
};
|
|
3033
|
-
const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
|
|
3034
|
-
console.log("\u2705 GitHub Action files created:");
|
|
3035
|
-
for (const file of files) {
|
|
3036
|
-
console.log(` - ${path2.relative(projectPath, file)}`);
|
|
3037
|
-
}
|
|
3038
|
-
console.log(instructions);
|
|
3421
|
+
async function analyzeIntent(keyword) {
|
|
3422
|
+
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
3423
|
+
const analysis = (0, import_agent_runtime8.classifyIntent)(keyword);
|
|
3424
|
+
console.log((0, import_agent_runtime8.formatIntentReport)(keyword, analysis));
|
|
3039
3425
|
}
|
|
3040
|
-
function
|
|
3041
|
-
|
|
3426
|
+
function extractBodyText(html) {
|
|
3427
|
+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
3428
|
+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
3429
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
3430
|
+
text = text.replace(/ /g, " ");
|
|
3431
|
+
text = text.replace(/&/g, "&");
|
|
3432
|
+
text = text.replace(/</g, "<");
|
|
3433
|
+
text = text.replace(/>/g, ">");
|
|
3434
|
+
text = text.replace(/"/g, '"');
|
|
3435
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
3436
|
+
return text;
|
|
3042
3437
|
}
|
|
3043
3438
|
|
|
3044
3439
|
// src/commands/alerts.ts
|
|
@@ -3063,7 +3458,7 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
3063
3458
|
return answer.toLowerCase().startsWith("y");
|
|
3064
3459
|
}
|
|
3065
3460
|
async function alerts(options) {
|
|
3066
|
-
console.log("\n\u{1F514}
|
|
3461
|
+
console.log("\n\u{1F514} RankCLI - Alert Configuration\n");
|
|
3067
3462
|
const rl = readline3.createInterface({
|
|
3068
3463
|
input: process.stdin,
|
|
3069
3464
|
output: process.stdout
|
|
@@ -3206,245 +3601,8 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
3206
3601
|
}
|
|
3207
3602
|
}
|
|
3208
3603
|
|
|
3209
|
-
// src/commands/auth.ts
|
|
3210
|
-
var import_chalk6 = __toESM(require("chalk"));
|
|
3211
|
-
var import_ora5 = __toESM(require("ora"));
|
|
3212
|
-
var import_inquirer = __toESM(require("inquirer"));
|
|
3213
|
-
var import_open = __toESM(require("open"));
|
|
3214
|
-
async function browserLogin(spinner) {
|
|
3215
|
-
spinner.start("Starting login...");
|
|
3216
|
-
let start;
|
|
3217
|
-
try {
|
|
3218
|
-
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
3219
|
-
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
3220
|
-
start = await res.json();
|
|
3221
|
-
} catch (err) {
|
|
3222
|
-
spinner.fail("Could not start login");
|
|
3223
|
-
console.log(import_chalk6.default.red(err instanceof Error ? err.message : "Network error"));
|
|
3224
|
-
return;
|
|
3225
|
-
}
|
|
3226
|
-
spinner.text = "Opening browser to approve...";
|
|
3227
|
-
try {
|
|
3228
|
-
await (0, import_open.default)(start.url);
|
|
3229
|
-
} catch {
|
|
3230
|
-
}
|
|
3231
|
-
console.log(import_chalk6.default.dim(`
|
|
3232
|
-
If a browser didn't open, visit:
|
|
3233
|
-
${start.url}
|
|
3234
|
-
`));
|
|
3235
|
-
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
3236
|
-
spinner.text = "Waiting for approval in your browser...";
|
|
3237
|
-
while (Date.now() < deadline) {
|
|
3238
|
-
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3239
|
-
let status;
|
|
3240
|
-
try {
|
|
3241
|
-
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
3242
|
-
method: "POST",
|
|
3243
|
-
headers: { "Content-Type": "application/json" },
|
|
3244
|
-
body: JSON.stringify({ connectId: start.connectId })
|
|
3245
|
-
});
|
|
3246
|
-
status = await res.json();
|
|
3247
|
-
} catch {
|
|
3248
|
-
continue;
|
|
3249
|
-
}
|
|
3250
|
-
if (status.status === "claimed" && status.apiKey) {
|
|
3251
|
-
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
3252
|
-
await fetchAndSaveSubscription();
|
|
3253
|
-
spinner.succeed(`Logged in as ${import_chalk6.default.green(status.email)}`);
|
|
3254
|
-
return;
|
|
3255
|
-
}
|
|
3256
|
-
if (status.status === "expired" || status.status === "not_found") {
|
|
3257
|
-
spinner.fail("Login link expired");
|
|
3258
|
-
console.log(import_chalk6.default.dim('Run "rankcli login" again.'));
|
|
3259
|
-
return;
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
spinner.fail("Timed out waiting for approval");
|
|
3263
|
-
console.log(import_chalk6.default.dim(`Run "rankcli login" again when you're ready.`));
|
|
3264
|
-
}
|
|
3265
|
-
async function login(options) {
|
|
3266
|
-
if (isLoggedIn()) {
|
|
3267
|
-
const { email } = getUserInfo();
|
|
3268
|
-
console.log(import_chalk6.default.yellow(`Already logged in as ${email}`));
|
|
3269
|
-
console.log(import_chalk6.default.dim('Run "rankcli logout" to sign out first.'));
|
|
3270
|
-
return;
|
|
3271
|
-
}
|
|
3272
|
-
const spinner = (0, import_ora5.default)();
|
|
3273
|
-
try {
|
|
3274
|
-
if (options.token) {
|
|
3275
|
-
const apiKey = options.token;
|
|
3276
|
-
if (!apiKey.startsWith("rankcli_")) {
|
|
3277
|
-
console.log(import_chalk6.default.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
3278
|
-
return;
|
|
3279
|
-
}
|
|
3280
|
-
spinner.start("Validating API key...");
|
|
3281
|
-
const result = await validateApiKey(apiKey);
|
|
3282
|
-
if (!result.valid) {
|
|
3283
|
-
spinner.fail("Invalid API key");
|
|
3284
|
-
console.log(import_chalk6.default.red(`
|
|
3285
|
-
${result.error}`));
|
|
3286
|
-
return;
|
|
3287
|
-
}
|
|
3288
|
-
saveApiKey(apiKey);
|
|
3289
|
-
if (result.subscription) {
|
|
3290
|
-
saveSubscription({
|
|
3291
|
-
planId: result.subscription.planId,
|
|
3292
|
-
planName: result.subscription.planName,
|
|
3293
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3294
|
-
});
|
|
3295
|
-
}
|
|
3296
|
-
spinner.succeed(`Authenticated via API key`);
|
|
3297
|
-
console.log(import_chalk6.default.dim(`
|
|
3298
|
-
Email: ${result.user?.email}`));
|
|
3299
|
-
console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3300
|
-
return;
|
|
3301
|
-
}
|
|
3302
|
-
if (options.browser !== false && !options.email) {
|
|
3303
|
-
await browserLogin(spinner);
|
|
3304
|
-
} else {
|
|
3305
|
-
let email = options.email;
|
|
3306
|
-
if (!email) {
|
|
3307
|
-
const answers = await import_inquirer.default.prompt([
|
|
3308
|
-
{
|
|
3309
|
-
type: "input",
|
|
3310
|
-
name: "email",
|
|
3311
|
-
message: "Email:",
|
|
3312
|
-
validate: (input) => {
|
|
3313
|
-
if (!input.includes("@")) return "Please enter a valid email";
|
|
3314
|
-
return true;
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
]);
|
|
3318
|
-
email = answers.email;
|
|
3319
|
-
}
|
|
3320
|
-
const { password } = await import_inquirer.default.prompt([
|
|
3321
|
-
{
|
|
3322
|
-
type: "password",
|
|
3323
|
-
name: "password",
|
|
3324
|
-
message: "Password:",
|
|
3325
|
-
mask: "*"
|
|
3326
|
-
}
|
|
3327
|
-
]);
|
|
3328
|
-
spinner.start("Signing in...");
|
|
3329
|
-
const supabase = getSupabaseClient();
|
|
3330
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
3331
|
-
email,
|
|
3332
|
-
password
|
|
3333
|
-
});
|
|
3334
|
-
if (error) {
|
|
3335
|
-
spinner.fail("Login failed");
|
|
3336
|
-
console.log(import_chalk6.default.red(`
|
|
3337
|
-
${error.message}`));
|
|
3338
|
-
if (error.message.includes("Invalid login")) {
|
|
3339
|
-
console.log(import_chalk6.default.dim("\nDon't have an account? Sign up at:"));
|
|
3340
|
-
console.log(import_chalk6.default.cyan(` ${WEB_APP_URL}/signup`));
|
|
3341
|
-
}
|
|
3342
|
-
return;
|
|
3343
|
-
}
|
|
3344
|
-
if (!data.session) {
|
|
3345
|
-
spinner.fail("No session returned");
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
saveSession(data.session);
|
|
3349
|
-
await fetchAndSaveSubscription();
|
|
3350
|
-
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
|
|
3351
|
-
const { planName, sitesLimit } = getUserInfo();
|
|
3352
|
-
console.log(import_chalk6.default.dim(`
|
|
3353
|
-
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
3354
|
-
}
|
|
3355
|
-
} catch (err) {
|
|
3356
|
-
spinner.fail("Login failed");
|
|
3357
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
async function logout() {
|
|
3361
|
-
if (!isLoggedIn()) {
|
|
3362
|
-
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3363
|
-
return;
|
|
3364
|
-
}
|
|
3365
|
-
const { email } = getUserInfo();
|
|
3366
|
-
const spinner = (0, import_ora5.default)("Signing out...").start();
|
|
3367
|
-
try {
|
|
3368
|
-
const supabase = getSupabaseClient();
|
|
3369
|
-
await supabase.auth.signOut();
|
|
3370
|
-
clearCredentials();
|
|
3371
|
-
spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
|
|
3372
|
-
} catch (err) {
|
|
3373
|
-
spinner.fail("Logout failed");
|
|
3374
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
async function whoami() {
|
|
3378
|
-
if (!isLoggedIn()) {
|
|
3379
|
-
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3380
|
-
console.log(import_chalk6.default.dim('\nRun "rankcli login" to sign in.'));
|
|
3381
|
-
console.log(import_chalk6.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
3382
|
-
return;
|
|
3383
|
-
}
|
|
3384
|
-
const spinner = (0, import_ora5.default)("Checking account...").start();
|
|
3385
|
-
try {
|
|
3386
|
-
if (isApiKeyAuth()) {
|
|
3387
|
-
const apiKey = getApiKey();
|
|
3388
|
-
if (apiKey) {
|
|
3389
|
-
const result = await validateApiKey(apiKey);
|
|
3390
|
-
if (result.valid && result.subscription) {
|
|
3391
|
-
saveSubscription({
|
|
3392
|
-
planId: result.subscription.planId,
|
|
3393
|
-
planName: result.subscription.planName,
|
|
3394
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
3397
|
-
}
|
|
3398
|
-
} else {
|
|
3399
|
-
await fetchAndSaveSubscription();
|
|
3400
|
-
}
|
|
3401
|
-
const { email, planName, sitesLimit } = getUserInfo();
|
|
3402
|
-
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
3403
|
-
spinner.stop();
|
|
3404
|
-
console.log(import_chalk6.default.bold("\nRankCLI Account"));
|
|
3405
|
-
console.log("\u2500".repeat(40));
|
|
3406
|
-
console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
|
|
3407
|
-
console.log(` Plan: ${import_chalk6.default.green(planName)}`);
|
|
3408
|
-
console.log(` Sites: ${sitesLimit} allowed`);
|
|
3409
|
-
console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
|
|
3410
|
-
console.log("\u2500".repeat(40));
|
|
3411
|
-
console.log(import_chalk6.default.dim(`
|
|
3412
|
-
Manage at: ${WEB_APP_URL}/account`));
|
|
3413
|
-
} catch (err) {
|
|
3414
|
-
spinner.fail("Failed to fetch account info");
|
|
3415
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
async function fetchAndSaveSubscription() {
|
|
3419
|
-
const supabase = await getAuthenticatedClient();
|
|
3420
|
-
if (!supabase) return;
|
|
3421
|
-
const { userId } = getUserInfo();
|
|
3422
|
-
if (!userId) return;
|
|
3423
|
-
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
3424
|
-
plan:subscription_plans (
|
|
3425
|
-
plan_id,
|
|
3426
|
-
name,
|
|
3427
|
-
sites_limit
|
|
3428
|
-
)
|
|
3429
|
-
`).eq("user_id", userId).single();
|
|
3430
|
-
if (subscription?.plan) {
|
|
3431
|
-
const plan = subscription.plan;
|
|
3432
|
-
saveSubscription({
|
|
3433
|
-
planId: plan.plan_id,
|
|
3434
|
-
planName: plan.name,
|
|
3435
|
-
sitesLimit: plan.sites_limit
|
|
3436
|
-
});
|
|
3437
|
-
} else {
|
|
3438
|
-
saveSubscription({
|
|
3439
|
-
planId: void 0,
|
|
3440
|
-
planName: "Free",
|
|
3441
|
-
sitesLimit: 1
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
|
|
3446
3604
|
// src/version.ts
|
|
3447
|
-
var VERSION = "0.0.
|
|
3605
|
+
var VERSION = "0.0.25";
|
|
3448
3606
|
|
|
3449
3607
|
// src/index.ts
|
|
3450
3608
|
var program = new import_commander.Command();
|
|
@@ -3452,9 +3610,9 @@ program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets
|
|
|
3452
3610
|
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);
|
|
3453
3611
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3454
3612
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3455
|
-
program.command("init").description("
|
|
3613
|
+
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);
|
|
3456
3614
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
3457
|
-
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
|
|
3615
|
+
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);
|
|
3458
3616
|
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);
|
|
3459
3617
|
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) => {
|
|
3460
3618
|
const result = await fix({
|