@rankcli/cli 0.0.25 → 0.0.26

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