@rankcli/cli 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1189 -993
- package/dist/index.mjs +1147 -949
- package/package.json +3 -3
package/dist/index.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"));
|
|
@@ -373,11 +126,14 @@ function isApiKeyAuth() {
|
|
|
373
126
|
function getApiKey() {
|
|
374
127
|
return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
|
|
375
128
|
}
|
|
376
|
-
function saveApiKey(apiKey, keyName) {
|
|
129
|
+
function saveApiKey(apiKey, keyName, email) {
|
|
377
130
|
config.set("apiKey", apiKey);
|
|
378
131
|
if (keyName) {
|
|
379
132
|
config.set("apiKeyName", keyName);
|
|
380
133
|
}
|
|
134
|
+
if (email) {
|
|
135
|
+
config.set("email", email);
|
|
136
|
+
}
|
|
381
137
|
}
|
|
382
138
|
async function validateApiKey(apiKey) {
|
|
383
139
|
try {
|
|
@@ -483,14 +239,806 @@ async function getAuthenticatedClient() {
|
|
|
483
239
|
return supabase;
|
|
484
240
|
}
|
|
485
241
|
|
|
486
|
-
// src/
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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,
|
|
494
1042
|
features: { geo: false, auto_fix: false, github: false, ai: false }
|
|
495
1043
|
},
|
|
496
1044
|
free: {
|
|
@@ -523,133 +1071,133 @@ var UPSELL_MESSAGES = {
|
|
|
523
1071
|
// After free audit completes
|
|
524
1072
|
free_audit_complete: () => [
|
|
525
1073
|
"",
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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."),
|
|
529
1077
|
"",
|
|
530
|
-
|
|
531
|
-
|
|
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"),
|
|
532
1080
|
""
|
|
533
1081
|
],
|
|
534
1082
|
// When checks limit is reached
|
|
535
1083
|
checks_limit: (ctx) => [
|
|
536
1084
|
"",
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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`),
|
|
540
1088
|
""
|
|
541
1089
|
],
|
|
542
1090
|
// When domain limit is reached
|
|
543
1091
|
domain_limit: (ctx) => [
|
|
544
1092
|
"",
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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.`),
|
|
548
1096
|
"",
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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"),
|
|
552
1100
|
""
|
|
553
1101
|
],
|
|
554
1102
|
// When GEO features are limited
|
|
555
1103
|
geo_limited: (ctx) => [
|
|
556
1104
|
"",
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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"),
|
|
561
1109
|
"",
|
|
562
|
-
|
|
563
|
-
|
|
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"),
|
|
564
1112
|
""
|
|
565
1113
|
],
|
|
566
1114
|
// When auto-fix is attempted without access
|
|
567
1115
|
auto_fix_blocked: () => [
|
|
568
1116
|
"",
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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."),
|
|
572
1120
|
"",
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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"),
|
|
577
1125
|
"",
|
|
578
|
-
|
|
579
|
-
|
|
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"),
|
|
580
1128
|
""
|
|
581
1129
|
],
|
|
582
1130
|
// When GitHub integration is attempted without access
|
|
583
1131
|
github_blocked: () => [
|
|
584
1132
|
"",
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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."),
|
|
588
1136
|
"",
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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"),
|
|
594
1142
|
"",
|
|
595
|
-
|
|
596
|
-
|
|
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"),
|
|
597
1145
|
""
|
|
598
1146
|
],
|
|
599
1147
|
// Keyword tracking limit reached
|
|
600
1148
|
keyword_limit: (ctx) => [
|
|
601
1149
|
"",
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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}`),
|
|
606
1154
|
"",
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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"),
|
|
611
1159
|
"",
|
|
612
|
-
|
|
613
|
-
|
|
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"),
|
|
614
1162
|
""
|
|
615
1163
|
],
|
|
616
1164
|
// Rate limit exceeded
|
|
617
1165
|
rate_limit: (ctx) => [
|
|
618
1166
|
"",
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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.`),
|
|
622
1170
|
"",
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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"),
|
|
627
1175
|
"",
|
|
628
|
-
|
|
629
|
-
|
|
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"),
|
|
630
1178
|
""
|
|
631
1179
|
],
|
|
632
1180
|
// Schedule exceeded (for GitHub Action)
|
|
633
1181
|
schedule_exceeded: (ctx) => [
|
|
634
1182
|
"",
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.`),
|
|
638
1186
|
"",
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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"),
|
|
643
1191
|
"",
|
|
644
|
-
|
|
645
|
-
|
|
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"),
|
|
646
1194
|
""
|
|
647
1195
|
],
|
|
648
1196
|
// Login prompt (soft)
|
|
649
1197
|
login_prompt: () => [
|
|
650
1198
|
"",
|
|
651
|
-
|
|
652
|
-
|
|
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"),
|
|
653
1201
|
""
|
|
654
1202
|
]
|
|
655
1203
|
};
|
|
@@ -670,8 +1218,8 @@ function hasFeature(tier, feature) {
|
|
|
670
1218
|
|
|
671
1219
|
// src/wasm-engine.ts
|
|
672
1220
|
var vm = __toESM(require("vm"));
|
|
673
|
-
var
|
|
674
|
-
var
|
|
1221
|
+
var path2 = __toESM(require("path"));
|
|
1222
|
+
var fs2 = __toESM(require("fs"));
|
|
675
1223
|
var os = __toESM(require("os"));
|
|
676
1224
|
var PREMIUM_CHECK_NAMES = [
|
|
677
1225
|
"analyzeFeaturedSnippet",
|
|
@@ -708,12 +1256,12 @@ async function loadWasm() {
|
|
|
708
1256
|
return wasmModule;
|
|
709
1257
|
}
|
|
710
1258
|
const possiblePaths = [
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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")
|
|
714
1262
|
];
|
|
715
1263
|
for (const wasmPath of possiblePaths) {
|
|
716
|
-
if (
|
|
1264
|
+
if (fs2.existsSync(wasmPath)) {
|
|
717
1265
|
wasmModule = require(wasmPath);
|
|
718
1266
|
return wasmModule;
|
|
719
1267
|
}
|
|
@@ -725,8 +1273,8 @@ Searched: ${possiblePaths.join(", ")}`
|
|
|
725
1273
|
}
|
|
726
1274
|
function extractSeaWasmAssets() {
|
|
727
1275
|
const sea = require("sea");
|
|
728
|
-
const dir =
|
|
729
|
-
|
|
1276
|
+
const dir = path2.join(os.tmpdir(), "rankcli-wasm-sea");
|
|
1277
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
730
1278
|
const glueAsset = sea.getRawAsset("rankcli_audit_engine.js");
|
|
731
1279
|
const wasmAsset = sea.getRawAsset("rankcli_audit_engine_bg.wasm");
|
|
732
1280
|
if (typeof glueAsset === "string" || typeof wasmAsset === "string") {
|
|
@@ -734,12 +1282,12 @@ function extractSeaWasmAssets() {
|
|
|
734
1282
|
}
|
|
735
1283
|
const glueBuffer = Buffer.from(glueAsset);
|
|
736
1284
|
const wasmBuffer = Buffer.from(wasmAsset);
|
|
737
|
-
const gluePath =
|
|
738
|
-
const wasmPath =
|
|
739
|
-
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;
|
|
740
1288
|
if (existingSize !== wasmBuffer.length) {
|
|
741
|
-
|
|
742
|
-
|
|
1289
|
+
fs2.writeFileSync(gluePath, glueBuffer);
|
|
1290
|
+
fs2.writeFileSync(wasmPath, wasmBuffer);
|
|
743
1291
|
}
|
|
744
1292
|
return gluePath;
|
|
745
1293
|
}
|
|
@@ -834,7 +1382,7 @@ async function getPremiumChecks() {
|
|
|
834
1382
|
}
|
|
835
1383
|
|
|
836
1384
|
// src/commands/audit.ts
|
|
837
|
-
var { detectFramework: detectFramework2, getFrameworkDisplayName } =
|
|
1385
|
+
var { detectFramework: detectFramework2, getFrameworkDisplayName } = import_agent_runtime2.frameworks;
|
|
838
1386
|
async function fetchHtml(url, timeout = 1e4) {
|
|
839
1387
|
try {
|
|
840
1388
|
const controller = new AbortController();
|
|
@@ -855,9 +1403,16 @@ async function fetchHtml(url, timeout = 1e4) {
|
|
|
855
1403
|
return null;
|
|
856
1404
|
}
|
|
857
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
|
+
};
|
|
858
1412
|
async function runAIAnalysis(url, issues, provider, apiKey) {
|
|
859
1413
|
const isAnthropic = provider === "anthropic";
|
|
860
|
-
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`;
|
|
861
1416
|
const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
|
|
862
1417
|
|
|
863
1418
|
URL: ${url}
|
|
@@ -888,19 +1443,26 @@ Return JSON with:
|
|
|
888
1443
|
});
|
|
889
1444
|
} else {
|
|
890
1445
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1446
|
+
if (provider === "openrouter") {
|
|
1447
|
+
headers["HTTP-Referer"] = "https://rankcli.dev";
|
|
1448
|
+
headers["X-Title"] = "RankCLI";
|
|
1449
|
+
}
|
|
891
1450
|
body = JSON.stringify({
|
|
892
|
-
model:
|
|
1451
|
+
model: providerConfig.model,
|
|
893
1452
|
messages: [
|
|
894
1453
|
{ role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
|
|
895
1454
|
{ role: "user", content: prompt }
|
|
896
1455
|
],
|
|
897
1456
|
max_tokens: 1500,
|
|
898
|
-
|
|
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" } } : {}
|
|
899
1461
|
});
|
|
900
1462
|
}
|
|
901
1463
|
const response = await fetch(apiUrl, { method: "POST", headers, body });
|
|
902
1464
|
if (!response.ok) {
|
|
903
|
-
console.error(
|
|
1465
|
+
console.error(import_chalk4.default.yellow(`AI analysis failed: ${response.status}`));
|
|
904
1466
|
return null;
|
|
905
1467
|
}
|
|
906
1468
|
const data = await response.json();
|
|
@@ -910,7 +1472,7 @@ Return JSON with:
|
|
|
910
1472
|
if (!jsonMatch) return null;
|
|
911
1473
|
return JSON.parse(jsonMatch[0]);
|
|
912
1474
|
} catch (error) {
|
|
913
|
-
console.error(
|
|
1475
|
+
console.error(import_chalk4.default.yellow(`AI analysis error: ${error}`));
|
|
914
1476
|
return null;
|
|
915
1477
|
}
|
|
916
1478
|
}
|
|
@@ -945,9 +1507,9 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
945
1507
|
const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
|
|
946
1508
|
const limit = sitesLimit ?? 1;
|
|
947
1509
|
if (count !== null && count >= limit) {
|
|
948
|
-
console.log(
|
|
1510
|
+
console.log(import_chalk4.default.yellow(`
|
|
949
1511
|
\u26A0\uFE0F Project limit reached (${count}/${limit})`));
|
|
950
|
-
console.log(
|
|
1512
|
+
console.log(import_chalk4.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
|
|
951
1513
|
return null;
|
|
952
1514
|
}
|
|
953
1515
|
const { data: newProject, error: projectError } = await supabase.from("projects").insert({
|
|
@@ -957,7 +1519,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
957
1519
|
domain
|
|
958
1520
|
}).select("id").single();
|
|
959
1521
|
if (projectError || !newProject) {
|
|
960
|
-
console.error(
|
|
1522
|
+
console.error(import_chalk4.default.dim("Failed to create project"));
|
|
961
1523
|
return null;
|
|
962
1524
|
}
|
|
963
1525
|
projectId = newProject.id;
|
|
@@ -998,7 +1560,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
998
1560
|
ai_analysis: aiAnalysis
|
|
999
1561
|
}).select("id").single();
|
|
1000
1562
|
if (auditError || !audit2) {
|
|
1001
|
-
console.error(
|
|
1563
|
+
console.error(import_chalk4.default.dim("Failed to save audit"));
|
|
1002
1564
|
return null;
|
|
1003
1565
|
}
|
|
1004
1566
|
if (mergedIssues.length > 0) {
|
|
@@ -1026,7 +1588,7 @@ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis,
|
|
|
1026
1588
|
}).eq("id", projectId);
|
|
1027
1589
|
return { projectId, auditId: audit2.id };
|
|
1028
1590
|
} catch (error) {
|
|
1029
|
-
console.error(
|
|
1591
|
+
console.error(import_chalk4.default.dim("Cloud sync failed"));
|
|
1030
1592
|
return null;
|
|
1031
1593
|
}
|
|
1032
1594
|
}
|
|
@@ -1034,8 +1596,16 @@ async function audit(options) {
|
|
|
1034
1596
|
const url = options.url;
|
|
1035
1597
|
const maxPages = parseInt(options.maxPages || "5", 10);
|
|
1036
1598
|
const enableAI = options.ai || false;
|
|
1037
|
-
const
|
|
1038
|
-
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] || "";
|
|
1039
1609
|
const loggedIn = isLoggedIn();
|
|
1040
1610
|
const { planId } = getUserInfo();
|
|
1041
1611
|
const tierId = planId && planId in TIER_LIMITS ? planId : loggedIn ? "free" : "free_anon";
|
|
@@ -1048,7 +1618,7 @@ async function audit(options) {
|
|
|
1048
1618
|
await initEngine(passphrase);
|
|
1049
1619
|
premiumChecks = await getPremiumChecks();
|
|
1050
1620
|
} catch (error) {
|
|
1051
|
-
console.error(
|
|
1621
|
+
console.error(import_chalk4.default.dim(`(premium checks unavailable: ${error instanceof Error ? error.message : error})`));
|
|
1052
1622
|
}
|
|
1053
1623
|
}
|
|
1054
1624
|
}
|
|
@@ -1058,12 +1628,12 @@ async function audit(options) {
|
|
|
1058
1628
|
console.log("\nOptions:");
|
|
1059
1629
|
console.log(" --max-pages <n> Max pages to crawl (default: 5)");
|
|
1060
1630
|
console.log(" --ai Enable AI analysis");
|
|
1061
|
-
console.log(" --ai-provider <p> AI provider: openai or
|
|
1062
|
-
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)");
|
|
1063
1633
|
console.log(" --output json Output as JSON");
|
|
1064
1634
|
if (!loggedIn) {
|
|
1065
|
-
console.log(
|
|
1066
|
-
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"));
|
|
1067
1637
|
}
|
|
1068
1638
|
console.log("");
|
|
1069
1639
|
return;
|
|
@@ -1076,13 +1646,13 @@ async function audit(options) {
|
|
|
1076
1646
|
}
|
|
1077
1647
|
const startTime = Date.now();
|
|
1078
1648
|
try {
|
|
1079
|
-
const discoverySpinner = (0,
|
|
1649
|
+
const discoverySpinner = (0, import_ora2.default)("Discovering pages...").start();
|
|
1080
1650
|
let pagesToAudit = [url];
|
|
1081
1651
|
const homepageHtml = await fetchHtml(url, 1e4);
|
|
1082
1652
|
let detectedFramework = detectFramework2({});
|
|
1083
1653
|
if (homepageHtml) {
|
|
1084
1654
|
detectedFramework = detectFramework2({ html: homepageHtml });
|
|
1085
|
-
const discovered = (0,
|
|
1655
|
+
const discovered = (0, import_agent_runtime2.discoverPagesFromLinks)(url, homepageHtml, maxPages - 1);
|
|
1086
1656
|
const additionalPages = discovered.map((p) => p.url).filter((u) => u !== url);
|
|
1087
1657
|
pagesToAudit = [url, ...additionalPages];
|
|
1088
1658
|
const frameworkLabel = detectedFramework.framework !== "unknown" ? ` (${getFrameworkDisplayName(detectedFramework.framework)})` : "";
|
|
@@ -1090,10 +1660,10 @@ async function audit(options) {
|
|
|
1090
1660
|
} else {
|
|
1091
1661
|
discoverySpinner.warn("Could not fetch homepage, auditing URL only");
|
|
1092
1662
|
}
|
|
1093
|
-
const auditSpinner = (0,
|
|
1663
|
+
const auditSpinner = (0, import_ora2.default)(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
|
|
1094
1664
|
const auditPage = async (pageUrl, index) => {
|
|
1095
1665
|
try {
|
|
1096
|
-
const report = await (0,
|
|
1666
|
+
const report = await (0, import_agent_runtime2.runFullAudit)({
|
|
1097
1667
|
url: pageUrl,
|
|
1098
1668
|
maxPages: 1,
|
|
1099
1669
|
checkBrokenLinks: options.checkLinks || false,
|
|
@@ -1137,7 +1707,7 @@ async function audit(options) {
|
|
|
1137
1707
|
const mergedIssues = mergeIssues(allIssues);
|
|
1138
1708
|
let aiAnalysis = null;
|
|
1139
1709
|
if (enableAI && aiKey) {
|
|
1140
|
-
const aiSpinner = (0,
|
|
1710
|
+
const aiSpinner = (0, import_ora2.default)(`Running AI analysis (${aiProvider})...`).start();
|
|
1141
1711
|
aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
|
|
1142
1712
|
if (aiAnalysis) {
|
|
1143
1713
|
aiSpinner.succeed("AI analysis complete");
|
|
@@ -1145,8 +1715,8 @@ async function audit(options) {
|
|
|
1145
1715
|
aiSpinner.warn("AI analysis failed or returned no results");
|
|
1146
1716
|
}
|
|
1147
1717
|
} else if (enableAI && !aiKey) {
|
|
1148
|
-
console.log(
|
|
1149
|
-
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"));
|
|
1150
1720
|
}
|
|
1151
1721
|
const totalTime = Date.now() - startTime;
|
|
1152
1722
|
const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
|
|
@@ -1174,27 +1744,27 @@ async function audit(options) {
|
|
|
1174
1744
|
};
|
|
1175
1745
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1176
1746
|
} else {
|
|
1177
|
-
console.log("\n" +
|
|
1178
|
-
console.log(
|
|
1179
|
-
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)));
|
|
1180
1750
|
const score = primaryReport.healthScore?.overall || 0;
|
|
1181
|
-
const scoreColor = score >= 80 ?
|
|
1751
|
+
const scoreColor = score >= 80 ? import_chalk4.default.green : score >= 60 ? import_chalk4.default.yellow : import_chalk4.default.red;
|
|
1182
1752
|
console.log(`
|
|
1183
|
-
${
|
|
1184
|
-
console.log(` ${
|
|
1185
|
-
console.log(` ${
|
|
1186
|
-
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`);
|
|
1187
1757
|
if (frameworkName) {
|
|
1188
|
-
console.log(` ${
|
|
1758
|
+
console.log(` ${import_chalk4.default.bold("Framework:")} ${import_chalk4.default.cyan(frameworkName)}`);
|
|
1189
1759
|
}
|
|
1190
1760
|
if (primaryReport.healthScore) {
|
|
1191
|
-
console.log("\n" +
|
|
1761
|
+
console.log("\n" + import_chalk4.default.bold(" Health Scores:"));
|
|
1192
1762
|
const hs = primaryReport.healthScore;
|
|
1193
1763
|
const bar = (score2) => {
|
|
1194
1764
|
const filled = Math.round(score2 / 5);
|
|
1195
1765
|
const empty = 20 - filled;
|
|
1196
|
-
const color = score2 >= 80 ?
|
|
1197
|
-
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));
|
|
1198
1768
|
};
|
|
1199
1769
|
console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
|
|
1200
1770
|
console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
|
|
@@ -1207,35 +1777,35 @@ async function audit(options) {
|
|
|
1207
1777
|
const errors = mergedIssues.filter((i) => i.severity === "error");
|
|
1208
1778
|
const warnings = mergedIssues.filter((i) => i.severity === "warning");
|
|
1209
1779
|
const notices = mergedIssues.filter((i) => i.severity === "notice");
|
|
1210
|
-
console.log("\n" +
|
|
1211
|
-
console.log(` ${
|
|
1212
|
-
console.log(` ${
|
|
1213
|
-
console.log(` ${
|
|
1214
|
-
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:"));
|
|
1215
1785
|
const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
|
|
1216
1786
|
topIssues.forEach((issue, i) => {
|
|
1217
|
-
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");
|
|
1218
1788
|
console.log(` ${i + 1}. ${icon} ${issue.title}`);
|
|
1219
1789
|
if (issue.affectedUrls && issue.affectedUrls.length > 1) {
|
|
1220
|
-
console.log(
|
|
1790
|
+
console.log(import_chalk4.default.dim(` Affects ${issue.affectedUrls.length} pages`));
|
|
1221
1791
|
}
|
|
1222
1792
|
});
|
|
1223
1793
|
if (aiAnalysis) {
|
|
1224
|
-
console.log("\n" +
|
|
1794
|
+
console.log("\n" + import_chalk4.default.bold(" AI Analysis:"));
|
|
1225
1795
|
console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
|
|
1226
|
-
console.log("\n " +
|
|
1796
|
+
console.log("\n " + import_chalk4.default.bold("Suggestions:"));
|
|
1227
1797
|
aiAnalysis.suggestions.forEach((s, i) => {
|
|
1228
1798
|
console.log(` ${i + 1}. ${s}`);
|
|
1229
1799
|
});
|
|
1230
1800
|
if (aiAnalysis.prioritizedFixes?.length > 0) {
|
|
1231
|
-
console.log("\n " +
|
|
1801
|
+
console.log("\n " + import_chalk4.default.bold("Priority Fixes:"));
|
|
1232
1802
|
aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix2, i) => {
|
|
1233
1803
|
console.log(` ${i + 1}. [P${fix2.priority}] ${fix2.code}`);
|
|
1234
|
-
console.log(
|
|
1804
|
+
console.log(import_chalk4.default.dim(` ${fix2.rationale}`));
|
|
1235
1805
|
});
|
|
1236
1806
|
}
|
|
1237
1807
|
}
|
|
1238
|
-
console.log("\n" +
|
|
1808
|
+
console.log("\n" + import_chalk4.default.bold("\u2550".repeat(60)));
|
|
1239
1809
|
const userInfo = getUserInfo();
|
|
1240
1810
|
const tier = userInfo.planName?.toLowerCase() || "free";
|
|
1241
1811
|
const isFreeTier = !loggedIn || tier === "free";
|
|
@@ -1251,13 +1821,13 @@ async function audit(options) {
|
|
|
1251
1821
|
showUpsell("geo_limited", { tier: loggedIn ? "free" : "free_anon" });
|
|
1252
1822
|
}
|
|
1253
1823
|
if (loggedIn) {
|
|
1254
|
-
const syncSpinner = (0,
|
|
1824
|
+
const syncSpinner = (0, import_ora2.default)("Syncing to cloud...").start();
|
|
1255
1825
|
const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis, totalTime, actualChecksRun);
|
|
1256
1826
|
if (syncResult) {
|
|
1257
1827
|
syncSpinner.succeed("Results synced to cloud");
|
|
1258
|
-
console.log(
|
|
1828
|
+
console.log(import_chalk4.default.cyan(`
|
|
1259
1829
|
View and manage issues in the web dashboard:`));
|
|
1260
|
-
console.log(
|
|
1830
|
+
console.log(import_chalk4.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
|
|
1261
1831
|
`));
|
|
1262
1832
|
} else {
|
|
1263
1833
|
syncSpinner.warn("Could not sync to cloud");
|
|
@@ -1276,7 +1846,7 @@ async function audit(options) {
|
|
|
1276
1846
|
// src/commands/apply.ts
|
|
1277
1847
|
var import_fs3 = require("fs");
|
|
1278
1848
|
var import_path3 = require("path");
|
|
1279
|
-
var
|
|
1849
|
+
var import_agent_runtime3 = require("@rankcli/agent-runtime");
|
|
1280
1850
|
var SEVERITY_ICON = {
|
|
1281
1851
|
critical: "\u274C",
|
|
1282
1852
|
warning: "\u26A0\uFE0F",
|
|
@@ -1304,7 +1874,7 @@ function truncate(str, maxLen) {
|
|
|
1304
1874
|
}
|
|
1305
1875
|
async function apply(options) {
|
|
1306
1876
|
const cwd = process.cwd();
|
|
1307
|
-
const configPath = (0, import_path3.join)(cwd, "
|
|
1877
|
+
const configPath = (0, import_path3.join)(cwd, "rankcli.config.json");
|
|
1308
1878
|
let config2 = null;
|
|
1309
1879
|
if ((0, import_fs3.existsSync)(configPath)) {
|
|
1310
1880
|
try {
|
|
@@ -1318,8 +1888,8 @@ async function apply(options) {
|
|
|
1318
1888
|
}
|
|
1319
1889
|
if (!url) {
|
|
1320
1890
|
console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
|
|
1321
|
-
console.log("
|
|
1322
|
-
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");
|
|
1323
1893
|
return;
|
|
1324
1894
|
}
|
|
1325
1895
|
try {
|
|
@@ -1328,7 +1898,7 @@ async function apply(options) {
|
|
|
1328
1898
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
1329
1899
|
return;
|
|
1330
1900
|
}
|
|
1331
|
-
console.log("\n\u{1F680}
|
|
1901
|
+
console.log("\n\u{1F680} RankCLI - Apply Fixes");
|
|
1332
1902
|
console.log("\u2550".repeat(50));
|
|
1333
1903
|
const loggedIn = isLoggedIn();
|
|
1334
1904
|
const userInfo = getUserInfo();
|
|
@@ -1339,7 +1909,7 @@ async function apply(options) {
|
|
|
1339
1909
|
return;
|
|
1340
1910
|
}
|
|
1341
1911
|
console.log("\n\u{1F50D} Detecting framework...");
|
|
1342
|
-
const frameworkResult = await (0,
|
|
1912
|
+
const frameworkResult = await (0, import_agent_runtime3.detectFramework)({ cwd });
|
|
1343
1913
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1344
1914
|
console.log(` Framework: ${framework.name}`);
|
|
1345
1915
|
console.log(`
|
|
@@ -1349,16 +1919,23 @@ async function apply(options) {
|
|
|
1349
1919
|
let score;
|
|
1350
1920
|
let auditResult = null;
|
|
1351
1921
|
try {
|
|
1352
|
-
auditResult = await (0,
|
|
1922
|
+
auditResult = await (0, import_agent_runtime3.runAuditWithFixes)({
|
|
1353
1923
|
url,
|
|
1354
1924
|
generateFixes: true
|
|
1355
1925
|
});
|
|
1356
1926
|
issues = auditResult.report.issues.map((issue) => ({
|
|
1357
|
-
severity
|
|
1927
|
+
// Squash the audit engine's 5-level severity into generateFixes's
|
|
1928
|
+
// 3-level SEOIssue scheme - matches how audit.ts already treats
|
|
1929
|
+
// 'error' as critical-weight (red ✗) and 'notice' as info-weight.
|
|
1930
|
+
severity: issue.severity === "error" ? "critical" : issue.severity === "notice" ? "info" : issue.severity,
|
|
1358
1931
|
category: issue.category,
|
|
1359
1932
|
code: issue.code,
|
|
1360
1933
|
message: issue.title,
|
|
1361
1934
|
impact: issue.description || void 0
|
|
1935
|
+
// Cast covers `category`: the audit engine's categories (17 values,
|
|
1936
|
+
// e.g. 'crawlability', 'links') don't map cleanly onto SEOIssue's
|
|
1937
|
+
// narrower set - generateFixes never reads category off its input
|
|
1938
|
+
// (each fixer hardcodes its own), so this is safe, not just silenced.
|
|
1362
1939
|
}));
|
|
1363
1940
|
score = auditResult.score;
|
|
1364
1941
|
} catch (error) {
|
|
@@ -1374,7 +1951,7 @@ async function apply(options) {
|
|
|
1374
1951
|
return;
|
|
1375
1952
|
}
|
|
1376
1953
|
console.log("\n\u{1F527} Generating fixes...");
|
|
1377
|
-
const fixes = await (0,
|
|
1954
|
+
const fixes = await (0, import_agent_runtime3.generateFixes)(issues, {
|
|
1378
1955
|
cwd,
|
|
1379
1956
|
url,
|
|
1380
1957
|
framework
|
|
@@ -1413,7 +1990,7 @@ async function apply(options) {
|
|
|
1413
1990
|
}
|
|
1414
1991
|
}
|
|
1415
1992
|
console.log("\n\u{1F527} Applying fixes...");
|
|
1416
|
-
const { applied, skipped } = await (0,
|
|
1993
|
+
const { applied, skipped } = await (0, import_agent_runtime3.applyFixes)(fixes, { cwd, dryRun: false });
|
|
1417
1994
|
console.log("\n" + "\u2550".repeat(50));
|
|
1418
1995
|
console.log("RESULTS");
|
|
1419
1996
|
console.log("\u2550".repeat(50));
|
|
@@ -1444,17 +2021,17 @@ async function apply(options) {
|
|
|
1444
2021
|
console.log("\n2. Commit the SEO improvements:");
|
|
1445
2022
|
console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
|
|
1446
2023
|
console.log("\n3. Re-run the audit to verify:");
|
|
1447
|
-
console.log(`
|
|
2024
|
+
console.log(` rankcli audit --url ${url}`);
|
|
1448
2025
|
console.log("");
|
|
1449
2026
|
}
|
|
1450
2027
|
|
|
1451
2028
|
// src/commands/fix.ts
|
|
1452
|
-
var
|
|
2029
|
+
var import_child_process2 = require("child_process");
|
|
1453
2030
|
var import_util = require("util");
|
|
1454
|
-
var
|
|
1455
|
-
var
|
|
1456
|
-
var
|
|
1457
|
-
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);
|
|
1458
2035
|
var SEVERITY_ICON2 = {
|
|
1459
2036
|
critical: "\u274C",
|
|
1460
2037
|
error: "\u274C",
|
|
@@ -1471,8 +2048,7 @@ function generateCommitMessage(fixes, url) {
|
|
|
1471
2048
|
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1472
2049
|
);
|
|
1473
2050
|
const type = isAddingNew ? "feat" : "fix";
|
|
1474
|
-
const
|
|
1475
|
-
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
2051
|
+
const categoryStr = "seo";
|
|
1476
2052
|
const issueCount = applied.length;
|
|
1477
2053
|
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1478
2054
|
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
@@ -1521,19 +2097,19 @@ function printFix2(fix2, index) {
|
|
|
1521
2097
|
const icon = SEVERITY_ICON2[fix2.issue.severity] || "\u2022";
|
|
1522
2098
|
console.log(`
|
|
1523
2099
|
${index + 1}. ${icon} ${fix2.issue.message}`);
|
|
1524
|
-
console.log(` Code: ${
|
|
1525
|
-
console.log(` File: ${
|
|
2100
|
+
console.log(` Code: ${import_chalk5.default.dim(fix2.issue.code)}`);
|
|
2101
|
+
console.log(` File: ${import_chalk5.default.cyan(fix2.file)}`);
|
|
1526
2102
|
if (fix2.skipped) {
|
|
1527
|
-
console.log(` Status: ${
|
|
2103
|
+
console.log(` Status: ${import_chalk5.default.yellow("SKIPPED")} - ${fix2.skipReason}`);
|
|
1528
2104
|
} else if (fix2.applied) {
|
|
1529
|
-
console.log(` Status: ${
|
|
1530
|
-
console.log(` ${
|
|
2105
|
+
console.log(` Status: ${import_chalk5.default.green("APPLIED")}`);
|
|
2106
|
+
console.log(` ${import_chalk5.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1531
2107
|
} else {
|
|
1532
|
-
console.log(` ${
|
|
2108
|
+
console.log(` ${import_chalk5.default.dim("Explanation:")} ${fix2.explanation}`);
|
|
1533
2109
|
if (fix2.before) {
|
|
1534
|
-
console.log(` ${
|
|
2110
|
+
console.log(` ${import_chalk5.default.red("Before:")} ${truncate2(fix2.before, 60)}`);
|
|
1535
2111
|
}
|
|
1536
|
-
console.log(` ${
|
|
2112
|
+
console.log(` ${import_chalk5.default.green("After:")} ${truncate2(fix2.after, 100)}`);
|
|
1537
2113
|
}
|
|
1538
2114
|
}
|
|
1539
2115
|
async function fix(options) {
|
|
@@ -1577,15 +2153,15 @@ async function fix(options) {
|
|
|
1577
2153
|
console.log("\n\u{1F527} RankCLI Fix - Agentic SEO Fixer");
|
|
1578
2154
|
console.log("\u2550".repeat(50));
|
|
1579
2155
|
}
|
|
1580
|
-
const frameworkSpinner = options.output !== "json" ? (0,
|
|
1581
|
-
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 });
|
|
1582
2158
|
const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
|
|
1583
2159
|
frameworkSpinner?.succeed(`Framework: ${framework.name}`);
|
|
1584
|
-
const analyzeSpinner = options.output !== "json" ? (0,
|
|
2160
|
+
const analyzeSpinner = options.output !== "json" ? (0, import_ora3.default)(`Analyzing ${url}...`).start() : null;
|
|
1585
2161
|
let issues;
|
|
1586
2162
|
let score;
|
|
1587
2163
|
try {
|
|
1588
|
-
const analysis = await (0,
|
|
2164
|
+
const analysis = await (0, import_agent_runtime4.runDirectAnalysis)(url);
|
|
1589
2165
|
issues = analysis.issues;
|
|
1590
2166
|
score = analysis.score;
|
|
1591
2167
|
} catch (error) {
|
|
@@ -1598,7 +2174,7 @@ async function fix(options) {
|
|
|
1598
2174
|
const onlyCodes = new Set(options.only.map((c) => c.toUpperCase()));
|
|
1599
2175
|
issues = issues.filter((i) => onlyCodes.has(i.code.toUpperCase()));
|
|
1600
2176
|
if (options.output !== "json") {
|
|
1601
|
-
console.log(
|
|
2177
|
+
console.log(import_chalk5.default.dim(` Filtered to ${issues.length} issue(s) matching: ${options.only.join(", ")}`));
|
|
1602
2178
|
}
|
|
1603
2179
|
}
|
|
1604
2180
|
if (issues.length === 0) {
|
|
@@ -1615,8 +2191,8 @@ async function fix(options) {
|
|
|
1615
2191
|
durationMs: Date.now() - startTime
|
|
1616
2192
|
};
|
|
1617
2193
|
}
|
|
1618
|
-
const fixSpinner = options.output !== "json" ? (0,
|
|
1619
|
-
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, {
|
|
1620
2196
|
cwd,
|
|
1621
2197
|
url,
|
|
1622
2198
|
framework
|
|
@@ -1627,13 +2203,13 @@ async function fix(options) {
|
|
|
1627
2203
|
if (options.dryRun) {
|
|
1628
2204
|
if (options.output !== "json") {
|
|
1629
2205
|
console.log("\n" + "\u2500".repeat(50));
|
|
1630
|
-
console.log(
|
|
2206
|
+
console.log(import_chalk5.default.yellow("DRY RUN - No changes will be made"));
|
|
1631
2207
|
console.log("\u2500".repeat(50));
|
|
1632
2208
|
for (let i = 0; i < fixes.length; i++) {
|
|
1633
2209
|
printFix2(fixes[i], i);
|
|
1634
2210
|
}
|
|
1635
2211
|
console.log("\n" + "\u2500".repeat(50));
|
|
1636
|
-
console.log(`\u{1F4CB} ${
|
|
2212
|
+
console.log(`\u{1F4CB} ${import_chalk5.default.bold("Summary:")} ${actionableFixes.length} fix(es) would be applied`);
|
|
1637
2213
|
console.log(` Remove --dry-run to apply these fixes.
|
|
1638
2214
|
`);
|
|
1639
2215
|
}
|
|
@@ -1699,8 +2275,8 @@ async function fix(options) {
|
|
|
1699
2275
|
durationMs: Date.now() - startTime
|
|
1700
2276
|
};
|
|
1701
2277
|
}
|
|
1702
|
-
const applySpinner = options.output !== "json" ? (0,
|
|
1703
|
-
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 });
|
|
1704
2280
|
for (const fix2 of applied) {
|
|
1705
2281
|
fix2.applied = true;
|
|
1706
2282
|
}
|
|
@@ -1711,7 +2287,7 @@ async function fix(options) {
|
|
|
1711
2287
|
let commitError;
|
|
1712
2288
|
let pushError;
|
|
1713
2289
|
if (options.commit && applied.length > 0 && commitMessage) {
|
|
1714
|
-
const gitSpinner = options.output !== "json" ? (0,
|
|
2290
|
+
const gitSpinner = options.output !== "json" ? (0, import_ora3.default)("Committing changes...").start() : null;
|
|
1715
2291
|
const gitResult = await executeGitCommit(cwd, commitMessage, options.push || false);
|
|
1716
2292
|
committed = gitResult.committed;
|
|
1717
2293
|
pushed = gitResult.pushed;
|
|
@@ -1737,23 +2313,23 @@ async function fix(options) {
|
|
|
1737
2313
|
console.log("\u2550".repeat(50));
|
|
1738
2314
|
if (applied.length > 0) {
|
|
1739
2315
|
console.log(`
|
|
1740
|
-
${
|
|
2316
|
+
${import_chalk5.default.green("\u2705")} Applied ${applied.length} fix(es):`);
|
|
1741
2317
|
for (const fix2 of applied) {
|
|
1742
2318
|
console.log(` - ${fix2.file}: ${fix2.issue.message}`);
|
|
1743
2319
|
}
|
|
1744
2320
|
}
|
|
1745
2321
|
if (skipped.length > 0) {
|
|
1746
2322
|
console.log(`
|
|
1747
|
-
${
|
|
2323
|
+
${import_chalk5.default.yellow("\u26A0\uFE0F")} Skipped ${skipped.length} fix(es):`);
|
|
1748
2324
|
for (const fix2 of skipped) {
|
|
1749
2325
|
console.log(` - ${fix2.file}: ${fix2.skipReason || "Manual fix required"}`);
|
|
1750
2326
|
}
|
|
1751
2327
|
}
|
|
1752
2328
|
if (committed) {
|
|
1753
2329
|
console.log(`
|
|
1754
|
-
${
|
|
2330
|
+
${import_chalk5.default.green("\u2705")} Changes committed`);
|
|
1755
2331
|
if (pushed) {
|
|
1756
|
-
console.log(`${
|
|
2332
|
+
console.log(`${import_chalk5.default.green("\u2705")} Changes pushed`);
|
|
1757
2333
|
}
|
|
1758
2334
|
}
|
|
1759
2335
|
if (!committed && applied.length > 0) {
|
|
@@ -1806,10 +2382,10 @@ ${import_chalk3.default.green("\u2705")} Changes committed`);
|
|
|
1806
2382
|
}
|
|
1807
2383
|
|
|
1808
2384
|
// src/commands/track.ts
|
|
1809
|
-
var
|
|
1810
|
-
var
|
|
2385
|
+
var import_ora4 = __toESM(require("ora"));
|
|
2386
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
1811
2387
|
var import_table = require("table");
|
|
1812
|
-
var
|
|
2388
|
+
var import_agent_runtime5 = require("@rankcli/agent-runtime");
|
|
1813
2389
|
async function getProjectForUrl(url) {
|
|
1814
2390
|
const supabase = await getAuthenticatedClient();
|
|
1815
2391
|
if (!supabase) return null;
|
|
@@ -1839,42 +2415,42 @@ async function getProjectForUrl(url) {
|
|
|
1839
2415
|
};
|
|
1840
2416
|
}
|
|
1841
2417
|
function getTierLimits(tier) {
|
|
1842
|
-
const limits =
|
|
2418
|
+
const limits = import_agent_runtime5.TIER_LIMITS[tier] || import_agent_runtime5.TIER_LIMITS.free;
|
|
1843
2419
|
return {
|
|
1844
2420
|
maxKeywords: limits.maxKeywords,
|
|
1845
2421
|
checksPerDay: limits.checksPerDay
|
|
1846
2422
|
};
|
|
1847
2423
|
}
|
|
1848
2424
|
function formatPosition(pos) {
|
|
1849
|
-
if (pos === null) return
|
|
1850
|
-
if (pos <= 3) return
|
|
1851
|
-
if (pos <= 10) return
|
|
1852
|
-
if (pos <= 20) return
|
|
1853
|
-
if (pos <= 50) return
|
|
1854
|
-
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}`);
|
|
1855
2431
|
}
|
|
1856
2432
|
function formatChange(change) {
|
|
1857
|
-
if (change === 0) return
|
|
1858
|
-
if (change > 0) return
|
|
1859
|
-
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)}`);
|
|
1860
2436
|
}
|
|
1861
2437
|
async function trackAdd(options) {
|
|
1862
2438
|
if (!options.keywords || options.keywords.length === 0) {
|
|
1863
|
-
console.log(
|
|
2439
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide keywords to track:"));
|
|
1864
2440
|
console.log(' rankcli track add "seo tool" "cli seo" --url https://your-site.com\n');
|
|
1865
2441
|
return { success: false, error: "No keywords provided" };
|
|
1866
2442
|
}
|
|
1867
2443
|
if (!options.url) {
|
|
1868
|
-
console.log(
|
|
2444
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL to track:"));
|
|
1869
2445
|
console.log(' rankcli track add "keyword" --url https://your-site.com\n');
|
|
1870
2446
|
return { success: false, error: "No URL provided" };
|
|
1871
2447
|
}
|
|
1872
2448
|
if (!isLoggedIn()) {
|
|
1873
|
-
console.log(
|
|
2449
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to track keywords:"));
|
|
1874
2450
|
console.log(" rankcli login\n");
|
|
1875
2451
|
return { success: false, error: "Not logged in" };
|
|
1876
2452
|
}
|
|
1877
|
-
const spinner = (0,
|
|
2453
|
+
const spinner = (0, import_ora4.default)("Adding keywords to track...").start();
|
|
1878
2454
|
try {
|
|
1879
2455
|
const project = await getProjectForUrl(options.url);
|
|
1880
2456
|
if (!project) {
|
|
@@ -1894,7 +2470,7 @@ async function trackAdd(options) {
|
|
|
1894
2470
|
const newCount = options.keywords.length;
|
|
1895
2471
|
if (existingCount + newCount > maxKeywords) {
|
|
1896
2472
|
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1897
|
-
showUpsell("keyword_limit", { tier, current: existingCount,
|
|
2473
|
+
showUpsell("keyword_limit", { tier, current: existingCount, max: maxKeywords });
|
|
1898
2474
|
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1899
2475
|
}
|
|
1900
2476
|
const keywordRecords = options.keywords.map((keyword) => ({
|
|
@@ -1915,10 +2491,10 @@ async function trackAdd(options) {
|
|
|
1915
2491
|
}
|
|
1916
2492
|
const addedCount = data?.length || 0;
|
|
1917
2493
|
spinner.succeed(`Added ${addedCount} keyword(s) to track`);
|
|
1918
|
-
console.log(
|
|
2494
|
+
console.log(import_chalk6.default.dim(`
|
|
1919
2495
|
Tracking: ${options.keywords.join(", ")}`));
|
|
1920
|
-
console.log(
|
|
1921
|
-
console.log(
|
|
2496
|
+
console.log(import_chalk6.default.dim(`For: ${project.domain}`));
|
|
2497
|
+
console.log(import_chalk6.default.cyan(`
|
|
1922
2498
|
Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
1923
2499
|
`));
|
|
1924
2500
|
return { success: true, addedCount };
|
|
@@ -1929,16 +2505,16 @@ Run 'rankcli track check --url ${options.url}' to check current positions.
|
|
|
1929
2505
|
}
|
|
1930
2506
|
async function trackStatus(options) {
|
|
1931
2507
|
if (!options.url) {
|
|
1932
|
-
console.log(
|
|
2508
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
1933
2509
|
console.log(" rankcli track status --url https://your-site.com\n");
|
|
1934
2510
|
return { success: false, error: "No URL provided" };
|
|
1935
2511
|
}
|
|
1936
2512
|
if (!isLoggedIn()) {
|
|
1937
|
-
console.log(
|
|
2513
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to view tracking:"));
|
|
1938
2514
|
console.log(" rankcli login\n");
|
|
1939
2515
|
return { success: false, error: "Not logged in" };
|
|
1940
2516
|
}
|
|
1941
|
-
const spinner = (0,
|
|
2517
|
+
const spinner = (0, import_ora4.default)("Loading keyword rankings...").start();
|
|
1942
2518
|
try {
|
|
1943
2519
|
const project = await getProjectForUrl(options.url);
|
|
1944
2520
|
if (!project) {
|
|
@@ -1957,8 +2533,8 @@ async function trackStatus(options) {
|
|
|
1957
2533
|
}
|
|
1958
2534
|
spinner.succeed(`Loaded ${data?.length || 0} tracked keywords`);
|
|
1959
2535
|
if (!data || data.length === 0) {
|
|
1960
|
-
console.log(
|
|
1961
|
-
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.
|
|
1962
2538
|
`));
|
|
1963
2539
|
return { success: true, keywords: [] };
|
|
1964
2540
|
}
|
|
@@ -1973,7 +2549,7 @@ async function trackStatus(options) {
|
|
|
1973
2549
|
if (options.output === "json") {
|
|
1974
2550
|
console.log(JSON.stringify({ keywords: keywords2 }, null, 2));
|
|
1975
2551
|
} else {
|
|
1976
|
-
console.log("\n" +
|
|
2552
|
+
console.log("\n" + import_chalk6.default.bold("\u{1F4CA} Keyword Rankings"));
|
|
1977
2553
|
console.log("\u2550".repeat(60));
|
|
1978
2554
|
const tableData = [
|
|
1979
2555
|
["Keyword", "Position", "Best", "Change", "Last Checked"],
|
|
@@ -2016,14 +2592,14 @@ async function trackHistory(options) {
|
|
|
2016
2592
|
return { success: false, error: "No URL provided" };
|
|
2017
2593
|
}
|
|
2018
2594
|
if (!options.keyword) {
|
|
2019
|
-
console.log(
|
|
2595
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a keyword:"));
|
|
2020
2596
|
console.log(' rankcli track history --keyword "seo tool" --url https://your-site.com\n');
|
|
2021
2597
|
return { success: false, error: "No keyword provided" };
|
|
2022
2598
|
}
|
|
2023
2599
|
if (!isLoggedIn()) {
|
|
2024
2600
|
return { success: false, error: "Not logged in" };
|
|
2025
2601
|
}
|
|
2026
|
-
const spinner = (0,
|
|
2602
|
+
const spinner = (0, import_ora4.default)(`Loading history for "${options.keyword}"...`).start();
|
|
2027
2603
|
try {
|
|
2028
2604
|
const project = await getProjectForUrl(options.url);
|
|
2029
2605
|
if (!project) {
|
|
@@ -2056,21 +2632,21 @@ async function trackHistory(options) {
|
|
|
2056
2632
|
if (options.output === "json") {
|
|
2057
2633
|
console.log(JSON.stringify({ keyword: options.keyword, history }, null, 2));
|
|
2058
2634
|
} else {
|
|
2059
|
-
console.log("\n" +
|
|
2635
|
+
console.log("\n" + import_chalk6.default.bold(`\u{1F4C8} History: "${options.keyword}"`));
|
|
2060
2636
|
console.log("\u2550".repeat(50));
|
|
2061
2637
|
if (history.length === 0) {
|
|
2062
|
-
console.log(
|
|
2638
|
+
console.log(import_chalk6.default.dim("\nNo history available yet.\n"));
|
|
2063
2639
|
} else {
|
|
2064
2640
|
const positions = history.slice(0, 30).reverse().map((h) => h.position || 101);
|
|
2065
2641
|
const sparkline = positions.map((p) => {
|
|
2066
|
-
if (p <= 10) return
|
|
2067
|
-
if (p <= 20) return
|
|
2068
|
-
if (p <= 50) return
|
|
2069
|
-
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");
|
|
2070
2646
|
}).join("");
|
|
2071
2647
|
console.log(`
|
|
2072
2648
|
${sparkline} (last ${positions.length} checks)`);
|
|
2073
|
-
console.log(
|
|
2649
|
+
console.log(import_chalk6.default.dim(" \u2190 newer older \u2192\n"));
|
|
2074
2650
|
const recent = history.slice(0, 10);
|
|
2075
2651
|
for (const entry of recent) {
|
|
2076
2652
|
console.log(` ${entry.recordedAt.toLocaleDateString()} ${formatPosition(entry.position)}`);
|
|
@@ -2090,7 +2666,7 @@ async function trackExport(options) {
|
|
|
2090
2666
|
if (!isLoggedIn()) {
|
|
2091
2667
|
return { success: false, error: "Not logged in" };
|
|
2092
2668
|
}
|
|
2093
|
-
const spinner = (0,
|
|
2669
|
+
const spinner = (0, import_ora4.default)("Exporting data...").start();
|
|
2094
2670
|
try {
|
|
2095
2671
|
const project = await getProjectForUrl(options.url);
|
|
2096
2672
|
if (!project) {
|
|
@@ -2147,12 +2723,12 @@ async function trackExport(options) {
|
|
|
2147
2723
|
}
|
|
2148
2724
|
async function trackCheck(options) {
|
|
2149
2725
|
if (!options.url) {
|
|
2150
|
-
console.log(
|
|
2726
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please provide a URL:"));
|
|
2151
2727
|
console.log(" rankcli track check --url https://your-site.com\n");
|
|
2152
2728
|
return { success: false, error: "No URL provided" };
|
|
2153
2729
|
}
|
|
2154
2730
|
if (!isLoggedIn()) {
|
|
2155
|
-
console.log(
|
|
2731
|
+
console.log(import_chalk6.default.yellow("\n\u26A0\uFE0F Please log in to check rankings:"));
|
|
2156
2732
|
console.log(" rankcli login\n");
|
|
2157
2733
|
return { success: false, error: "Not logged in" };
|
|
2158
2734
|
}
|
|
@@ -2169,14 +2745,14 @@ async function trackCheck(options) {
|
|
|
2169
2745
|
const minHoursBetweenChecks = 24 / checksPerDay;
|
|
2170
2746
|
if (hoursSinceCheck < minHoursBetweenChecks) {
|
|
2171
2747
|
const nextCheck = new Date(project.lastRankCheckAt.getTime() + minHoursBetweenChecks * 60 * 60 * 1e3);
|
|
2172
|
-
console.log(
|
|
2748
|
+
console.log(import_chalk6.default.yellow(`
|
|
2173
2749
|
\u26A0\uFE0F Rate limited. Next check available: ${nextCheck.toLocaleString()}`));
|
|
2174
|
-
console.log(
|
|
2750
|
+
console.log(import_chalk6.default.dim(` Your tier (${tier}) allows ${checksPerDay} check(s) per day.
|
|
2175
2751
|
`));
|
|
2176
2752
|
return { success: true, rateLimited: true };
|
|
2177
2753
|
}
|
|
2178
2754
|
}
|
|
2179
|
-
const spinner = (0,
|
|
2755
|
+
const spinner = (0, import_ora4.default)("Checking current rankings...").start();
|
|
2180
2756
|
const supabase = await getAuthenticatedClient();
|
|
2181
2757
|
if (!supabase) {
|
|
2182
2758
|
spinner.fail("Could not connect to database");
|
|
@@ -2190,11 +2766,11 @@ async function trackCheck(options) {
|
|
|
2190
2766
|
const serpApiKey = process.env.VALUESERP_API_KEY || process.env.SERPAPI_KEY;
|
|
2191
2767
|
if (!serpApiKey) {
|
|
2192
2768
|
spinner.warn("No SERP API key configured");
|
|
2193
|
-
console.log(
|
|
2194
|
-
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"));
|
|
2195
2771
|
return { success: false, error: "No SERP API key" };
|
|
2196
2772
|
}
|
|
2197
|
-
const tracker = new
|
|
2773
|
+
const tracker = new import_agent_runtime5.RankTracker({
|
|
2198
2774
|
supabase,
|
|
2199
2775
|
serpConfig: {
|
|
2200
2776
|
provider: process.env.VALUESERP_API_KEY ? "valueserp" : "serpapi",
|
|
@@ -2203,13 +2779,13 @@ async function trackCheck(options) {
|
|
|
2203
2779
|
});
|
|
2204
2780
|
const results = await tracker.checkRankings(project.id, project.domain);
|
|
2205
2781
|
spinner.succeed(`Checked ${results.length} keyword(s)`);
|
|
2206
|
-
console.log("\n" +
|
|
2782
|
+
console.log("\n" + import_chalk6.default.bold("\u{1F50D} Rank Check Results"));
|
|
2207
2783
|
console.log("\u2550".repeat(50));
|
|
2208
2784
|
for (const result of results) {
|
|
2209
2785
|
const posStr = formatPosition(result.position);
|
|
2210
2786
|
console.log(` ${result.keyword}: ${posStr}`);
|
|
2211
2787
|
if (result.serpFeatures.length > 0) {
|
|
2212
|
-
console.log(
|
|
2788
|
+
console.log(import_chalk6.default.dim(` SERP features: ${result.serpFeatures.map((f) => f.type).join(", ")}`));
|
|
2213
2789
|
}
|
|
2214
2790
|
}
|
|
2215
2791
|
console.log("");
|
|
@@ -2227,9 +2803,9 @@ async function trackCheck(options) {
|
|
|
2227
2803
|
}
|
|
2228
2804
|
|
|
2229
2805
|
// src/commands/geo.ts
|
|
2230
|
-
var
|
|
2231
|
-
var
|
|
2232
|
-
var
|
|
2806
|
+
var import_agent_runtime6 = require("@rankcli/agent-runtime");
|
|
2807
|
+
var import_ora5 = __toESM(require("ora"));
|
|
2808
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
2233
2809
|
async function fetchResources(url) {
|
|
2234
2810
|
const headers = {};
|
|
2235
2811
|
let html = null;
|
|
@@ -2275,7 +2851,7 @@ async function geo(options) {
|
|
|
2275
2851
|
console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
|
|
2276
2852
|
return;
|
|
2277
2853
|
}
|
|
2278
|
-
const spinner = (0,
|
|
2854
|
+
const spinner = (0, import_ora5.default)("Fetching page and robots.txt...").start();
|
|
2279
2855
|
const { html, robotsTxt, headers } = await fetchResources(url);
|
|
2280
2856
|
if (!html) {
|
|
2281
2857
|
spinner.fail("Could not fetch page content");
|
|
@@ -2283,11 +2859,11 @@ async function geo(options) {
|
|
|
2283
2859
|
}
|
|
2284
2860
|
spinner.text = "Running GEO analysis...";
|
|
2285
2861
|
try {
|
|
2286
|
-
const geoResult = await
|
|
2287
|
-
const cwvResult =
|
|
2288
|
-
const securityResult =
|
|
2289
|
-
const schemaResult =
|
|
2290
|
-
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);
|
|
2291
2867
|
spinner.succeed("GEO analysis complete");
|
|
2292
2868
|
if (options.output === "json") {
|
|
2293
2869
|
const jsonOutput = {
|
|
@@ -2301,55 +2877,55 @@ async function geo(options) {
|
|
|
2301
2877
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2302
2878
|
return;
|
|
2303
2879
|
}
|
|
2304
|
-
console.log("\n" +
|
|
2305
|
-
console.log(
|
|
2306
|
-
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)));
|
|
2307
2883
|
const geoScore = geoResult.score;
|
|
2308
|
-
const scoreColor = geoScore >= 80 ?
|
|
2884
|
+
const scoreColor = geoScore >= 80 ? import_chalk7.default.green : geoScore >= 60 ? import_chalk7.default.yellow : import_chalk7.default.red;
|
|
2309
2885
|
console.log(`
|
|
2310
|
-
${
|
|
2311
|
-
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:"));
|
|
2312
2888
|
const { aiCrawlerAccess } = geoResult;
|
|
2313
2889
|
if (aiCrawlerAccess.blockedCrawlers.length > 0) {
|
|
2314
|
-
console.log(
|
|
2890
|
+
console.log(import_chalk7.default.red(` \u274C Blocked: ${aiCrawlerAccess.blockedCrawlers.join(", ")}`));
|
|
2315
2891
|
}
|
|
2316
2892
|
if (aiCrawlerAccess.allowedCrawlers.length > 0) {
|
|
2317
2893
|
const topAllowed = aiCrawlerAccess.allowedCrawlers.slice(0, 5);
|
|
2318
|
-
console.log(
|
|
2894
|
+
console.log(import_chalk7.default.green(` \u2705 Allowed: ${topAllowed.join(", ")}${aiCrawlerAccess.allowedCrawlers.length > 5 ? "..." : ""}`));
|
|
2319
2895
|
}
|
|
2320
|
-
console.log("\n" +
|
|
2896
|
+
console.log("\n" + import_chalk7.default.bold(" Rendering:"));
|
|
2321
2897
|
if (aiCrawlerAccess.serverSideRendered) {
|
|
2322
|
-
console.log(
|
|
2898
|
+
console.log(import_chalk7.default.green(" \u2705 Server-side rendered (AI crawlers can see content)"));
|
|
2323
2899
|
} else if (aiCrawlerAccess.jsRenderingRequired) {
|
|
2324
|
-
console.log(
|
|
2900
|
+
console.log(import_chalk7.default.red(" \u274C JavaScript required (AI crawlers may see blank page)"));
|
|
2325
2901
|
}
|
|
2326
|
-
console.log("\n" +
|
|
2902
|
+
console.log("\n" + import_chalk7.default.bold(" LLM Friendliness:"));
|
|
2327
2903
|
const { llmSignals } = geoResult;
|
|
2328
2904
|
const bar = (score) => {
|
|
2329
2905
|
const filled = Math.round(score / 5);
|
|
2330
2906
|
const empty = 20 - filled;
|
|
2331
|
-
const color = score >= 70 ?
|
|
2332
|
-
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));
|
|
2333
2909
|
};
|
|
2334
2910
|
console.log(` Content Clarity: ${bar(llmSignals.contentClarity)} ${llmSignals.contentClarity}%`);
|
|
2335
2911
|
console.log(` Fact Density: ${bar(llmSignals.factDensity)} ${llmSignals.factDensity}%`);
|
|
2336
2912
|
console.log(` Structure: ${bar(llmSignals.structureQuality)} ${llmSignals.structureQuality}%`);
|
|
2337
2913
|
console.log(` Citations: ${bar(llmSignals.citationQuality)} ${llmSignals.citationQuality}%`);
|
|
2338
|
-
console.log("\n" +
|
|
2914
|
+
console.log("\n" + import_chalk7.default.bold(" Content Structure:"));
|
|
2339
2915
|
const { contentStructure } = geoResult;
|
|
2340
|
-
console.log(` ${contentStructure.hasStructuredData ?
|
|
2341
|
-
console.log(` ${contentStructure.hasFAQSchema ?
|
|
2342
|
-
console.log(` ${contentStructure.hasArticleSchema ?
|
|
2343
|
-
console.log(` ${contentStructure.hasBreadcrumbs ?
|
|
2344
|
-
console.log(` Heading Hierarchy: ${contentStructure.headingHierarchy === "good" ?
|
|
2345
|
-
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:"));
|
|
2346
2922
|
const { citationReadiness } = geoResult;
|
|
2347
2923
|
if (citationReadiness.trustSignals.length > 0) {
|
|
2348
|
-
console.log(
|
|
2924
|
+
console.log(import_chalk7.default.green(` \u2705 ${citationReadiness.trustSignals.join(", ")}`));
|
|
2349
2925
|
} else {
|
|
2350
|
-
console.log(
|
|
2926
|
+
console.log(import_chalk7.default.yellow(" \u25CB No trust signals detected"));
|
|
2351
2927
|
}
|
|
2352
|
-
console.log("\n" +
|
|
2928
|
+
console.log("\n" + import_chalk7.default.bold(" Other Scores:"));
|
|
2353
2929
|
console.log(` Core Web Vitals: ${bar(cwvResult.overallScore)} ${cwvResult.overallScore}%`);
|
|
2354
2930
|
console.log(` Security Headers: ${securityResult.grade} (${securityResult.score}%)`);
|
|
2355
2931
|
console.log(` Structured Data: ${bar(schemaResult.score)} ${schemaResult.score}%`);
|
|
@@ -2363,18 +2939,18 @@ async function geo(options) {
|
|
|
2363
2939
|
];
|
|
2364
2940
|
const criticalIssues = allIssues.filter((i) => i.severity === "critical" || i.severity === "error");
|
|
2365
2941
|
if (criticalIssues.length > 0) {
|
|
2366
|
-
console.log("\n" +
|
|
2942
|
+
console.log("\n" + import_chalk7.default.bold.red(" Critical Issues:"));
|
|
2367
2943
|
criticalIssues.slice(0, 5).forEach((issue, i) => {
|
|
2368
|
-
console.log(
|
|
2944
|
+
console.log(import_chalk7.default.red(` ${i + 1}. ${issue.title}`));
|
|
2369
2945
|
});
|
|
2370
2946
|
}
|
|
2371
2947
|
if (geoResult.recommendations.length > 0) {
|
|
2372
|
-
console.log("\n" +
|
|
2948
|
+
console.log("\n" + import_chalk7.default.bold(" Recommendations:"));
|
|
2373
2949
|
geoResult.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
2374
2950
|
console.log(` ${i + 1}. ${rec}`);
|
|
2375
2951
|
});
|
|
2376
2952
|
}
|
|
2377
|
-
console.log("\n" +
|
|
2953
|
+
console.log("\n" + import_chalk7.default.bold("\u2550".repeat(60)));
|
|
2378
2954
|
console.log("");
|
|
2379
2955
|
} catch (error) {
|
|
2380
2956
|
spinner.fail(`Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -2382,13 +2958,13 @@ async function geo(options) {
|
|
|
2382
2958
|
}
|
|
2383
2959
|
|
|
2384
2960
|
// src/commands/keywords.ts
|
|
2385
|
-
var
|
|
2386
|
-
var
|
|
2961
|
+
var readline2 = __toESM(require("readline"));
|
|
2962
|
+
var import_agent_runtime7 = require("@rankcli/agent-runtime");
|
|
2387
2963
|
async function keywords(options) {
|
|
2388
|
-
console.log("\n\u{1F511}
|
|
2964
|
+
console.log("\n\u{1F511} RankCLI - Keyword Research\n");
|
|
2389
2965
|
console.log("This tool helps you find keywords you can actually rank for,");
|
|
2390
2966
|
console.log("based on your site's current authority and competition level.\n");
|
|
2391
|
-
const rl =
|
|
2967
|
+
const rl = readline2.createInterface({
|
|
2392
2968
|
input: process.stdin,
|
|
2393
2969
|
output: process.stdout
|
|
2394
2970
|
});
|
|
@@ -2434,7 +3010,7 @@ ${question}`);
|
|
|
2434
3010
|
seedKeywords = options.seed.split(",").map((s) => s.trim());
|
|
2435
3011
|
} else if (options.auto) {
|
|
2436
3012
|
console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
|
|
2437
|
-
seedKeywords = await (0,
|
|
3013
|
+
seedKeywords = await (0, import_agent_runtime7.extractSeedKeywords)(url);
|
|
2438
3014
|
if (seedKeywords.length > 0) {
|
|
2439
3015
|
console.log(`Found: ${seedKeywords.join(", ")}`);
|
|
2440
3016
|
}
|
|
@@ -2459,7 +3035,7 @@ ${question}`);
|
|
|
2459
3035
|
contentCapacity: "medium",
|
|
2460
3036
|
targetGeo: "us"
|
|
2461
3037
|
};
|
|
2462
|
-
for (const q of
|
|
3038
|
+
for (const q of import_agent_runtime7.SITE_PROFILE_QUESTIONS) {
|
|
2463
3039
|
const answer = await askChoice(q.question, q.options);
|
|
2464
3040
|
siteProfile[q.id] = answer;
|
|
2465
3041
|
}
|
|
@@ -2478,13 +3054,13 @@ ${question}`);
|
|
|
2478
3054
|
url,
|
|
2479
3055
|
maxKeywords: 100
|
|
2480
3056
|
};
|
|
2481
|
-
const result = await (0,
|
|
2482
|
-
console.log((0,
|
|
3057
|
+
const result = await (0, import_agent_runtime7.runKeywordResearch)(researchOptions);
|
|
3058
|
+
console.log((0, import_agent_runtime7.formatKeywordReport)(result));
|
|
2483
3059
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
2484
3060
|
if (allKeywords.length > 5) {
|
|
2485
3061
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
2486
|
-
const topicResult = (0,
|
|
2487
|
-
console.log((0,
|
|
3062
|
+
const topicResult = (0, import_agent_runtime7.groupKeywordsByTopic)(allKeywords);
|
|
3063
|
+
console.log((0, import_agent_runtime7.formatTopicReport)(topicResult));
|
|
2488
3064
|
}
|
|
2489
3065
|
if (result.quickWins.length > 0) {
|
|
2490
3066
|
console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
|
|
@@ -2509,7 +3085,7 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
2509
3085
|
`);
|
|
2510
3086
|
} else {
|
|
2511
3087
|
console.log("\u{1F50D} Extracting keywords from your page...");
|
|
2512
|
-
seedKeywords = await (0,
|
|
3088
|
+
seedKeywords = await (0, import_agent_runtime7.extractSeedKeywords)(url);
|
|
2513
3089
|
if (seedKeywords.length === 0) {
|
|
2514
3090
|
console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
|
|
2515
3091
|
return;
|
|
@@ -2525,18 +3101,18 @@ async function keywordsQuick(url, providedSeeds) {
|
|
|
2525
3101
|
contentCapacity: "medium",
|
|
2526
3102
|
targetGeo: "us"
|
|
2527
3103
|
};
|
|
2528
|
-
const result = await (0,
|
|
3104
|
+
const result = await (0, import_agent_runtime7.runKeywordResearch)({
|
|
2529
3105
|
seedKeywords,
|
|
2530
3106
|
siteProfile,
|
|
2531
3107
|
url,
|
|
2532
3108
|
maxKeywords: 50
|
|
2533
3109
|
});
|
|
2534
|
-
console.log((0,
|
|
3110
|
+
console.log((0, import_agent_runtime7.formatKeywordReport)(result));
|
|
2535
3111
|
const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
|
|
2536
3112
|
if (allKeywords.length > 5) {
|
|
2537
3113
|
console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
|
|
2538
|
-
const topicResult = (0,
|
|
2539
|
-
console.log((0,
|
|
3114
|
+
const topicResult = (0, import_agent_runtime7.groupKeywordsByTopic)(allKeywords);
|
|
3115
|
+
console.log((0, import_agent_runtime7.formatTopicReport)(topicResult));
|
|
2540
3116
|
}
|
|
2541
3117
|
} catch (error) {
|
|
2542
3118
|
console.log(`
|
|
@@ -2599,7 +3175,7 @@ ${result2.teaser.upgradeMessage}`);
|
|
|
2599
3175
|
return;
|
|
2600
3176
|
}
|
|
2601
3177
|
console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
|
|
2602
|
-
const result = await (0,
|
|
3178
|
+
const result = await (0, import_agent_runtime7.runAIKeywordResearch)({
|
|
2603
3179
|
url,
|
|
2604
3180
|
tier: "solo",
|
|
2605
3181
|
openaiApiKey,
|
|
@@ -2718,12 +3294,12 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
2718
3294
|
}
|
|
2719
3295
|
console.log("");
|
|
2720
3296
|
console.log("Analyzing competitor keywords (this may take a moment)...\n");
|
|
2721
|
-
const result = await (0,
|
|
3297
|
+
const result = await (0, import_agent_runtime7.discoverCompetitorKeywords)(
|
|
2722
3298
|
yourDomain,
|
|
2723
3299
|
seedKeywords,
|
|
2724
3300
|
competitors
|
|
2725
3301
|
);
|
|
2726
|
-
console.log((0,
|
|
3302
|
+
console.log((0, import_agent_runtime7.formatCompetitorReport)(result));
|
|
2727
3303
|
if (result.missingKeywords.length > 0) {
|
|
2728
3304
|
console.log("\n\u{1F3AF} ACTION ITEMS:");
|
|
2729
3305
|
console.log("\u2500".repeat(60));
|
|
@@ -2742,9 +3318,9 @@ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
|
|
|
2742
3318
|
}
|
|
2743
3319
|
|
|
2744
3320
|
// src/commands/content.ts
|
|
2745
|
-
var
|
|
3321
|
+
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
2746
3322
|
async function analyzeContent(options) {
|
|
2747
|
-
console.log("\n\u{1F4DD}
|
|
3323
|
+
console.log("\n\u{1F4DD} RankCLI - Content Analysis\n");
|
|
2748
3324
|
const mode = options.mode || "full";
|
|
2749
3325
|
if (mode === "headline" || options.headline) {
|
|
2750
3326
|
const headline = options.headline || "";
|
|
@@ -2752,12 +3328,12 @@ async function analyzeContent(options) {
|
|
|
2752
3328
|
console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
|
|
2753
3329
|
return;
|
|
2754
3330
|
}
|
|
2755
|
-
const analysis = (0,
|
|
2756
|
-
console.log((0,
|
|
3331
|
+
const analysis = (0, import_agent_runtime8.analyzeHeadline)(headline);
|
|
3332
|
+
console.log((0, import_agent_runtime8.formatHeadlineReport)(analysis));
|
|
2757
3333
|
if (options.keyword) {
|
|
2758
3334
|
console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
|
|
2759
3335
|
console.log("\u2500".repeat(60));
|
|
2760
|
-
const variations = (0,
|
|
3336
|
+
const variations = (0, import_agent_runtime8.generateHeadlineVariations)(options.keyword, [options.keyword]);
|
|
2761
3337
|
for (const v of variations.slice(0, 5)) {
|
|
2762
3338
|
console.log(` \u2022 ${v}`);
|
|
2763
3339
|
}
|
|
@@ -2768,29 +3344,36 @@ async function analyzeContent(options) {
|
|
|
2768
3344
|
if (options.url) {
|
|
2769
3345
|
console.log(`Fetching content from ${options.url}...`);
|
|
2770
3346
|
try {
|
|
2771
|
-
const
|
|
2772
|
-
if (!
|
|
3347
|
+
const crawlResult = await (0, import_agent_runtime8.crawlUrl)({ url: options.url });
|
|
3348
|
+
if (!crawlResult.success || !crawlResult.data) {
|
|
2773
3349
|
console.log("\u274C Could not fetch content from URL");
|
|
2774
3350
|
return;
|
|
2775
3351
|
}
|
|
2776
|
-
const
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
3352
|
+
const { html } = crawlResult.data;
|
|
3353
|
+
const [metaResult, headingsResult] = await Promise.all([
|
|
3354
|
+
(0, import_agent_runtime8.extractMeta)({ html, url: options.url }),
|
|
3355
|
+
(0, import_agent_runtime8.analyzeHeadings)({ html })
|
|
3356
|
+
]);
|
|
3357
|
+
const meta = metaResult.success ? metaResult.data : void 0;
|
|
3358
|
+
const headingStructure = headingsResult.success ? headingsResult.data : [];
|
|
3359
|
+
const title = meta?.title || "";
|
|
3360
|
+
const h1 = headingStructure.find((h) => h.level === 1)?.text || "";
|
|
3361
|
+
const headings = headingStructure.map((h) => h.text);
|
|
3362
|
+
const bodyText = extractBodyText(html);
|
|
2780
3363
|
const paragraphs = bodyText.split(/\n\n+/);
|
|
2781
3364
|
console.log(`
|
|
2782
3365
|
Analyzing: "${title || options.url}"
|
|
2783
3366
|
`);
|
|
2784
3367
|
if (mode === "full" || mode === "readability") {
|
|
2785
|
-
const readability = (0,
|
|
2786
|
-
console.log((0,
|
|
3368
|
+
const readability = (0, import_agent_runtime8.analyzeReadability)(bodyText);
|
|
3369
|
+
console.log((0, import_agent_runtime8.formatReadabilityReport)(readability));
|
|
2787
3370
|
}
|
|
2788
3371
|
if (mode === "full" && title) {
|
|
2789
|
-
const headlineAnalysis = (0,
|
|
2790
|
-
console.log((0,
|
|
3372
|
+
const headlineAnalysis = (0, import_agent_runtime8.analyzeHeadline)(title);
|
|
3373
|
+
console.log((0, import_agent_runtime8.formatHeadlineReport)(headlineAnalysis));
|
|
2791
3374
|
}
|
|
2792
3375
|
if (options.keyword && (mode === "full" || mode === "density")) {
|
|
2793
|
-
const densityAnalysis = (0,
|
|
3376
|
+
const densityAnalysis = (0, import_agent_runtime8.analyzeKeywordDensity)(options.keyword, {
|
|
2794
3377
|
title,
|
|
2795
3378
|
h1,
|
|
2796
3379
|
headings,
|
|
@@ -2798,229 +3381,59 @@ Analyzing: "${title || options.url}"
|
|
|
2798
3381
|
firstParagraph: paragraphs[0],
|
|
2799
3382
|
lastParagraph: paragraphs[paragraphs.length - 1]
|
|
2800
3383
|
});
|
|
2801
|
-
console.log((0,
|
|
3384
|
+
console.log((0, import_agent_runtime8.formatKeywordDensityReport)(densityAnalysis));
|
|
2802
3385
|
}
|
|
2803
3386
|
if (options.keyword && (mode === "full" || mode === "snippet")) {
|
|
2804
|
-
const snippetAnalysis = (0,
|
|
2805
|
-
options.keyword,
|
|
2806
|
-
bodyText,
|
|
2807
|
-
headings
|
|
2808
|
-
);
|
|
2809
|
-
console.log((0,
|
|
2810
|
-
}
|
|
2811
|
-
if (options.keyword) {
|
|
2812
|
-
const intentAnalysis = (0,
|
|
2813
|
-
console.log((0,
|
|
2814
|
-
}
|
|
2815
|
-
console.log("\n\u{1F4CA} CONTENT SUMMARY");
|
|
2816
|
-
console.log("\u2500".repeat(60));
|
|
2817
|
-
console.log(` Title: ${title || "(none)"}`);
|
|
2818
|
-
console.log(` H1: ${h1 || "(none)"}`);
|
|
2819
|
-
console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
|
|
2820
|
-
console.log(` Headings: ${headings.length}`);
|
|
2821
|
-
console.log("");
|
|
2822
|
-
} catch (error) {
|
|
2823
|
-
console.log(`
|
|
2824
|
-
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2825
|
-
`);
|
|
2826
|
-
}
|
|
2827
|
-
} else {
|
|
2828
|
-
console.log("Usage:");
|
|
2829
|
-
console.log(" seo content -u <url> Full content analysis");
|
|
2830
|
-
console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
|
|
2831
|
-
console.log(' seo content --headline "Your Title" Analyze headline only');
|
|
2832
|
-
console.log(" seo content -u <url> --mode readability Readability only");
|
|
2833
|
-
console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
|
|
2834
|
-
console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
|
|
2835
|
-
console.log("");
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
async function analyzeIntent(keyword) {
|
|
2839
|
-
console.log("\n\u{1F50D} Search Intent Analysis\n");
|
|
2840
|
-
const analysis = (0, import_agent_runtime7.classifyIntent)(keyword);
|
|
2841
|
-
console.log((0, import_agent_runtime7.formatIntentReport)(keyword, analysis));
|
|
2842
|
-
}
|
|
2843
|
-
function extractBodyText(html) {
|
|
2844
|
-
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
2845
|
-
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
2846
|
-
text = text.replace(/<[^>]+>/g, " ");
|
|
2847
|
-
text = text.replace(/ /g, " ");
|
|
2848
|
-
text = text.replace(/&/g, "&");
|
|
2849
|
-
text = text.replace(/</g, "<");
|
|
2850
|
-
text = text.replace(/>/g, ">");
|
|
2851
|
-
text = text.replace(/"/g, '"');
|
|
2852
|
-
text = text.replace(/\s+/g, " ").trim();
|
|
2853
|
-
return text;
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
// src/commands/setup.ts
|
|
2857
|
-
var readline2 = __toESM(require("readline"));
|
|
2858
|
-
var fs2 = __toESM(require("fs"));
|
|
2859
|
-
var path2 = __toESM(require("path"));
|
|
2860
|
-
var import_agent_runtime8 = require("@rankcli/agent-runtime");
|
|
2861
|
-
async function setup(options) {
|
|
2862
|
-
console.log("\n\u{1F527} SEO Autopilot - Setup\n");
|
|
2863
|
-
const projectPath = process.cwd();
|
|
2864
|
-
if (!fs2.existsSync(path2.join(projectPath, "package.json")) && !fs2.existsSync(path2.join(projectPath, "index.html"))) {
|
|
2865
|
-
console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
|
|
2866
|
-
}
|
|
2867
|
-
if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
|
|
2868
|
-
await interactiveSetup(projectPath);
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
|
-
if (options.ga4) {
|
|
2872
|
-
await setupGA4(projectPath, options.ga4);
|
|
2873
|
-
}
|
|
2874
|
-
if (options.gsc) {
|
|
2875
|
-
await setupGSC(projectPath, options.gsc);
|
|
2876
|
-
}
|
|
2877
|
-
if (options.githubAction) {
|
|
2878
|
-
await setupGitHubAction(projectPath, options.schedule || "weekly");
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
async function interactiveSetup(projectPath) {
|
|
2882
|
-
const rl = readline2.createInterface({
|
|
2883
|
-
input: process.stdin,
|
|
2884
|
-
output: process.stdout
|
|
2885
|
-
});
|
|
2886
|
-
const ask2 = (question) => {
|
|
2887
|
-
return new Promise((resolve) => {
|
|
2888
|
-
rl.question(question, (answer) => {
|
|
2889
|
-
resolve(answer.trim());
|
|
2890
|
-
});
|
|
2891
|
-
});
|
|
2892
|
-
};
|
|
2893
|
-
try {
|
|
2894
|
-
console.log("This wizard will help you set up SEO tracking for your project.\n");
|
|
2895
|
-
console.log("\u2500".repeat(60));
|
|
2896
|
-
console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
|
|
2897
|
-
console.log("GA4 tracks visitor behavior on your site.");
|
|
2898
|
-
console.log("Get your Measurement ID from: https://analytics.google.com");
|
|
2899
|
-
console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
|
|
2900
|
-
const ga4Id = await ask2("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
|
|
2901
|
-
if (ga4Id && ga4Id.startsWith("G-")) {
|
|
2902
|
-
await setupGA4(projectPath, ga4Id);
|
|
2903
|
-
} else if (ga4Id) {
|
|
2904
|
-
console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
|
|
2905
|
-
} else {
|
|
2906
|
-
console.log("Skipping GA4 setup.\n");
|
|
2907
|
-
}
|
|
2908
|
-
console.log("\u2500".repeat(60));
|
|
2909
|
-
console.log("\u{1F50D} STEP 2: Google Search Console\n");
|
|
2910
|
-
console.log("GSC shows how Google sees your site and what queries you rank for.");
|
|
2911
|
-
console.log("Get your verification code from: https://search.google.com/search-console");
|
|
2912
|
-
console.log("Go to: Settings > Ownership verification > HTML tag\n");
|
|
2913
|
-
const gscCode = await ask2("Enter GSC verification code or press Enter to skip: ");
|
|
2914
|
-
if (gscCode && gscCode.length > 10) {
|
|
2915
|
-
await setupGSC(projectPath, gscCode);
|
|
2916
|
-
} else if (gscCode) {
|
|
2917
|
-
console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
|
|
2918
|
-
} else {
|
|
2919
|
-
console.log("Skipping GSC verification.\n");
|
|
2920
|
-
}
|
|
2921
|
-
console.log("\u2500".repeat(60));
|
|
2922
|
-
console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
|
|
2923
|
-
console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
|
|
2924
|
-
console.log("It creates issues with reports and can auto-fix some problems.\n");
|
|
2925
|
-
const setupAction = await ask2("Set up GitHub Action? (y/n): ");
|
|
2926
|
-
if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
|
|
2927
|
-
console.log("\nHow often should the check run?");
|
|
2928
|
-
console.log(" 1. Weekly (recommended for most sites)");
|
|
2929
|
-
console.log(" 2. Daily (for high-traffic sites)");
|
|
2930
|
-
console.log(" 3. Monthly (for low-traffic sites)\n");
|
|
2931
|
-
const scheduleChoice = await ask2("Enter choice (1-3): ");
|
|
2932
|
-
const scheduleMap = {
|
|
2933
|
-
"1": "weekly",
|
|
2934
|
-
"2": "daily",
|
|
2935
|
-
"3": "monthly"
|
|
2936
|
-
};
|
|
2937
|
-
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2938
|
-
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2939
|
-
if (siteUrl) {
|
|
2940
|
-
await setupGitHubAction(projectPath, schedule, siteUrl);
|
|
2941
|
-
}
|
|
2942
|
-
} else {
|
|
2943
|
-
console.log("Skipping GitHub Action setup.\n");
|
|
2944
|
-
}
|
|
2945
|
-
console.log("\u2500".repeat(60));
|
|
2946
|
-
console.log("\u2705 Setup complete!\n");
|
|
2947
|
-
console.log("Next steps:");
|
|
2948
|
-
console.log(" 1. Commit the changes to your repository");
|
|
2949
|
-
console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
|
|
2950
|
-
console.log(" 3. Deploy your site to apply tracking code changes");
|
|
2951
|
-
console.log(" 4. Verify GSC ownership and GA4 data collection\n");
|
|
2952
|
-
rl.close();
|
|
2953
|
-
} catch (error) {
|
|
2954
|
-
console.log(`
|
|
3387
|
+
const snippetAnalysis = (0, import_agent_runtime8.analyzeFeaturedSnippetPotential)(
|
|
3388
|
+
options.keyword,
|
|
3389
|
+
bodyText,
|
|
3390
|
+
headings
|
|
3391
|
+
);
|
|
3392
|
+
console.log((0, import_agent_runtime8.formatFeaturedSnippetReport)(snippetAnalysis));
|
|
3393
|
+
}
|
|
3394
|
+
if (options.keyword) {
|
|
3395
|
+
const intentAnalysis = (0, import_agent_runtime8.classifyIntent)(options.keyword);
|
|
3396
|
+
console.log((0, import_agent_runtime8.formatIntentReport)(options.keyword, intentAnalysis));
|
|
3397
|
+
}
|
|
3398
|
+
console.log("\n\u{1F4CA} CONTENT SUMMARY");
|
|
3399
|
+
console.log("\u2500".repeat(60));
|
|
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(`
|
|
2955
3407
|
\u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2956
3408
|
`);
|
|
2957
|
-
rl.close();
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
async function setupGA4(projectPath, measurementId) {
|
|
2961
|
-
console.log(`Setting up Google Analytics 4 (${measurementId})...`);
|
|
2962
|
-
const result = await (0, import_agent_runtime8.injectGA4)(projectPath, { measurementId });
|
|
2963
|
-
if (result.success) {
|
|
2964
|
-
console.log(`\u2705 ${result.message}`);
|
|
2965
|
-
if (result.file) {
|
|
2966
|
-
console.log(` Modified: ${result.file}`);
|
|
2967
|
-
}
|
|
2968
|
-
} else {
|
|
2969
|
-
console.log(`\u26A0\uFE0F ${result.message}`);
|
|
2970
|
-
if (result.code) {
|
|
2971
|
-
console.log("\nManually add this code to your HTML <head>:\n");
|
|
2972
|
-
console.log(result.code);
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
const envExamplePath = path2.join(projectPath, ".env.example");
|
|
2976
|
-
const envContent = (0, import_agent_runtime8.generateGA4EnvTemplate)();
|
|
2977
|
-
if (fs2.existsSync(envExamplePath)) {
|
|
2978
|
-
const existing = fs2.readFileSync(envExamplePath, "utf-8");
|
|
2979
|
-
if (!existing.includes("GA4_MEASUREMENT_ID")) {
|
|
2980
|
-
fs2.appendFileSync(envExamplePath, "\n" + envContent);
|
|
2981
|
-
console.log(" Updated: .env.example");
|
|
2982
|
-
}
|
|
2983
|
-
} else {
|
|
2984
|
-
fs2.writeFileSync(envExamplePath, envContent);
|
|
2985
|
-
console.log(" Created: .env.example");
|
|
2986
|
-
}
|
|
2987
|
-
console.log("");
|
|
2988
|
-
}
|
|
2989
|
-
async function setupGSC(projectPath, verificationCode) {
|
|
2990
|
-
console.log("Setting up Google Search Console verification...");
|
|
2991
|
-
const result = await (0, import_agent_runtime8.injectGSCVerification)(projectPath, verificationCode);
|
|
2992
|
-
if (result.success) {
|
|
2993
|
-
console.log(`\u2705 ${result.message}`);
|
|
2994
|
-
if (result.file) {
|
|
2995
|
-
console.log(` Modified: ${result.file}`);
|
|
2996
3409
|
}
|
|
2997
3410
|
} else {
|
|
2998
|
-
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("");
|
|
2999
3419
|
}
|
|
3000
|
-
console.log("");
|
|
3001
3420
|
}
|
|
3002
|
-
async function
|
|
3003
|
-
console.log(
|
|
3004
|
-
const
|
|
3005
|
-
|
|
3006
|
-
siteUrl: siteUrl || "https://example.com",
|
|
3007
|
-
features: {
|
|
3008
|
-
audit: true,
|
|
3009
|
-
tracking: true,
|
|
3010
|
-
autoFix: true,
|
|
3011
|
-
createIssues: true,
|
|
3012
|
-
createPRs: true
|
|
3013
|
-
}
|
|
3014
|
-
};
|
|
3015
|
-
const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
|
|
3016
|
-
console.log("\u2705 GitHub Action files created:");
|
|
3017
|
-
for (const file of files) {
|
|
3018
|
-
console.log(` - ${path2.relative(projectPath, file)}`);
|
|
3019
|
-
}
|
|
3020
|
-
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));
|
|
3021
3425
|
}
|
|
3022
|
-
function
|
|
3023
|
-
|
|
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;
|
|
3024
3437
|
}
|
|
3025
3438
|
|
|
3026
3439
|
// src/commands/alerts.ts
|
|
@@ -3045,7 +3458,7 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
3045
3458
|
return answer.toLowerCase().startsWith("y");
|
|
3046
3459
|
}
|
|
3047
3460
|
async function alerts(options) {
|
|
3048
|
-
console.log("\n\u{1F514}
|
|
3461
|
+
console.log("\n\u{1F514} RankCLI - Alert Configuration\n");
|
|
3049
3462
|
const rl = readline3.createInterface({
|
|
3050
3463
|
input: process.stdin,
|
|
3051
3464
|
output: process.stdout
|
|
@@ -3188,235 +3601,18 @@ async function testWebhook(channel, webhookUrl) {
|
|
|
3188
3601
|
}
|
|
3189
3602
|
}
|
|
3190
3603
|
|
|
3191
|
-
// src/commands/auth.ts
|
|
3192
|
-
var import_chalk6 = __toESM(require("chalk"));
|
|
3193
|
-
var import_ora5 = __toESM(require("ora"));
|
|
3194
|
-
var import_inquirer = __toESM(require("inquirer"));
|
|
3195
|
-
var import_open = __toESM(require("open"));
|
|
3196
|
-
async function login(options) {
|
|
3197
|
-
if (isLoggedIn()) {
|
|
3198
|
-
const { email } = getUserInfo();
|
|
3199
|
-
console.log(import_chalk6.default.yellow(`Already logged in as ${email}`));
|
|
3200
|
-
console.log(import_chalk6.default.dim('Run "rankcli logout" to sign out first.'));
|
|
3201
|
-
return;
|
|
3202
|
-
}
|
|
3203
|
-
const spinner = (0, import_ora5.default)();
|
|
3204
|
-
try {
|
|
3205
|
-
if (options.token) {
|
|
3206
|
-
const apiKey = options.token;
|
|
3207
|
-
if (!apiKey.startsWith("rankcli_")) {
|
|
3208
|
-
console.log(import_chalk6.default.red('Invalid API key format. Keys should start with "rankcli_"'));
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
spinner.start("Validating API key...");
|
|
3212
|
-
const result = await validateApiKey(apiKey);
|
|
3213
|
-
if (!result.valid) {
|
|
3214
|
-
spinner.fail("Invalid API key");
|
|
3215
|
-
console.log(import_chalk6.default.red(`
|
|
3216
|
-
${result.error}`));
|
|
3217
|
-
return;
|
|
3218
|
-
}
|
|
3219
|
-
saveApiKey(apiKey);
|
|
3220
|
-
if (result.subscription) {
|
|
3221
|
-
saveSubscription({
|
|
3222
|
-
planId: result.subscription.planId,
|
|
3223
|
-
planName: result.subscription.planName,
|
|
3224
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3225
|
-
});
|
|
3226
|
-
}
|
|
3227
|
-
spinner.succeed(`Authenticated via API key`);
|
|
3228
|
-
console.log(import_chalk6.default.dim(`
|
|
3229
|
-
Email: ${result.user?.email}`));
|
|
3230
|
-
console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3231
|
-
return;
|
|
3232
|
-
}
|
|
3233
|
-
if (options.browser) {
|
|
3234
|
-
console.log(import_chalk6.default.cyan("\nOpening browser for login..."));
|
|
3235
|
-
console.log(import_chalk6.default.dim("Complete the login in your browser, then return here.\n"));
|
|
3236
|
-
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
3237
|
-
await (0, import_open.default)(loginUrl);
|
|
3238
|
-
const { token } = await import_inquirer.default.prompt([
|
|
3239
|
-
{
|
|
3240
|
-
type: "password",
|
|
3241
|
-
name: "token",
|
|
3242
|
-
message: "Paste your CLI token from the browser:",
|
|
3243
|
-
mask: "*"
|
|
3244
|
-
}
|
|
3245
|
-
]);
|
|
3246
|
-
spinner.start("Verifying token...");
|
|
3247
|
-
const supabase = getSupabaseClient();
|
|
3248
|
-
const { data, error } = await supabase.auth.getUser(token);
|
|
3249
|
-
if (error || !data.user) {
|
|
3250
|
-
spinner.fail("Invalid token");
|
|
3251
|
-
console.log(import_chalk6.default.red("The token is invalid or expired. Please try again."));
|
|
3252
|
-
return;
|
|
3253
|
-
}
|
|
3254
|
-
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
3255
|
-
access_token: token,
|
|
3256
|
-
refresh_token: ""
|
|
3257
|
-
// Will need to handle this differently
|
|
3258
|
-
});
|
|
3259
|
-
if (sessionError || !sessionData.session) {
|
|
3260
|
-
spinner.fail("Failed to establish session");
|
|
3261
|
-
return;
|
|
3262
|
-
}
|
|
3263
|
-
saveSession(sessionData.session);
|
|
3264
|
-
await fetchAndSaveSubscription();
|
|
3265
|
-
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
|
|
3266
|
-
} else {
|
|
3267
|
-
let email = options.email;
|
|
3268
|
-
if (!email) {
|
|
3269
|
-
const answers = await import_inquirer.default.prompt([
|
|
3270
|
-
{
|
|
3271
|
-
type: "input",
|
|
3272
|
-
name: "email",
|
|
3273
|
-
message: "Email:",
|
|
3274
|
-
validate: (input) => {
|
|
3275
|
-
if (!input.includes("@")) return "Please enter a valid email";
|
|
3276
|
-
return true;
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
]);
|
|
3280
|
-
email = answers.email;
|
|
3281
|
-
}
|
|
3282
|
-
const { password } = await import_inquirer.default.prompt([
|
|
3283
|
-
{
|
|
3284
|
-
type: "password",
|
|
3285
|
-
name: "password",
|
|
3286
|
-
message: "Password:",
|
|
3287
|
-
mask: "*"
|
|
3288
|
-
}
|
|
3289
|
-
]);
|
|
3290
|
-
spinner.start("Signing in...");
|
|
3291
|
-
const supabase = getSupabaseClient();
|
|
3292
|
-
const { data, error } = await supabase.auth.signInWithPassword({
|
|
3293
|
-
email,
|
|
3294
|
-
password
|
|
3295
|
-
});
|
|
3296
|
-
if (error) {
|
|
3297
|
-
spinner.fail("Login failed");
|
|
3298
|
-
console.log(import_chalk6.default.red(`
|
|
3299
|
-
${error.message}`));
|
|
3300
|
-
if (error.message.includes("Invalid login")) {
|
|
3301
|
-
console.log(import_chalk6.default.dim("\nDon't have an account? Sign up at:"));
|
|
3302
|
-
console.log(import_chalk6.default.cyan(` ${WEB_APP_URL}/signup`));
|
|
3303
|
-
}
|
|
3304
|
-
return;
|
|
3305
|
-
}
|
|
3306
|
-
if (!data.session) {
|
|
3307
|
-
spinner.fail("No session returned");
|
|
3308
|
-
return;
|
|
3309
|
-
}
|
|
3310
|
-
saveSession(data.session);
|
|
3311
|
-
await fetchAndSaveSubscription();
|
|
3312
|
-
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user?.email)}`);
|
|
3313
|
-
const { planName, sitesLimit } = getUserInfo();
|
|
3314
|
-
console.log(import_chalk6.default.dim(`
|
|
3315
|
-
Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
|
|
3316
|
-
}
|
|
3317
|
-
} catch (err) {
|
|
3318
|
-
spinner.fail("Login failed");
|
|
3319
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
async function logout() {
|
|
3323
|
-
if (!isLoggedIn()) {
|
|
3324
|
-
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3325
|
-
return;
|
|
3326
|
-
}
|
|
3327
|
-
const { email } = getUserInfo();
|
|
3328
|
-
const spinner = (0, import_ora5.default)("Signing out...").start();
|
|
3329
|
-
try {
|
|
3330
|
-
const supabase = getSupabaseClient();
|
|
3331
|
-
await supabase.auth.signOut();
|
|
3332
|
-
clearCredentials();
|
|
3333
|
-
spinner.succeed(`Logged out from ${import_chalk6.default.dim(email)}`);
|
|
3334
|
-
} catch (err) {
|
|
3335
|
-
spinner.fail("Logout failed");
|
|
3336
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
async function whoami() {
|
|
3340
|
-
if (!isLoggedIn()) {
|
|
3341
|
-
console.log(import_chalk6.default.yellow("Not logged in"));
|
|
3342
|
-
console.log(import_chalk6.default.dim('\nRun "rankcli login" to sign in.'));
|
|
3343
|
-
console.log(import_chalk6.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
|
|
3344
|
-
return;
|
|
3345
|
-
}
|
|
3346
|
-
const spinner = (0, import_ora5.default)("Checking account...").start();
|
|
3347
|
-
try {
|
|
3348
|
-
if (isApiKeyAuth()) {
|
|
3349
|
-
const apiKey = getApiKey();
|
|
3350
|
-
if (apiKey) {
|
|
3351
|
-
const result = await validateApiKey(apiKey);
|
|
3352
|
-
if (result.valid && result.subscription) {
|
|
3353
|
-
saveSubscription({
|
|
3354
|
-
planId: result.subscription.planId,
|
|
3355
|
-
planName: result.subscription.planName,
|
|
3356
|
-
sitesLimit: result.subscription.sitesLimit
|
|
3357
|
-
});
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
} else {
|
|
3361
|
-
await fetchAndSaveSubscription();
|
|
3362
|
-
}
|
|
3363
|
-
const { email, planName, sitesLimit } = getUserInfo();
|
|
3364
|
-
const authMethod = isApiKeyAuth() ? "API Key" : "Session";
|
|
3365
|
-
spinner.stop();
|
|
3366
|
-
console.log(import_chalk6.default.bold("\nRankCLI Account"));
|
|
3367
|
-
console.log("\u2500".repeat(40));
|
|
3368
|
-
console.log(` Email: ${import_chalk6.default.cyan(email || "N/A")}`);
|
|
3369
|
-
console.log(` Plan: ${import_chalk6.default.green(planName)}`);
|
|
3370
|
-
console.log(` Sites: ${sitesLimit} allowed`);
|
|
3371
|
-
console.log(` Auth: ${import_chalk6.default.dim(authMethod)}`);
|
|
3372
|
-
console.log("\u2500".repeat(40));
|
|
3373
|
-
console.log(import_chalk6.default.dim(`
|
|
3374
|
-
Manage at: ${WEB_APP_URL}/account`));
|
|
3375
|
-
} catch (err) {
|
|
3376
|
-
spinner.fail("Failed to fetch account info");
|
|
3377
|
-
console.error(import_chalk6.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
async function fetchAndSaveSubscription() {
|
|
3381
|
-
const supabase = await getAuthenticatedClient();
|
|
3382
|
-
if (!supabase) return;
|
|
3383
|
-
const { userId } = getUserInfo();
|
|
3384
|
-
if (!userId) return;
|
|
3385
|
-
const { data: subscription } = await supabase.from("user_subscriptions").select(`
|
|
3386
|
-
plan:subscription_plans (
|
|
3387
|
-
plan_id,
|
|
3388
|
-
name,
|
|
3389
|
-
sites_limit
|
|
3390
|
-
)
|
|
3391
|
-
`).eq("user_id", userId).single();
|
|
3392
|
-
if (subscription?.plan) {
|
|
3393
|
-
const plan = subscription.plan;
|
|
3394
|
-
saveSubscription({
|
|
3395
|
-
planId: plan.plan_id,
|
|
3396
|
-
planName: plan.name,
|
|
3397
|
-
sitesLimit: plan.sites_limit
|
|
3398
|
-
});
|
|
3399
|
-
} else {
|
|
3400
|
-
saveSubscription({
|
|
3401
|
-
planId: void 0,
|
|
3402
|
-
planName: "Free",
|
|
3403
|
-
sitesLimit: 1
|
|
3404
|
-
});
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
3604
|
// src/version.ts
|
|
3409
|
-
var VERSION = "0.0.
|
|
3605
|
+
var VERSION = "0.0.25";
|
|
3410
3606
|
|
|
3411
3607
|
// src/index.ts
|
|
3412
3608
|
var program = new import_commander.Command();
|
|
3413
3609
|
program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version(VERSION);
|
|
3414
|
-
program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "
|
|
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);
|
|
3415
3611
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3416
3612
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3417
|
-
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);
|
|
3418
3614
|
program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
|
|
3419
|
-
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);
|
|
3420
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);
|
|
3421
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) => {
|
|
3422
3618
|
const result = await fix({
|