@rankcli/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1901 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/commands/init.ts
30
+ var import_fs = require("fs");
31
+ var import_path = require("path");
32
+ var FRAMEWORKS = [
33
+ { pattern: "next.config", name: "Next.js" },
34
+ { pattern: "astro.config", name: "Astro" },
35
+ { pattern: "nuxt.config", name: "Nuxt" },
36
+ { pattern: "svelte.config", name: "SvelteKit" },
37
+ { pattern: "gatsby-config", name: "Gatsby" },
38
+ { pattern: "vite.config", name: "Vite" },
39
+ { pattern: "hugo.toml", name: "Hugo" },
40
+ { pattern: "_config.yml", name: "Jekyll" },
41
+ { pattern: "angular.json", name: "Angular" },
42
+ { pattern: "vue.config", name: "Vue CLI" }
43
+ ];
44
+ function detectFramework() {
45
+ const cwd = process.cwd();
46
+ for (const fw of FRAMEWORKS) {
47
+ const patterns = [
48
+ `${fw.pattern}.js`,
49
+ `${fw.pattern}.ts`,
50
+ `${fw.pattern}.mjs`,
51
+ `${fw.pattern}.cjs`,
52
+ fw.pattern
53
+ ];
54
+ for (const p of patterns) {
55
+ if ((0, import_fs.existsSync)((0, import_path.join)(cwd, p))) {
56
+ return fw.name;
57
+ }
58
+ }
59
+ }
60
+ const pkgPath = (0, import_path.join)(cwd, "package.json");
61
+ if ((0, import_fs.existsSync)(pkgPath)) {
62
+ try {
63
+ const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
64
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
65
+ if (deps["next"]) return "Next.js";
66
+ if (deps["astro"]) return "Astro";
67
+ if (deps["nuxt"]) return "Nuxt";
68
+ if (deps["@sveltejs/kit"]) return "SvelteKit";
69
+ if (deps["gatsby"]) return "Gatsby";
70
+ if (deps["react"]) return "React";
71
+ if (deps["vue"]) return "Vue";
72
+ } catch {
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ function getProjectName() {
78
+ const cwd = process.cwd();
79
+ const pkgPath = (0, import_path.join)(cwd, "package.json");
80
+ if ((0, import_fs.existsSync)(pkgPath)) {
81
+ try {
82
+ const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
83
+ if (pkg.name) return pkg.name;
84
+ } catch {
85
+ }
86
+ }
87
+ return cwd.split("/").pop() || "unknown";
88
+ }
89
+ async function init(options) {
90
+ console.log("\n\u{1F680} Initializing SEO Autopilot...\n");
91
+ const configPath = (0, import_path.join)(process.cwd(), "seo-autopilot.config.json");
92
+ if ((0, import_fs.existsSync)(configPath)) {
93
+ console.log("\u26A0\uFE0F Configuration file already exists: seo-autopilot.config.json");
94
+ console.log(' Run "seo analyze" to analyze your project.\n');
95
+ return;
96
+ }
97
+ const framework = detectFramework();
98
+ const projectName = getProjectName();
99
+ console.log(`\u{1F4E6} Project: ${projectName}`);
100
+ console.log(`\u{1F527} Framework: ${framework || "Unknown (static site?)"}`);
101
+ const config2 = {
102
+ name: projectName,
103
+ framework,
104
+ url: null,
105
+ initialized: (/* @__PURE__ */ new Date()).toISOString()
106
+ };
107
+ (0, import_fs.writeFileSync)(configPath, JSON.stringify(config2, null, 2));
108
+ console.log("\n\u2705 Created seo-autopilot.config.json");
109
+ console.log("\n\u{1F4CB} Next steps:");
110
+ console.log(' 1. Run "seo analyze" to analyze your project');
111
+ console.log(' 2. Run "seo audit --url <your-site>" to run a technical audit');
112
+ console.log(" 3. Connect Google Search Console at https://seo-autopilot.dev/connect\n");
113
+ }
114
+
115
+ // src/commands/analyze.ts
116
+ var import_fs2 = require("fs");
117
+ var import_path2 = require("path");
118
+ var CONTENT_EXTENSIONS = [".md", ".mdx", ".html", ".tsx", ".jsx", ".vue", ".astro", ".svelte"];
119
+ function findFiles(dir, extensions, maxDepth = 3, depth = 0) {
120
+ if (depth > maxDepth) return [];
121
+ const files = [];
122
+ try {
123
+ const items = (0, import_fs2.readdirSync)(dir);
124
+ for (const item of items) {
125
+ if (item.startsWith(".") || item === "node_modules" || item === "dist" || item === "build") {
126
+ continue;
127
+ }
128
+ const fullPath = (0, import_path2.join)(dir, item);
129
+ const stat = (0, import_fs2.statSync)(fullPath);
130
+ if (stat.isDirectory()) {
131
+ files.push(...findFiles(fullPath, extensions, maxDepth, depth + 1));
132
+ } else if (extensions.includes((0, import_path2.extname)(item))) {
133
+ files.push(fullPath);
134
+ }
135
+ }
136
+ } catch {
137
+ }
138
+ return files;
139
+ }
140
+ function extractKeywords(text) {
141
+ const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3);
142
+ const frequency = {};
143
+ for (const word of words) {
144
+ frequency[word] = (frequency[word] || 0) + 1;
145
+ }
146
+ const stopWords = /* @__PURE__ */ new Set([
147
+ "this",
148
+ "that",
149
+ "with",
150
+ "from",
151
+ "have",
152
+ "been",
153
+ "were",
154
+ "will",
155
+ "would",
156
+ "could",
157
+ "should",
158
+ "their",
159
+ "there",
160
+ "about",
161
+ "which",
162
+ "when",
163
+ "what",
164
+ "your",
165
+ "more",
166
+ "some",
167
+ "just",
168
+ "into",
169
+ "over",
170
+ "such",
171
+ "only",
172
+ "than",
173
+ "them",
174
+ "then",
175
+ "these",
176
+ "most",
177
+ "other",
178
+ "also",
179
+ "being",
180
+ "after",
181
+ "const",
182
+ "function",
183
+ "return",
184
+ "import",
185
+ "export",
186
+ "default",
187
+ "true",
188
+ "false"
189
+ ]);
190
+ return Object.entries(frequency).filter(([word]) => !stopWords.has(word)).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([word]) => word);
191
+ }
192
+ function loadConfig() {
193
+ const configPath = (0, import_path2.join)(process.cwd(), "seo-autopilot.config.json");
194
+ if ((0, import_fs2.existsSync)(configPath)) {
195
+ try {
196
+ return JSON.parse((0, import_fs2.readFileSync)(configPath, "utf-8"));
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ async function analyze(options) {
204
+ console.log("\n\u{1F50D} Analyzing project for SEO opportunities...\n");
205
+ const cwd = process.cwd();
206
+ const config2 = loadConfig();
207
+ const result = {
208
+ projectName: config2?.name || cwd.split("/").pop() || "unknown",
209
+ framework: config2?.framework || null,
210
+ hasReadme: false,
211
+ readmeSummary: null,
212
+ pageCount: 0,
213
+ hasMetaTags: false,
214
+ hasSitemap: false,
215
+ hasRobotsTxt: false,
216
+ keywords: [],
217
+ suggestions: []
218
+ };
219
+ const readmePaths = ["README.md", "readme.md", "Readme.md", "README.MD"];
220
+ for (const rp of readmePaths) {
221
+ if ((0, import_fs2.existsSync)((0, import_path2.join)(cwd, rp))) {
222
+ result.hasReadme = true;
223
+ const content = (0, import_fs2.readFileSync)((0, import_path2.join)(cwd, rp), "utf-8");
224
+ result.readmeSummary = content.slice(0, 200).replace(/\n/g, " ").trim() + "...";
225
+ result.keywords.push(...extractKeywords(content));
226
+ break;
227
+ }
228
+ }
229
+ result.hasSitemap = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "sitemap.xml")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "sitemap.xml"));
230
+ result.hasRobotsTxt = (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "public", "robots.txt")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "robots.txt"));
231
+ const contentFiles = findFiles(cwd, CONTENT_EXTENSIONS);
232
+ result.pageCount = contentFiles.length;
233
+ const htmlFiles = contentFiles.filter((f) => f.endsWith(".html") || f.includes("_document") || f.includes("layout"));
234
+ for (const hf of htmlFiles.slice(0, 5)) {
235
+ const content = (0, import_fs2.readFileSync)(hf, "utf-8");
236
+ if (content.includes("<meta") || content.includes("metadata")) {
237
+ result.hasMetaTags = true;
238
+ break;
239
+ }
240
+ }
241
+ for (const cf of contentFiles.slice(0, 10)) {
242
+ const content = (0, import_fs2.readFileSync)(cf, "utf-8");
243
+ result.keywords.push(...extractKeywords(content));
244
+ }
245
+ result.keywords = [...new Set(result.keywords)].slice(0, 20);
246
+ if (!result.hasReadme) {
247
+ result.suggestions.push("Add a README.md with clear project description");
248
+ }
249
+ if (!result.hasSitemap) {
250
+ result.suggestions.push("Generate a sitemap.xml for better crawlability");
251
+ }
252
+ if (!result.hasRobotsTxt) {
253
+ result.suggestions.push("Add robots.txt to guide search engine crawlers");
254
+ }
255
+ if (!result.hasMetaTags) {
256
+ result.suggestions.push("Add meta tags (title, description, og:*) to pages");
257
+ }
258
+ if (result.pageCount < 5) {
259
+ result.suggestions.push("Consider adding more content pages for SEO coverage");
260
+ }
261
+ console.log("\u{1F4CA} Analysis Results\n");
262
+ console.log(`Project: ${result.projectName}`);
263
+ console.log(`Framework: ${result.framework || "Unknown"}`);
264
+ console.log(`Content files: ${result.pageCount}`);
265
+ console.log(`Has README: ${result.hasReadme ? "\u2705" : "\u274C"}`);
266
+ console.log(`Has sitemap.xml: ${result.hasSitemap ? "\u2705" : "\u274C"}`);
267
+ console.log(`Has robots.txt: ${result.hasRobotsTxt ? "\u2705" : "\u274C"}`);
268
+ console.log(`Has meta tags: ${result.hasMetaTags ? "\u2705" : "\u274C"}`);
269
+ if (result.keywords.length > 0) {
270
+ console.log("\n\u{1F511} Detected Keywords:");
271
+ console.log(` ${result.keywords.join(", ")}`);
272
+ }
273
+ if (result.suggestions.length > 0) {
274
+ console.log("\n\u{1F4A1} Suggestions:");
275
+ for (const s of result.suggestions) {
276
+ console.log(` \u2022 ${s}`);
277
+ }
278
+ } else {
279
+ console.log("\n\u2705 No critical SEO issues found!");
280
+ }
281
+ console.log('\n\u{1F4C8} Next: Run "seo audit --url <your-deployed-site>" for a full technical audit\n');
282
+ }
283
+
284
+ // src/commands/audit.ts
285
+ var import_agent_runtime = require("@rankcli/agent-runtime");
286
+ var import_ora = __toESM(require("ora"));
287
+ var import_chalk = __toESM(require("chalk"));
288
+
289
+ // src/lib/config.ts
290
+ var import_conf = __toESM(require("conf"));
291
+ var import_supabase_js = require("@supabase/supabase-js");
292
+ var SUPABASE_URL = process.env.RANKCLI_SUPABASE_URL || process.env.VITE_SUPABASE_URL || "https://uisqfyeasobjjrgurwnb.supabase.co";
293
+ var SUPABASE_ANON_KEY = process.env.RANKCLI_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVpc3FmeWVhc29iampyZ3Vyd25iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDgwMDc3NjIsImV4cCI6MjA2MzU4Mzc2Mn0.r-GaAqD-OwAKMyH8w7qpAj6P7w52cAPTsb8JfMZmtJE";
294
+ var WEB_APP_URL = process.env.RANKCLI_WEB_URL || "http://localhost:3005";
295
+ var API_URL = process.env.RANKCLI_API_URL || SUPABASE_URL;
296
+ var config = new import_conf.default({
297
+ projectName: "rankcli",
298
+ schema: {
299
+ accessToken: { type: "string" },
300
+ refreshToken: { type: "string" },
301
+ expiresAt: { type: "number" },
302
+ userId: { type: "string" },
303
+ email: { type: "string" },
304
+ apiKey: { type: "string" },
305
+ apiKeyName: { type: "string" },
306
+ planId: { type: "string" },
307
+ planName: { type: "string" },
308
+ sitesLimit: { type: "number" }
309
+ }
310
+ });
311
+ var supabaseClient = null;
312
+ function getSupabaseClient() {
313
+ if (!supabaseClient) {
314
+ supabaseClient = (0, import_supabase_js.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY, {
315
+ auth: {
316
+ persistSession: false,
317
+ // We manage session ourselves via conf
318
+ autoRefreshToken: false
319
+ }
320
+ });
321
+ }
322
+ return supabaseClient;
323
+ }
324
+ function isLoggedIn() {
325
+ const apiKey = process.env.RANKCLI_API_KEY || config.get("apiKey");
326
+ if (apiKey && apiKey.startsWith("rankcli_")) {
327
+ return true;
328
+ }
329
+ const accessToken = config.get("accessToken");
330
+ const expiresAt = config.get("expiresAt");
331
+ if (!accessToken || !expiresAt) {
332
+ return false;
333
+ }
334
+ const now = Math.floor(Date.now() / 1e3);
335
+ return expiresAt > now + 300;
336
+ }
337
+ function isApiKeyAuth() {
338
+ const apiKey = process.env.RANKCLI_API_KEY || config.get("apiKey");
339
+ return !!(apiKey && apiKey.startsWith("rankcli_"));
340
+ }
341
+ function getApiKey() {
342
+ return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
343
+ }
344
+ function saveApiKey(apiKey, keyName) {
345
+ config.set("apiKey", apiKey);
346
+ if (keyName) {
347
+ config.set("apiKeyName", keyName);
348
+ }
349
+ }
350
+ async function validateApiKey(apiKey) {
351
+ try {
352
+ const response = await fetch(`${API_URL}/functions/v1/validate-api-key`, {
353
+ method: "POST",
354
+ headers: {
355
+ "Content-Type": "application/json",
356
+ "X-API-Key": apiKey
357
+ }
358
+ });
359
+ const data = await response.json();
360
+ if (!response.ok) {
361
+ return { valid: false, error: data.error || "Invalid API key" };
362
+ }
363
+ return {
364
+ valid: true,
365
+ user: data.user,
366
+ subscription: data.subscription
367
+ };
368
+ } catch (err) {
369
+ return { valid: false, error: err instanceof Error ? err.message : "Validation failed" };
370
+ }
371
+ }
372
+ async function getSession() {
373
+ const accessToken = config.get("accessToken");
374
+ const refreshToken = config.get("refreshToken");
375
+ const expiresAt = config.get("expiresAt");
376
+ if (!accessToken || !refreshToken) {
377
+ return null;
378
+ }
379
+ const now = Math.floor(Date.now() / 1e3);
380
+ if (expiresAt && expiresAt <= now + 300) {
381
+ const supabase = getSupabaseClient();
382
+ const { data, error } = await supabase.auth.refreshSession({
383
+ refresh_token: refreshToken
384
+ });
385
+ if (error || !data.session) {
386
+ clearCredentials();
387
+ return null;
388
+ }
389
+ saveSession(data.session);
390
+ return data.session;
391
+ }
392
+ return {
393
+ access_token: accessToken,
394
+ refresh_token: refreshToken,
395
+ expires_at: expiresAt,
396
+ expires_in: expiresAt ? expiresAt - now : 0,
397
+ token_type: "bearer",
398
+ user: {
399
+ id: config.get("userId") || "",
400
+ email: config.get("email"),
401
+ app_metadata: {},
402
+ user_metadata: {},
403
+ aud: "authenticated",
404
+ created_at: ""
405
+ }
406
+ };
407
+ }
408
+ function saveSession(session) {
409
+ config.set("accessToken", session.access_token);
410
+ config.set("refreshToken", session.refresh_token);
411
+ config.set("expiresAt", session.expires_at);
412
+ config.set("userId", session.user.id);
413
+ config.set("email", session.user.email);
414
+ }
415
+ function saveSubscription(subscription) {
416
+ if (subscription.planId !== void 0) config.set("planId", subscription.planId);
417
+ if (subscription.planName !== void 0) config.set("planName", subscription.planName);
418
+ if (subscription.sitesLimit !== void 0) config.set("sitesLimit", subscription.sitesLimit);
419
+ }
420
+ function clearCredentials() {
421
+ config.delete("accessToken");
422
+ config.delete("refreshToken");
423
+ config.delete("expiresAt");
424
+ config.delete("userId");
425
+ config.delete("email");
426
+ config.delete("apiKey");
427
+ config.delete("apiKeyName");
428
+ config.delete("planId");
429
+ config.delete("planName");
430
+ config.delete("sitesLimit");
431
+ }
432
+ function getUserInfo() {
433
+ return {
434
+ userId: config.get("userId"),
435
+ email: config.get("email"),
436
+ planName: config.get("planName") || "Free",
437
+ sitesLimit: config.get("sitesLimit") || 1
438
+ };
439
+ }
440
+ async function getAuthenticatedClient() {
441
+ const session = await getSession();
442
+ if (!session) {
443
+ return null;
444
+ }
445
+ const supabase = getSupabaseClient();
446
+ await supabase.auth.setSession({
447
+ access_token: session.access_token,
448
+ refresh_token: session.refresh_token
449
+ });
450
+ return supabase;
451
+ }
452
+
453
+ // src/commands/audit.ts
454
+ var { detectFramework: detectFramework2, getFrameworkDisplayName, getFrameworkIssues } = import_agent_runtime.frameworks;
455
+ async function fetchHtml(url, timeout = 1e4) {
456
+ try {
457
+ const controller = new AbortController();
458
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
459
+ const response = await fetch(url, {
460
+ signal: controller.signal,
461
+ headers: {
462
+ "User-Agent": "RankCLI-Bot/1.0 (+https://rankcli.dev/bot)",
463
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
464
+ }
465
+ });
466
+ clearTimeout(timeoutId);
467
+ if (!response.ok) {
468
+ return null;
469
+ }
470
+ return await response.text();
471
+ } catch {
472
+ return null;
473
+ }
474
+ }
475
+ async function runAIAnalysis(url, issues, provider, apiKey) {
476
+ const isAnthropic = provider === "anthropic";
477
+ const apiUrl = isAnthropic ? "https://api.anthropic.com/v1/messages" : "https://api.openai.com/v1/chat/completions";
478
+ const prompt = `Analyze these SEO audit results and provide prioritized recommendations:
479
+
480
+ URL: ${url}
481
+
482
+ Issues found:
483
+ ${issues.map((i) => `- [${i.severity.toUpperCase()}] ${i.code}: ${i.title}`).join("\n")}
484
+
485
+ Return JSON with:
486
+ {
487
+ "contentScore": <0-100 overall content quality>,
488
+ "suggestions": [<3-5 high-impact suggestions>],
489
+ "prioritizedFixes": [
490
+ {"code": "<issue code>", "priority": <1-5>, "rationale": "<why fix this first>"}
491
+ ]
492
+ }`;
493
+ try {
494
+ const headers = {
495
+ "Content-Type": "application/json"
496
+ };
497
+ let body;
498
+ if (isAnthropic) {
499
+ headers["x-api-key"] = apiKey;
500
+ headers["anthropic-version"] = "2023-06-01";
501
+ body = JSON.stringify({
502
+ model: "claude-3-haiku-20240307",
503
+ max_tokens: 1500,
504
+ messages: [{ role: "user", content: prompt }]
505
+ });
506
+ } else {
507
+ headers["Authorization"] = `Bearer ${apiKey}`;
508
+ body = JSON.stringify({
509
+ model: "gpt-4o-mini",
510
+ messages: [
511
+ { role: "system", content: "You are an SEO expert. Respond with valid JSON only." },
512
+ { role: "user", content: prompt }
513
+ ],
514
+ max_tokens: 1500,
515
+ response_format: { type: "json_object" }
516
+ });
517
+ }
518
+ const response = await fetch(apiUrl, { method: "POST", headers, body });
519
+ if (!response.ok) {
520
+ console.error(import_chalk.default.yellow(`AI analysis failed: ${response.status}`));
521
+ return null;
522
+ }
523
+ const data = await response.json();
524
+ const content = isAnthropic ? data.content?.[0]?.text : data.choices?.[0]?.message?.content;
525
+ if (!content) return null;
526
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
527
+ if (!jsonMatch) return null;
528
+ return JSON.parse(jsonMatch[0]);
529
+ } catch (error) {
530
+ console.error(import_chalk.default.yellow(`AI analysis error: ${error}`));
531
+ return null;
532
+ }
533
+ }
534
+ function mergeIssues(issueArrays) {
535
+ const issueMap = /* @__PURE__ */ new Map();
536
+ for (const issues of issueArrays) {
537
+ for (const issue of issues) {
538
+ const existing = issueMap.get(issue.code);
539
+ if (existing) {
540
+ const urls = /* @__PURE__ */ new Set([...existing.affectedUrls || [], ...issue.affectedUrls || []]);
541
+ existing.affectedUrls = Array.from(urls);
542
+ } else {
543
+ issueMap.set(issue.code, { ...issue });
544
+ }
545
+ }
546
+ }
547
+ return Array.from(issueMap.values());
548
+ }
549
+ async function syncToCloud(url, report, mergedIssues, pagesAudited, aiAnalysis) {
550
+ const supabase = await getAuthenticatedClient();
551
+ if (!supabase) return null;
552
+ const { userId, sitesLimit } = getUserInfo();
553
+ if (!userId) return null;
554
+ try {
555
+ const parsedUrl = new URL(url);
556
+ const domain = parsedUrl.hostname;
557
+ const { data: existingProject } = await supabase.from("projects").select("id").eq("user_id", userId).eq("domain", domain).eq("is_active", true).single();
558
+ let projectId;
559
+ if (existingProject) {
560
+ projectId = existingProject.id;
561
+ } else {
562
+ const { count } = await supabase.from("projects").select("id", { count: "exact", head: true }).eq("user_id", userId).eq("is_active", true);
563
+ const limit = sitesLimit ?? 1;
564
+ if (count !== null && count >= limit) {
565
+ console.log(import_chalk.default.yellow(`
566
+ \u26A0\uFE0F Project limit reached (${count}/${limit})`));
567
+ console.log(import_chalk.default.dim(` Upgrade at: ${WEB_APP_URL}/account`));
568
+ return null;
569
+ }
570
+ const { data: newProject, error: projectError } = await supabase.from("projects").insert({
571
+ user_id: userId,
572
+ name: domain,
573
+ url,
574
+ domain
575
+ }).select("id").single();
576
+ if (projectError || !newProject) {
577
+ console.error(import_chalk.default.dim("Failed to create project"));
578
+ return null;
579
+ }
580
+ projectId = newProject.id;
581
+ }
582
+ const score = report.healthScore?.overall || 0;
583
+ const healthScores = {
584
+ overall: score,
585
+ crawlability: report.healthScore?.crawlability || 0,
586
+ onPage: report.healthScore?.onPage || 0,
587
+ content: report.healthScore?.content || 0,
588
+ performance: report.healthScore?.performance || 0,
589
+ security: report.healthScore?.security || 0
590
+ };
591
+ const { data: audit2, error: auditError } = await supabase.from("audits").insert({
592
+ project_id: projectId,
593
+ triggered_by: "cli",
594
+ triggered_by_user: userId,
595
+ status: "completed",
596
+ score,
597
+ pages_crawled: pagesAudited,
598
+ issues_found: mergedIssues.length,
599
+ checks_run: mergedIssues.length > 0 ? 200 : 0,
600
+ // Approximate
601
+ health_scores: healthScores,
602
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
603
+ summary: {
604
+ errors: mergedIssues.filter((i) => i.severity === "error").length,
605
+ warnings: mergedIssues.filter((i) => i.severity === "warning").length,
606
+ notices: mergedIssues.filter((i) => i.severity === "notice").length,
607
+ pagesAudited,
608
+ source: "cli"
609
+ },
610
+ ai_analysis: aiAnalysis
611
+ }).select("id").single();
612
+ if (auditError || !audit2) {
613
+ console.error(import_chalk.default.dim("Failed to save audit"));
614
+ return null;
615
+ }
616
+ if (mergedIssues.length > 0) {
617
+ const issueRecords = mergedIssues.map((issue) => ({
618
+ audit_id: audit2.id,
619
+ project_id: projectId,
620
+ code: issue.code,
621
+ severity: issue.severity,
622
+ category: issue.category,
623
+ title: issue.title,
624
+ description: issue.description || "",
625
+ impact: issue.impact || "",
626
+ how_to_fix: issue.howToFix || "",
627
+ affected_urls: issue.affectedUrls || [],
628
+ details: issue.details || {},
629
+ status: "open"
630
+ }));
631
+ await supabase.from("audit_issues").insert(issueRecords);
632
+ }
633
+ await supabase.from("projects").update({
634
+ last_audit_at: (/* @__PURE__ */ new Date()).toISOString(),
635
+ last_audit_score: score
636
+ }).eq("id", projectId);
637
+ return { projectId, auditId: audit2.id };
638
+ } catch (error) {
639
+ console.error(import_chalk.default.dim("Cloud sync failed"));
640
+ return null;
641
+ }
642
+ }
643
+ async function audit(options) {
644
+ const url = options.url;
645
+ const maxPages = parseInt(options.maxPages || "5", 10);
646
+ const enableAI = options.ai || false;
647
+ const aiProvider = options.aiProvider || "openai";
648
+ const aiKey = options.aiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || "";
649
+ const loggedIn = isLoggedIn();
650
+ if (!url) {
651
+ console.log("\n\u26A0\uFE0F Please provide a URL to audit:");
652
+ console.log(" rankcli audit --url https://your-site.com");
653
+ console.log("\nOptions:");
654
+ console.log(" --max-pages <n> Max pages to crawl (default: 5)");
655
+ console.log(" --ai Enable AI analysis");
656
+ console.log(" --ai-provider <p> AI provider: openai or anthropic");
657
+ console.log(" --ai-key <key> API key (or set OPENAI_API_KEY/ANTHROPIC_API_KEY)");
658
+ console.log(" --output json Output as JSON");
659
+ if (!loggedIn) {
660
+ console.log(import_chalk.default.dim("\nTip: Log in to sync results to the web dashboard:"));
661
+ console.log(import_chalk.default.dim(" rankcli login"));
662
+ }
663
+ console.log("");
664
+ return;
665
+ }
666
+ try {
667
+ new URL(url);
668
+ } catch {
669
+ console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
670
+ return;
671
+ }
672
+ const startTime = Date.now();
673
+ try {
674
+ const discoverySpinner = (0, import_ora.default)("Discovering pages...").start();
675
+ let pagesToAudit = [url];
676
+ const homepageHtml = await fetchHtml(url, 1e4);
677
+ let detectedFramework = detectFramework2({});
678
+ if (homepageHtml) {
679
+ detectedFramework = detectFramework2({ html: homepageHtml });
680
+ const discovered = (0, import_agent_runtime.discoverPagesFromLinks)(url, homepageHtml, maxPages - 1);
681
+ const additionalPages = discovered.map((p) => p.url).filter((u) => u !== url);
682
+ pagesToAudit = [url, ...additionalPages];
683
+ const frameworkLabel = detectedFramework.framework !== "unknown" ? ` (${getFrameworkDisplayName(detectedFramework.framework)})` : "";
684
+ discoverySpinner.succeed(`Discovered ${pagesToAudit.length} pages to audit${frameworkLabel}`);
685
+ } else {
686
+ discoverySpinner.warn("Could not fetch homepage, auditing URL only");
687
+ }
688
+ const auditSpinner = (0, import_ora.default)(`Auditing ${pagesToAudit.length} pages in parallel...`).start();
689
+ const auditPage = async (pageUrl, index) => {
690
+ try {
691
+ const report = await (0, import_agent_runtime.runFullAudit)({
692
+ url: pageUrl,
693
+ maxPages: 1,
694
+ checkBrokenLinks: options.checkLinks || false
695
+ });
696
+ auditSpinner.text = `Auditing pages... (${index + 1}/${pagesToAudit.length} complete)`;
697
+ return { report, url: pageUrl };
698
+ } catch (error) {
699
+ return { report: null, url: pageUrl };
700
+ }
701
+ };
702
+ const parallelStart = Date.now();
703
+ const auditResults = await Promise.allSettled(
704
+ pagesToAudit.map((pageUrl, index) => auditPage(pageUrl, index))
705
+ );
706
+ const parallelTime = Date.now() - parallelStart;
707
+ const allIssues = [];
708
+ let primaryReport = null;
709
+ let successCount = 0;
710
+ for (let i = 0; i < auditResults.length; i++) {
711
+ const result = auditResults[i];
712
+ if (result.status === "fulfilled" && result.value.report) {
713
+ allIssues.push(result.value.report.issues);
714
+ if (i === 0) {
715
+ primaryReport = result.value.report;
716
+ }
717
+ successCount++;
718
+ }
719
+ }
720
+ if (!primaryReport) {
721
+ auditSpinner.fail("Audit failed - could not analyze any pages");
722
+ return;
723
+ }
724
+ auditSpinner.succeed(`Audited ${successCount}/${pagesToAudit.length} pages in ${parallelTime}ms`);
725
+ let mergedIssues = mergeIssues(allIssues);
726
+ if (detectedFramework.framework !== "unknown" && homepageHtml) {
727
+ try {
728
+ const frameworkIssues = await getFrameworkIssues(detectedFramework, homepageHtml, url);
729
+ if (frameworkIssues.length > 0) {
730
+ mergedIssues = [...mergedIssues, ...frameworkIssues];
731
+ }
732
+ } catch {
733
+ }
734
+ }
735
+ let aiAnalysis = null;
736
+ if (enableAI && aiKey) {
737
+ const aiSpinner = (0, import_ora.default)(`Running AI analysis (${aiProvider})...`).start();
738
+ aiAnalysis = await runAIAnalysis(url, mergedIssues, aiProvider, aiKey);
739
+ if (aiAnalysis) {
740
+ aiSpinner.succeed("AI analysis complete");
741
+ } else {
742
+ aiSpinner.warn("AI analysis failed or returned no results");
743
+ }
744
+ } else if (enableAI && !aiKey) {
745
+ console.log(import_chalk.default.yellow("\n\u26A0\uFE0F AI analysis requested but no API key provided"));
746
+ console.log(import_chalk.default.dim(" Set OPENAI_API_KEY or ANTHROPIC_API_KEY, or use --ai-key"));
747
+ }
748
+ const totalTime = Date.now() - startTime;
749
+ const frameworkName = detectedFramework.framework !== "unknown" ? getFrameworkDisplayName(detectedFramework.framework) : null;
750
+ if (options.output === "json") {
751
+ const jsonOutput = {
752
+ url,
753
+ score: primaryReport.healthScore?.overall || 0,
754
+ pagesAudited: successCount,
755
+ totalTime,
756
+ framework: frameworkName,
757
+ issues: mergedIssues,
758
+ healthScores: primaryReport.healthScore,
759
+ aiAnalysis
760
+ };
761
+ console.log(JSON.stringify(jsonOutput, null, 2));
762
+ } else {
763
+ console.log("\n" + import_chalk.default.bold("\u2550".repeat(60)));
764
+ console.log(import_chalk.default.bold.cyan(` SEO AUDIT REPORT: ${url}`));
765
+ console.log(import_chalk.default.bold("\u2550".repeat(60)));
766
+ const score = primaryReport.healthScore?.overall || 0;
767
+ const scoreColor = score >= 80 ? import_chalk.default.green : score >= 60 ? import_chalk.default.yellow : import_chalk.default.red;
768
+ console.log(`
769
+ ${import_chalk.default.bold("Score:")} ${scoreColor.bold(score + "/100")}`);
770
+ console.log(` ${import_chalk.default.bold("Pages audited:")} ${successCount}`);
771
+ console.log(` ${import_chalk.default.bold("Issues found:")} ${mergedIssues.length}`);
772
+ console.log(` ${import_chalk.default.bold("Time:")} ${totalTime}ms`);
773
+ if (frameworkName) {
774
+ console.log(` ${import_chalk.default.bold("Framework:")} ${import_chalk.default.cyan(frameworkName)}`);
775
+ }
776
+ if (primaryReport.healthScore) {
777
+ console.log("\n" + import_chalk.default.bold(" Health Scores:"));
778
+ const hs = primaryReport.healthScore;
779
+ const bar = (score2) => {
780
+ const filled = Math.round(score2 / 5);
781
+ const empty = 20 - filled;
782
+ const color = score2 >= 80 ? import_chalk.default.green : score2 >= 60 ? import_chalk.default.yellow : import_chalk.default.red;
783
+ return color("\u2588".repeat(filled)) + import_chalk.default.gray("\u2591".repeat(empty));
784
+ };
785
+ console.log(` Crawlability: ${bar(hs.crawlability || 0)} ${hs.crawlability || 0}%`);
786
+ console.log(` On-Page: ${bar(hs.onPage || 0)} ${hs.onPage || 0}%`);
787
+ console.log(` Content: ${bar(hs.content || 0)} ${hs.content || 0}%`);
788
+ console.log(` Performance: ${bar(hs.performance || 0)} ${hs.performance || 0}%`);
789
+ console.log(` Security: ${bar(hs.security || 0)} ${hs.security || 0}%`);
790
+ }
791
+ const errors = mergedIssues.filter((i) => i.severity === "error");
792
+ const warnings = mergedIssues.filter((i) => i.severity === "warning");
793
+ const notices = mergedIssues.filter((i) => i.severity === "notice");
794
+ console.log("\n" + import_chalk.default.bold(" Issues by Severity:"));
795
+ console.log(` ${import_chalk.default.red("\u25CF")} Errors: ${errors.length}`);
796
+ console.log(` ${import_chalk.default.yellow("\u25CF")} Warnings: ${warnings.length}`);
797
+ console.log(` ${import_chalk.default.blue("\u25CF")} Notices: ${notices.length}`);
798
+ console.log("\n" + import_chalk.default.bold(" Top Issues:"));
799
+ const topIssues = [...errors, ...warnings, ...notices].slice(0, 10);
800
+ topIssues.forEach((issue, i) => {
801
+ const icon = issue.severity === "error" ? import_chalk.default.red("\u2717") : issue.severity === "warning" ? import_chalk.default.yellow("!") : import_chalk.default.blue("i");
802
+ console.log(` ${i + 1}. ${icon} ${issue.title}`);
803
+ if (issue.affectedUrls && issue.affectedUrls.length > 1) {
804
+ console.log(import_chalk.default.dim(` Affects ${issue.affectedUrls.length} pages`));
805
+ }
806
+ });
807
+ if (aiAnalysis) {
808
+ console.log("\n" + import_chalk.default.bold(" AI Analysis:"));
809
+ console.log(` Content Score: ${aiAnalysis.contentScore}/100`);
810
+ console.log("\n " + import_chalk.default.bold("Suggestions:"));
811
+ aiAnalysis.suggestions.forEach((s, i) => {
812
+ console.log(` ${i + 1}. ${s}`);
813
+ });
814
+ if (aiAnalysis.prioritizedFixes?.length > 0) {
815
+ console.log("\n " + import_chalk.default.bold("Priority Fixes:"));
816
+ aiAnalysis.prioritizedFixes.slice(0, 5).forEach((fix, i) => {
817
+ console.log(` ${i + 1}. [P${fix.priority}] ${fix.code}`);
818
+ console.log(import_chalk.default.dim(` ${fix.rationale}`));
819
+ });
820
+ }
821
+ }
822
+ console.log("\n" + import_chalk.default.bold("\u2550".repeat(60)));
823
+ if (loggedIn) {
824
+ const syncSpinner = (0, import_ora.default)("Syncing to cloud...").start();
825
+ const syncResult = await syncToCloud(url, primaryReport, mergedIssues, successCount, aiAnalysis);
826
+ if (syncResult) {
827
+ syncSpinner.succeed("Results synced to cloud");
828
+ console.log(import_chalk.default.cyan(`
829
+ View and manage issues in the web dashboard:`));
830
+ console.log(import_chalk.default.bold(` ${WEB_APP_URL}/dashboard/projects/${syncResult.projectId}
831
+ `));
832
+ } else {
833
+ syncSpinner.warn("Could not sync to cloud");
834
+ }
835
+ } else {
836
+ console.log(import_chalk.default.dim("\n Tip: Log in to sync results and manage issues in the web dashboard:"));
837
+ console.log(import_chalk.default.dim(" rankcli login\n"));
838
+ }
839
+ }
840
+ } catch (error) {
841
+ console.log(`
842
+ \u274C Audit failed: ${error instanceof Error ? error.message : "Unknown error"}
843
+ `);
844
+ }
845
+ }
846
+
847
+ // src/commands/apply.ts
848
+ var import_fs3 = require("fs");
849
+ var import_path3 = require("path");
850
+ var import_agent_runtime2 = require("@rankcli/agent-runtime");
851
+ var SEVERITY_ICON = {
852
+ critical: "\u274C",
853
+ warning: "\u26A0\uFE0F",
854
+ info: "\u2139\uFE0F"
855
+ };
856
+ function printFix(fix, index) {
857
+ const icon = SEVERITY_ICON[fix.issue.severity] || "\u2022";
858
+ console.log(`
859
+ ${index + 1}. ${icon} ${fix.issue.message}`);
860
+ console.log(` File: ${fix.file}`);
861
+ if (fix.skipped) {
862
+ console.log(` Status: SKIPPED - ${fix.skipReason}`);
863
+ } else {
864
+ console.log(` Explanation: ${fix.explanation}`);
865
+ if (fix.before) {
866
+ console.log(` Before: ${truncate(fix.before, 60)}`);
867
+ }
868
+ console.log(` After: ${truncate(fix.after, 100)}`);
869
+ }
870
+ }
871
+ function truncate(str, maxLen) {
872
+ const oneLine = str.replace(/\n/g, " ").trim();
873
+ if (oneLine.length <= maxLen) return oneLine;
874
+ return oneLine.substring(0, maxLen - 3) + "...";
875
+ }
876
+ async function apply(options) {
877
+ const cwd = process.cwd();
878
+ const configPath = (0, import_path3.join)(cwd, "seo-autopilot.config.json");
879
+ let config2 = null;
880
+ if ((0, import_fs3.existsSync)(configPath)) {
881
+ try {
882
+ config2 = JSON.parse((0, import_fs3.readFileSync)(configPath, "utf-8"));
883
+ } catch {
884
+ }
885
+ }
886
+ let url = options.url;
887
+ if (!url && config2?.url) {
888
+ url = config2.url;
889
+ }
890
+ if (!url) {
891
+ console.log("\n\u26A0\uFE0F Please provide a URL to analyze:");
892
+ console.log(" seo apply --url https://your-site.com");
893
+ console.log("\n Or set a URL in your seo-autopilot.config.json\n");
894
+ return;
895
+ }
896
+ try {
897
+ new URL(url);
898
+ } catch {
899
+ console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
900
+ return;
901
+ }
902
+ console.log("\n\u{1F680} SEO Autopilot - Apply Fixes");
903
+ console.log("\u2550".repeat(50));
904
+ console.log("\n\u{1F50D} Detecting framework...");
905
+ const frameworkResult = await (0, import_agent_runtime2.detectFramework)({ cwd });
906
+ const framework = frameworkResult.success ? frameworkResult.data : { name: "Unknown", metaPattern: "html-head" };
907
+ console.log(` Framework: ${framework.name}`);
908
+ console.log(`
909
+ \u{1F50D} Analyzing ${url}...`);
910
+ let analysis;
911
+ try {
912
+ analysis = await (0, import_agent_runtime2.runDirectAnalysis)(url);
913
+ } catch (error) {
914
+ console.log(`
915
+ \u274C Analysis failed: ${error instanceof Error ? error.message : "Unknown error"}
916
+ `);
917
+ return;
918
+ }
919
+ console.log(` Score: ${analysis.score}/100`);
920
+ console.log(` Issues found: ${analysis.issues.length}`);
921
+ if (analysis.issues.length === 0) {
922
+ console.log("\n\u2705 No issues found! Your site looks great.\n");
923
+ return;
924
+ }
925
+ console.log("\n\u{1F527} Generating fixes...");
926
+ const fixes = await (0, import_agent_runtime2.generateFixes)(analysis.issues, {
927
+ cwd,
928
+ url,
929
+ framework
930
+ });
931
+ const actionableFixes = fixes.filter((f) => !f.skipped);
932
+ const skippedFixes = fixes.filter((f) => f.skipped);
933
+ console.log(` Generated ${actionableFixes.length} actionable fix(es)`);
934
+ if (skippedFixes.length > 0) {
935
+ console.log(` ${skippedFixes.length} issue(s) require manual attention`);
936
+ }
937
+ console.log("\n" + "\u2500".repeat(50));
938
+ console.log("PROPOSED FIXES:");
939
+ console.log("\u2500".repeat(50));
940
+ for (let i = 0; i < fixes.length; i++) {
941
+ printFix(fixes[i], i);
942
+ }
943
+ if (options.dryRun) {
944
+ console.log("\n" + "\u2500".repeat(50));
945
+ console.log("\u{1F4CB} DRY RUN - No changes were made.");
946
+ console.log(" Remove --dry-run to apply these fixes.\n");
947
+ return;
948
+ }
949
+ if (actionableFixes.length === 0) {
950
+ console.log("\n\u26A0\uFE0F No automatic fixes available. Please address issues manually.\n");
951
+ return;
952
+ }
953
+ if (!options.auto) {
954
+ console.log("\n" + "\u2500".repeat(50));
955
+ console.log(`Ready to apply ${actionableFixes.length} fix(es).`);
956
+ console.log("Run with --auto to apply without confirmation.");
957
+ console.log("Run with --dry-run to preview changes.\n");
958
+ const proceed = process.env.SEO_AUTO_APPLY === "true";
959
+ if (!proceed) {
960
+ console.log("Set SEO_AUTO_APPLY=true or use --auto flag to apply fixes.\n");
961
+ return;
962
+ }
963
+ }
964
+ console.log("\n\u{1F527} Applying fixes...");
965
+ const { applied, skipped } = await (0, import_agent_runtime2.applyFixes)(fixes, { cwd, dryRun: false });
966
+ console.log("\n" + "\u2550".repeat(50));
967
+ console.log("RESULTS");
968
+ console.log("\u2550".repeat(50));
969
+ if (applied.length > 0) {
970
+ console.log(`
971
+ \u2705 Applied ${applied.length} fix(es):`);
972
+ for (const fix of applied) {
973
+ console.log(` - ${fix.file}: ${fix.issue.message}`);
974
+ }
975
+ }
976
+ if (skipped.length > 0) {
977
+ console.log(`
978
+ \u26A0\uFE0F Skipped ${skipped.length} fix(es):`);
979
+ for (const fix of skipped) {
980
+ console.log(` - ${fix.file}: ${fix.skipReason || "Manual fix required"}`);
981
+ }
982
+ }
983
+ if (config2 && !config2.url) {
984
+ config2.url = url;
985
+ (0, import_fs3.writeFileSync)(configPath, JSON.stringify(config2, null, 2));
986
+ console.log("\n\u{1F4BE} Updated config with URL.");
987
+ }
988
+ console.log("\n" + "\u2500".repeat(50));
989
+ console.log("NEXT STEPS:");
990
+ console.log("\u2500".repeat(50));
991
+ console.log("1. Review the changes:");
992
+ console.log(" git diff");
993
+ console.log("\n2. Commit the SEO improvements:");
994
+ console.log(' git add -A && git commit -m "feat(seo): add meta tags and structured data"');
995
+ console.log("\n3. Re-run the audit to verify:");
996
+ console.log(` seo audit --url ${url}`);
997
+ console.log("");
998
+ }
999
+
1000
+ // src/commands/keywords.ts
1001
+ var readline = __toESM(require("readline"));
1002
+ var import_agent_runtime3 = require("@rankcli/agent-runtime");
1003
+ async function keywords(options) {
1004
+ console.log("\n\u{1F511} SEO Autopilot - Keyword Research\n");
1005
+ console.log("This tool helps you find keywords you can actually rank for,");
1006
+ console.log("based on your site's current authority and competition level.\n");
1007
+ const rl = readline.createInterface({
1008
+ input: process.stdin,
1009
+ output: process.stdout
1010
+ });
1011
+ const ask = (question) => {
1012
+ return new Promise((resolve) => {
1013
+ rl.question(question, (answer) => {
1014
+ resolve(answer.trim());
1015
+ });
1016
+ });
1017
+ };
1018
+ const askChoice = async (question, choices) => {
1019
+ console.log(`
1020
+ ${question}`);
1021
+ choices.forEach((choice, i) => {
1022
+ console.log(` ${i + 1}. ${choice.label}`);
1023
+ console.log(` ${choice.description}`);
1024
+ });
1025
+ while (true) {
1026
+ const answer = await ask("\nEnter your choice (1-" + choices.length + "): ");
1027
+ const num = parseInt(answer, 10);
1028
+ if (num >= 1 && num <= choices.length) {
1029
+ return choices[num - 1].value;
1030
+ }
1031
+ console.log("Please enter a valid number.");
1032
+ }
1033
+ };
1034
+ try {
1035
+ let url = options.url;
1036
+ if (!url) {
1037
+ url = await ask("Enter your website URL: ");
1038
+ }
1039
+ try {
1040
+ const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
1041
+ url = parsed.href;
1042
+ } catch {
1043
+ console.log("\n\u274C Invalid URL format. Please provide a valid URL.\n");
1044
+ rl.close();
1045
+ return;
1046
+ }
1047
+ const domain = new URL(url).hostname;
1048
+ let seedKeywords = [];
1049
+ if (options.seed) {
1050
+ seedKeywords = options.seed.split(",").map((s) => s.trim());
1051
+ } else if (options.auto) {
1052
+ console.log("\n\u{1F50D} Auto-extracting seed keywords from your page...");
1053
+ seedKeywords = await (0, import_agent_runtime3.extractSeedKeywords)(url);
1054
+ if (seedKeywords.length > 0) {
1055
+ console.log(`Found: ${seedKeywords.join(", ")}`);
1056
+ }
1057
+ }
1058
+ if (seedKeywords.length === 0) {
1059
+ console.log("\n\u{1F4DD} What is your website/product about?");
1060
+ console.log(" (Enter 1-3 main topics or keywords, comma-separated)");
1061
+ const seedInput = await ask(" > ");
1062
+ seedKeywords = seedInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1063
+ if (seedKeywords.length === 0) {
1064
+ console.log("\n\u274C Please provide at least one seed keyword.\n");
1065
+ rl.close();
1066
+ return;
1067
+ }
1068
+ }
1069
+ console.log("\n\u{1F4CB} Let me ask a few questions to tailor recommendations to your site...\n");
1070
+ const siteProfile = {
1071
+ domain,
1072
+ domainAge: "new",
1073
+ backlinkCount: "none",
1074
+ businessGoal: "signups",
1075
+ contentCapacity: "medium",
1076
+ targetGeo: "us"
1077
+ };
1078
+ for (const q of import_agent_runtime3.SITE_PROFILE_QUESTIONS) {
1079
+ const answer = await askChoice(q.question, q.options);
1080
+ siteProfile[q.id] = answer;
1081
+ }
1082
+ rl.close();
1083
+ console.log("\n" + "\u2500".repeat(60));
1084
+ console.log("Starting keyword research with your profile:");
1085
+ console.log(` Domain: ${domain}`);
1086
+ console.log(` Seeds: ${seedKeywords.join(", ")}`);
1087
+ console.log(` Age: ${siteProfile.domainAge}`);
1088
+ console.log(` Backlinks: ${siteProfile.backlinkCount}`);
1089
+ console.log(` Goal: ${siteProfile.businessGoal}`);
1090
+ console.log("\u2500".repeat(60));
1091
+ const researchOptions = {
1092
+ seedKeywords,
1093
+ siteProfile,
1094
+ url,
1095
+ maxKeywords: 100
1096
+ };
1097
+ const result = await (0, import_agent_runtime3.runKeywordResearch)(researchOptions);
1098
+ console.log((0, import_agent_runtime3.formatKeywordReport)(result));
1099
+ const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1100
+ if (allKeywords.length > 5) {
1101
+ console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1102
+ const topicResult = (0, import_agent_runtime3.groupKeywordsByTopic)(allKeywords);
1103
+ console.log((0, import_agent_runtime3.formatTopicReport)(topicResult));
1104
+ }
1105
+ if (result.quickWins.length > 0) {
1106
+ console.log('\n\u{1F4A1} TIP: Run "seo apply --url ' + url + '" to apply keyword optimizations');
1107
+ console.log('\u{1F4A1} TIP: Run "seo keywords --competitor <domain>" to find competitor keyword gaps');
1108
+ }
1109
+ } catch (error) {
1110
+ console.log(`
1111
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1112
+ `);
1113
+ rl.close();
1114
+ }
1115
+ }
1116
+ async function keywordsQuick(url, providedSeeds) {
1117
+ console.log("\n\u{1F680} Quick Keyword Research Mode\n");
1118
+ try {
1119
+ const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
1120
+ url = parsed.href;
1121
+ let seedKeywords = [];
1122
+ if (providedSeeds) {
1123
+ seedKeywords = providedSeeds.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1124
+ console.log(`Using provided seeds: ${seedKeywords.join(", ")}
1125
+ `);
1126
+ } else {
1127
+ console.log("\u{1F50D} Extracting keywords from your page...");
1128
+ seedKeywords = await (0, import_agent_runtime3.extractSeedKeywords)(url);
1129
+ if (seedKeywords.length === 0) {
1130
+ console.log("\u274C Could not extract keywords. Please use interactive mode.\n");
1131
+ return;
1132
+ }
1133
+ console.log(`Found seeds: ${seedKeywords.join(", ")}
1134
+ `);
1135
+ }
1136
+ const siteProfile = {
1137
+ domain: parsed.hostname,
1138
+ domainAge: "new",
1139
+ backlinkCount: "few",
1140
+ businessGoal: "signups",
1141
+ contentCapacity: "medium",
1142
+ targetGeo: "us"
1143
+ };
1144
+ const result = await (0, import_agent_runtime3.runKeywordResearch)({
1145
+ seedKeywords,
1146
+ siteProfile,
1147
+ url,
1148
+ maxKeywords: 50
1149
+ });
1150
+ console.log((0, import_agent_runtime3.formatKeywordReport)(result));
1151
+ const allKeywords = [...result.quickWins, ...result.mediumTerm, ...result.longTerm];
1152
+ if (allKeywords.length > 5) {
1153
+ console.log("\n\u{1F4DA} Grouping keywords by topic...\n");
1154
+ const topicResult = (0, import_agent_runtime3.groupKeywordsByTopic)(allKeywords);
1155
+ console.log((0, import_agent_runtime3.formatTopicReport)(topicResult));
1156
+ }
1157
+ } catch (error) {
1158
+ console.log(`
1159
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1160
+ `);
1161
+ }
1162
+ }
1163
+ var SUPABASE_URL2 = "https://eqzlmjbvrtrglknphdai.supabase.co";
1164
+ async function keywordsAI(url, options = {}) {
1165
+ console.log("\n\u{1F916} AI-Powered Keyword Research\n");
1166
+ console.log("This uses GPT-4 to analyze your site and generate keyword recommendations");
1167
+ console.log("with competitive analysis and free tool ideas.\n");
1168
+ const openaiApiKey = options.openaiApiKey || process.env.OPENAI_API_KEY;
1169
+ try {
1170
+ const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
1171
+ url = parsed.href;
1172
+ console.log(`\u{1F4CD} Analyzing: ${url}`);
1173
+ console.log("");
1174
+ if (!options.forceLocal) {
1175
+ console.log("\u{1F517} Connecting to RankCLI cloud...");
1176
+ try {
1177
+ const response = await fetch(`${SUPABASE_URL2}/functions/v1/ai-keywords`, {
1178
+ method: "POST",
1179
+ headers: {
1180
+ "Content-Type": "application/json"
1181
+ // Auth header would go here if user is logged in
1182
+ },
1183
+ body: JSON.stringify({
1184
+ url,
1185
+ mode: "analyze"
1186
+ })
1187
+ });
1188
+ if (response.ok) {
1189
+ const result2 = await response.json();
1190
+ if (result2.tier === "free" && result2.teaser) {
1191
+ console.log("\n" + "\u2500".repeat(60));
1192
+ console.log("\u{1F512} FREE TIER - Limited Preview");
1193
+ console.log("\u2500".repeat(60));
1194
+ console.log(`Found ${result2.teaser.totalKeywordsFound} keywords`);
1195
+ console.log(`Top keywords: ${result2.teaser.topKeywords.join(", ")}`);
1196
+ console.log(`
1197
+ ${result2.teaser.upgradeMessage}`);
1198
+ console.log('\n\u{1F4A1} Run "rankcli login" to access your paid plan features.\n');
1199
+ return;
1200
+ }
1201
+ displayAIKeywordResults(result2);
1202
+ return;
1203
+ } else {
1204
+ const errorText = await response.text();
1205
+ console.log(`\u26A0\uFE0F Cloud request failed, running locally...
1206
+ `);
1207
+ }
1208
+ } catch (err) {
1209
+ console.log("\u26A0\uFE0F Cloud unavailable, running locally...\n");
1210
+ }
1211
+ }
1212
+ if (!openaiApiKey) {
1213
+ console.log("\u274C OpenAI API key required for local processing.");
1214
+ console.log(' Set OPENAI_API_KEY environment variable, or use "rankcli login" for cloud processing.\n');
1215
+ return;
1216
+ }
1217
+ console.log("\u{1F52C} Running local AI analysis (this may take a minute)...\n");
1218
+ const result = await (0, import_agent_runtime3.runAIKeywordResearch)({
1219
+ url,
1220
+ tier: "solo",
1221
+ openaiApiKey,
1222
+ enableEnhancedToolIdeas: true,
1223
+ competitiveSearchKeys: {
1224
+ braveApiKey: process.env.BRAVE_API_KEY,
1225
+ serperApiKey: process.env.SERPER_API_KEY,
1226
+ githubToken: process.env.GITHUB_TOKEN
1227
+ }
1228
+ });
1229
+ displayAIKeywordResults(result);
1230
+ } catch (error) {
1231
+ console.log(`
1232
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1233
+ `);
1234
+ }
1235
+ }
1236
+ function displayAIKeywordResults(result) {
1237
+ if (result.siteSummary) {
1238
+ console.log("\u2500".repeat(60));
1239
+ console.log("\u{1F4CB} SITE ANALYSIS");
1240
+ console.log("\u2500".repeat(60));
1241
+ console.log(`Product: ${result.siteSummary.productName || "Unknown"}`);
1242
+ console.log(`Industry: ${result.siteSummary.industry || "Unknown"}`);
1243
+ console.log(`Target: ${result.siteSummary.targetAudience || "Unknown"}`);
1244
+ if (result.siteSummary.tagline) {
1245
+ console.log(`Tagline: ${result.siteSummary.tagline}`);
1246
+ }
1247
+ console.log("");
1248
+ }
1249
+ if (result.uncertainty) {
1250
+ const confidence = Math.round(result.uncertainty.overallConfidence * 100);
1251
+ const emoji = confidence >= 70 ? "\u2705" : confidence >= 50 ? "\u26A0\uFE0F" : "\u274C";
1252
+ console.log(`${emoji} Confidence: ${confidence}%`);
1253
+ if (result.uncertainty.explanation) {
1254
+ console.log(` ${result.uncertainty.explanation}`);
1255
+ }
1256
+ console.log("");
1257
+ }
1258
+ if (result.recommendations && result.recommendations.length > 0) {
1259
+ console.log("\u2500".repeat(60));
1260
+ console.log("\u{1F3AF} KEYWORD RECOMMENDATIONS");
1261
+ console.log("\u2500".repeat(60));
1262
+ for (const rec of result.recommendations.slice(0, 15)) {
1263
+ const actionEmoji = {
1264
+ "create-page": "\u{1F4C4}",
1265
+ "build-tool": "\u{1F6E0}\uFE0F",
1266
+ "optimize-existing": "\u{1F4C8}",
1267
+ "needs-input": "\u2753",
1268
+ "skip": "\u23ED\uFE0F"
1269
+ }[rec.action] || "\u2022";
1270
+ console.log(`
1271
+ ${actionEmoji} ${rec.keyword}`);
1272
+ console.log(` Action: ${rec.action.replace("-", " ")}`);
1273
+ console.log(` Volume: ${rec.searchVolume || "unknown"} | Difficulty: ${rec.difficulty || "unknown"}`);
1274
+ console.log(` Impact: ${rec.impact || "unknown"} | Effort: ${rec.effort || "unknown"}`);
1275
+ if (rec.rationale) {
1276
+ console.log(` ${rec.rationale}`);
1277
+ }
1278
+ }
1279
+ console.log("");
1280
+ }
1281
+ if (result.freeToolIdeas && result.freeToolIdeas.length > 0) {
1282
+ console.log("\u2500".repeat(60));
1283
+ console.log("\u{1F6E0}\uFE0F FREE TOOL IDEAS (Engineering as Marketing)");
1284
+ console.log("\u2500".repeat(60));
1285
+ for (const tool of result.freeToolIdeas.slice(0, 5)) {
1286
+ console.log(`
1287
+ \u{1F4A1} ${tool.toolName || tool.toolName}`);
1288
+ console.log(` ${tool.description || tool.toolDescription || ""}`);
1289
+ if (tool.inputFormat && tool.outputFormat) {
1290
+ console.log(` \u{1F4E5} Input: ${tool.inputFormat}`);
1291
+ console.log(` \u{1F4E4} Output: ${tool.outputFormat}`);
1292
+ }
1293
+ if (tool.productTieIn) {
1294
+ console.log(` \u{1F517} Tie-in: ${tool.productTieIn}`);
1295
+ }
1296
+ if (tool.competitorAnalysis) {
1297
+ console.log(` \u{1F3C6} Existing: ${tool.competitorAnalysis.existingTools?.join(", ") || "None found"}`);
1298
+ console.log(` \u2728 Advantage: ${tool.competitorAnalysis.ourAdvantage || "N/A"}`);
1299
+ }
1300
+ if (tool.implementationHints) {
1301
+ console.log(` \u23F1\uFE0F Est. time: ${tool.implementationHints.estimatedHours || "Unknown"} hours`);
1302
+ if (tool.implementationHints.suggestedLibraries?.length > 0) {
1303
+ console.log(` \u{1F4E6} Libraries: ${tool.implementationHints.suggestedLibraries.join(", ")}`);
1304
+ }
1305
+ }
1306
+ if (tool.feasibilityScore) {
1307
+ const score = typeof tool.feasibilityScore === "object" ? tool.feasibilityScore.overallScore : tool.feasibilityScore;
1308
+ console.log(` \u{1F4CA} Feasibility: ${score}/5`);
1309
+ }
1310
+ console.log(` \u{1F3AF} Keyword: ${tool.targetKeyword || tool.keyword}`);
1311
+ console.log(` \u{1F4C8} Volume: ${tool.searchVolume || "unknown"} | Difficulty: ${tool.difficulty || "unknown"}`);
1312
+ }
1313
+ console.log("");
1314
+ }
1315
+ console.log("\u2500".repeat(60));
1316
+ console.log("\u{1F4A1} NEXT STEPS");
1317
+ console.log("\u2500".repeat(60));
1318
+ console.log("1. Start with quick wins (low difficulty, high impact keywords)");
1319
+ console.log("2. Build 1-2 free tools to capture search traffic");
1320
+ console.log('3. Create content pages targeting "Create Page" keywords');
1321
+ console.log("4. Re-run with --wizard to improve accuracy if needed\n");
1322
+ }
1323
+ async function keywordsCompetitor(yourUrl, seedKeywords, competitors) {
1324
+ console.log("\n\u{1F50D} Competitor Keyword Gap Analysis (SpyFu Kombat-style)\n");
1325
+ try {
1326
+ const parsed = new URL(yourUrl.startsWith("http") ? yourUrl : `https://${yourUrl}`);
1327
+ const yourDomain = parsed.hostname.replace(/^www\./, "");
1328
+ console.log(`Your domain: ${yourDomain}`);
1329
+ console.log(`Seed keywords: ${seedKeywords.join(", ")}`);
1330
+ if (competitors && competitors.length > 0) {
1331
+ console.log(`Competitors: ${competitors.join(", ")}`);
1332
+ } else {
1333
+ console.log("Competitors: Auto-discovering from SERP...");
1334
+ }
1335
+ console.log("");
1336
+ console.log("Analyzing competitor keywords (this may take a moment)...\n");
1337
+ const result = await (0, import_agent_runtime3.discoverCompetitorKeywords)(
1338
+ yourDomain,
1339
+ seedKeywords,
1340
+ competitors
1341
+ );
1342
+ console.log((0, import_agent_runtime3.formatCompetitorReport)(result));
1343
+ if (result.missingKeywords.length > 0) {
1344
+ console.log("\n\u{1F3AF} ACTION ITEMS:");
1345
+ console.log("\u2500".repeat(60));
1346
+ console.log(`1. Create content targeting these ${Math.min(5, result.missingKeywords.length)} high-priority gaps:`);
1347
+ for (const kw of result.missingKeywords.slice(0, 5)) {
1348
+ console.log(` \u2022 "${kw.keyword}"`);
1349
+ }
1350
+ console.log("\n2. Optimize existing pages for core keywords your competitors all rank for");
1351
+ console.log("3. Protect your unique keyword advantages with quality content\n");
1352
+ }
1353
+ } catch (error) {
1354
+ console.log(`
1355
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1356
+ `);
1357
+ }
1358
+ }
1359
+
1360
+ // src/commands/content.ts
1361
+ var import_agent_runtime4 = require("@rankcli/agent-runtime");
1362
+ async function analyzeContent(options) {
1363
+ console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
1364
+ const mode = options.mode || "full";
1365
+ if (mode === "headline" || options.headline) {
1366
+ const headline = options.headline || "";
1367
+ if (!headline) {
1368
+ console.log('\u274C Please provide a headline with --headline "Your Headline Here"');
1369
+ return;
1370
+ }
1371
+ const analysis = (0, import_agent_runtime4.analyzeHeadline)(headline);
1372
+ console.log((0, import_agent_runtime4.formatHeadlineReport)(analysis));
1373
+ if (options.keyword) {
1374
+ console.log("\n\u{1F4CB} HEADLINE VARIATIONS");
1375
+ console.log("\u2500".repeat(60));
1376
+ const variations = (0, import_agent_runtime4.generateHeadlineVariations)(options.keyword, [options.keyword]);
1377
+ for (const v of variations.slice(0, 5)) {
1378
+ console.log(` \u2022 ${v}`);
1379
+ }
1380
+ console.log("");
1381
+ }
1382
+ return;
1383
+ }
1384
+ if (options.url) {
1385
+ console.log(`Fetching content from ${options.url}...`);
1386
+ try {
1387
+ const result = await (0, import_agent_runtime4.crawlUrl)(options.url);
1388
+ if (!result.html) {
1389
+ console.log("\u274C Could not fetch content from URL");
1390
+ return;
1391
+ }
1392
+ const title = result.meta?.title || "";
1393
+ const h1 = result.headings?.find((h) => h.level === 1)?.text || "";
1394
+ const headings = result.headings?.map((h) => h.text) || [];
1395
+ const bodyText = extractBodyText(result.html);
1396
+ const paragraphs = bodyText.split(/\n\n+/);
1397
+ console.log(`
1398
+ Analyzing: "${title || options.url}"
1399
+ `);
1400
+ if (mode === "full" || mode === "readability") {
1401
+ const readability = (0, import_agent_runtime4.analyzeReadability)(bodyText);
1402
+ console.log((0, import_agent_runtime4.formatReadabilityReport)(readability));
1403
+ }
1404
+ if (mode === "full" && title) {
1405
+ const headlineAnalysis = (0, import_agent_runtime4.analyzeHeadline)(title);
1406
+ console.log((0, import_agent_runtime4.formatHeadlineReport)(headlineAnalysis));
1407
+ }
1408
+ if (options.keyword && (mode === "full" || mode === "density")) {
1409
+ const densityAnalysis = (0, import_agent_runtime4.analyzeKeywordDensity)(options.keyword, {
1410
+ title,
1411
+ h1,
1412
+ headings,
1413
+ body: bodyText,
1414
+ firstParagraph: paragraphs[0],
1415
+ lastParagraph: paragraphs[paragraphs.length - 1]
1416
+ });
1417
+ console.log((0, import_agent_runtime4.formatKeywordDensityReport)(densityAnalysis));
1418
+ }
1419
+ if (options.keyword && (mode === "full" || mode === "snippet")) {
1420
+ const snippetAnalysis = (0, import_agent_runtime4.analyzeFeaturedSnippetPotential)(
1421
+ options.keyword,
1422
+ bodyText,
1423
+ headings
1424
+ );
1425
+ console.log((0, import_agent_runtime4.formatFeaturedSnippetReport)(snippetAnalysis));
1426
+ }
1427
+ if (options.keyword) {
1428
+ const intentAnalysis = (0, import_agent_runtime4.classifyIntent)(options.keyword);
1429
+ console.log((0, import_agent_runtime4.formatIntentReport)(options.keyword, intentAnalysis));
1430
+ }
1431
+ console.log("\n\u{1F4CA} CONTENT SUMMARY");
1432
+ console.log("\u2500".repeat(60));
1433
+ console.log(` Title: ${title || "(none)"}`);
1434
+ console.log(` H1: ${h1 || "(none)"}`);
1435
+ console.log(` Word Count: ${bodyText.split(/\s+/).length}`);
1436
+ console.log(` Headings: ${headings.length}`);
1437
+ console.log("");
1438
+ } catch (error) {
1439
+ console.log(`
1440
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1441
+ `);
1442
+ }
1443
+ } else {
1444
+ console.log("Usage:");
1445
+ console.log(" seo content -u <url> Full content analysis");
1446
+ console.log(' seo content -u <url> -k "keyword" Analyze with target keyword');
1447
+ console.log(' seo content --headline "Your Title" Analyze headline only');
1448
+ console.log(" seo content -u <url> --mode readability Readability only");
1449
+ console.log(' seo content -u <url> -k "kw" --mode density Keyword density only');
1450
+ console.log(' seo content -u <url> -k "kw" --mode snippet Featured snippet analysis');
1451
+ console.log("");
1452
+ }
1453
+ }
1454
+ async function analyzeIntent(keyword) {
1455
+ console.log("\n\u{1F50D} Search Intent Analysis\n");
1456
+ const analysis = (0, import_agent_runtime4.classifyIntent)(keyword);
1457
+ console.log((0, import_agent_runtime4.formatIntentReport)(keyword, analysis));
1458
+ }
1459
+ function extractBodyText(html) {
1460
+ let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
1461
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
1462
+ text = text.replace(/<[^>]+>/g, " ");
1463
+ text = text.replace(/&nbsp;/g, " ");
1464
+ text = text.replace(/&amp;/g, "&");
1465
+ text = text.replace(/&lt;/g, "<");
1466
+ text = text.replace(/&gt;/g, ">");
1467
+ text = text.replace(/&quot;/g, '"');
1468
+ text = text.replace(/\s+/g, " ").trim();
1469
+ return text;
1470
+ }
1471
+
1472
+ // src/commands/setup.ts
1473
+ var readline2 = __toESM(require("readline"));
1474
+ var fs = __toESM(require("fs"));
1475
+ var path = __toESM(require("path"));
1476
+ var import_agent_runtime5 = require("@rankcli/agent-runtime");
1477
+ async function setup(options) {
1478
+ console.log("\n\u{1F527} SEO Autopilot - Setup\n");
1479
+ const projectPath = process.cwd();
1480
+ if (!fs.existsSync(path.join(projectPath, "package.json")) && !fs.existsSync(path.join(projectPath, "index.html"))) {
1481
+ console.log("\u26A0\uFE0F No package.json or index.html found. Are you in a web project directory?\n");
1482
+ }
1483
+ if (options.all || !options.ga4 && !options.gsc && !options.githubAction) {
1484
+ await interactiveSetup(projectPath);
1485
+ return;
1486
+ }
1487
+ if (options.ga4) {
1488
+ await setupGA4(projectPath, options.ga4);
1489
+ }
1490
+ if (options.gsc) {
1491
+ await setupGSC(projectPath, options.gsc);
1492
+ }
1493
+ if (options.githubAction) {
1494
+ await setupGitHubAction(projectPath, options.schedule || "weekly");
1495
+ }
1496
+ }
1497
+ async function interactiveSetup(projectPath) {
1498
+ const rl = readline2.createInterface({
1499
+ input: process.stdin,
1500
+ output: process.stdout
1501
+ });
1502
+ const ask = (question) => {
1503
+ return new Promise((resolve) => {
1504
+ rl.question(question, (answer) => {
1505
+ resolve(answer.trim());
1506
+ });
1507
+ });
1508
+ };
1509
+ try {
1510
+ console.log("This wizard will help you set up SEO tracking for your project.\n");
1511
+ console.log("\u2500".repeat(60));
1512
+ console.log("\u{1F4CA} STEP 1: Google Analytics 4\n");
1513
+ console.log("GA4 tracks visitor behavior on your site.");
1514
+ console.log("Get your Measurement ID from: https://analytics.google.com");
1515
+ console.log("Go to: Admin > Data Streams > Your Stream > Measurement ID\n");
1516
+ const ga4Id = await ask("Enter GA4 Measurement ID (G-XXXXXXXXXX) or press Enter to skip: ");
1517
+ if (ga4Id && ga4Id.startsWith("G-")) {
1518
+ await setupGA4(projectPath, ga4Id);
1519
+ } else if (ga4Id) {
1520
+ console.log('\u26A0\uFE0F Invalid format. GA4 IDs start with "G-". Skipping.\n');
1521
+ } else {
1522
+ console.log("Skipping GA4 setup.\n");
1523
+ }
1524
+ console.log("\u2500".repeat(60));
1525
+ console.log("\u{1F50D} STEP 2: Google Search Console\n");
1526
+ console.log("GSC shows how Google sees your site and what queries you rank for.");
1527
+ console.log("Get your verification code from: https://search.google.com/search-console");
1528
+ console.log("Go to: Settings > Ownership verification > HTML tag\n");
1529
+ const gscCode = await ask("Enter GSC verification code or press Enter to skip: ");
1530
+ if (gscCode && gscCode.length > 10) {
1531
+ await setupGSC(projectPath, gscCode);
1532
+ } else if (gscCode) {
1533
+ console.log("\u26A0\uFE0F Code seems too short. Skipping.\n");
1534
+ } else {
1535
+ console.log("Skipping GSC verification.\n");
1536
+ }
1537
+ console.log("\u2500".repeat(60));
1538
+ console.log("\u{1F916} STEP 3: Automated SEO Monitoring\n");
1539
+ console.log("A GitHub Action can automatically check your site's SEO on a schedule.");
1540
+ console.log("It creates issues with reports and can auto-fix some problems.\n");
1541
+ const setupAction = await ask("Set up GitHub Action? (y/n): ");
1542
+ if (setupAction.toLowerCase() === "y" || setupAction.toLowerCase() === "yes") {
1543
+ console.log("\nHow often should the check run?");
1544
+ console.log(" 1. Weekly (recommended for most sites)");
1545
+ console.log(" 2. Daily (for high-traffic sites)");
1546
+ console.log(" 3. Monthly (for low-traffic sites)\n");
1547
+ const scheduleChoice = await ask("Enter choice (1-3): ");
1548
+ const scheduleMap = {
1549
+ "1": "weekly",
1550
+ "2": "daily",
1551
+ "3": "monthly"
1552
+ };
1553
+ const schedule = scheduleMap[scheduleChoice] || "weekly";
1554
+ const siteUrl = await ask("Enter your site URL (https://example.com): ");
1555
+ if (siteUrl) {
1556
+ await setupGitHubAction(projectPath, schedule, siteUrl);
1557
+ }
1558
+ } else {
1559
+ console.log("Skipping GitHub Action setup.\n");
1560
+ }
1561
+ console.log("\u2500".repeat(60));
1562
+ console.log("\u2705 Setup complete!\n");
1563
+ console.log("Next steps:");
1564
+ console.log(" 1. Commit the changes to your repository");
1565
+ console.log(" 2. If using GitHub Actions, add required secrets (see .github/SEO_SETUP.md)");
1566
+ console.log(" 3. Deploy your site to apply tracking code changes");
1567
+ console.log(" 4. Verify GSC ownership and GA4 data collection\n");
1568
+ rl.close();
1569
+ } catch (error) {
1570
+ console.log(`
1571
+ \u274C Error: ${error instanceof Error ? error.message : "Unknown error"}
1572
+ `);
1573
+ rl.close();
1574
+ }
1575
+ }
1576
+ async function setupGA4(projectPath, measurementId) {
1577
+ console.log(`Setting up Google Analytics 4 (${measurementId})...`);
1578
+ const result = await (0, import_agent_runtime5.injectGA4)(projectPath, { measurementId });
1579
+ if (result.success) {
1580
+ console.log(`\u2705 ${result.message}`);
1581
+ if (result.file) {
1582
+ console.log(` Modified: ${result.file}`);
1583
+ }
1584
+ } else {
1585
+ console.log(`\u26A0\uFE0F ${result.message}`);
1586
+ if (result.code) {
1587
+ console.log("\nManually add this code to your HTML <head>:\n");
1588
+ console.log(result.code);
1589
+ }
1590
+ }
1591
+ const envExamplePath = path.join(projectPath, ".env.example");
1592
+ const envContent = (0, import_agent_runtime5.generateGA4EnvTemplate)();
1593
+ if (fs.existsSync(envExamplePath)) {
1594
+ const existing = fs.readFileSync(envExamplePath, "utf-8");
1595
+ if (!existing.includes("GA4_MEASUREMENT_ID")) {
1596
+ fs.appendFileSync(envExamplePath, "\n" + envContent);
1597
+ console.log(" Updated: .env.example");
1598
+ }
1599
+ } else {
1600
+ fs.writeFileSync(envExamplePath, envContent);
1601
+ console.log(" Created: .env.example");
1602
+ }
1603
+ console.log("");
1604
+ }
1605
+ async function setupGSC(projectPath, verificationCode) {
1606
+ console.log("Setting up Google Search Console verification...");
1607
+ const result = await (0, import_agent_runtime5.injectGSCVerification)(projectPath, verificationCode);
1608
+ if (result.success) {
1609
+ console.log(`\u2705 ${result.message}`);
1610
+ if (result.file) {
1611
+ console.log(` Modified: ${result.file}`);
1612
+ }
1613
+ } else {
1614
+ console.log(`\u26A0\uFE0F ${result.message}`);
1615
+ }
1616
+ console.log("");
1617
+ }
1618
+ async function setupGitHubAction(projectPath, schedule, siteUrl) {
1619
+ console.log(`Setting up GitHub Action (${schedule} schedule)...`);
1620
+ const config2 = {
1621
+ schedule,
1622
+ siteUrl: siteUrl || "https://example.com",
1623
+ features: {
1624
+ audit: true,
1625
+ tracking: true,
1626
+ autoFix: true,
1627
+ createIssues: true,
1628
+ createPRs: true
1629
+ }
1630
+ };
1631
+ const { files, instructions } = (0, import_agent_runtime5.writeGitHubActionFiles)(projectPath, config2);
1632
+ console.log("\u2705 GitHub Action files created:");
1633
+ for (const file of files) {
1634
+ console.log(` - ${path.relative(projectPath, file)}`);
1635
+ }
1636
+ console.log(instructions);
1637
+ }
1638
+ function showGSCInstructions() {
1639
+ console.log((0, import_agent_runtime5.getGSCSetupInstructions)());
1640
+ }
1641
+
1642
+ // src/commands/auth.ts
1643
+ var import_chalk2 = __toESM(require("chalk"));
1644
+ var import_ora2 = __toESM(require("ora"));
1645
+ var import_inquirer = __toESM(require("inquirer"));
1646
+ var import_open = __toESM(require("open"));
1647
+ async function login(options) {
1648
+ if (isLoggedIn()) {
1649
+ const { email } = getUserInfo();
1650
+ console.log(import_chalk2.default.yellow(`Already logged in as ${email}`));
1651
+ console.log(import_chalk2.default.dim('Run "rankcli logout" to sign out first.'));
1652
+ return;
1653
+ }
1654
+ const spinner = (0, import_ora2.default)();
1655
+ try {
1656
+ if (options.token) {
1657
+ const apiKey = options.token;
1658
+ if (!apiKey.startsWith("rankcli_")) {
1659
+ console.log(import_chalk2.default.red('Invalid API key format. Keys should start with "rankcli_"'));
1660
+ return;
1661
+ }
1662
+ spinner.start("Validating API key...");
1663
+ const result = await validateApiKey(apiKey);
1664
+ if (!result.valid) {
1665
+ spinner.fail("Invalid API key");
1666
+ console.log(import_chalk2.default.red(`
1667
+ ${result.error}`));
1668
+ return;
1669
+ }
1670
+ saveApiKey(apiKey);
1671
+ if (result.subscription) {
1672
+ saveSubscription({
1673
+ planId: result.subscription.planId,
1674
+ planName: result.subscription.planName,
1675
+ sitesLimit: result.subscription.sitesLimit
1676
+ });
1677
+ }
1678
+ spinner.succeed(`Authenticated via API key`);
1679
+ console.log(import_chalk2.default.dim(`
1680
+ Email: ${result.user?.email}`));
1681
+ console.log(import_chalk2.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
1682
+ return;
1683
+ }
1684
+ if (options.browser) {
1685
+ console.log(import_chalk2.default.cyan("\nOpening browser for login..."));
1686
+ console.log(import_chalk2.default.dim("Complete the login in your browser, then return here.\n"));
1687
+ const loginUrl = `${WEB_APP_URL}/login?cli=true`;
1688
+ await (0, import_open.default)(loginUrl);
1689
+ const { token } = await import_inquirer.default.prompt([
1690
+ {
1691
+ type: "password",
1692
+ name: "token",
1693
+ message: "Paste your CLI token from the browser:",
1694
+ mask: "*"
1695
+ }
1696
+ ]);
1697
+ spinner.start("Verifying token...");
1698
+ const supabase = getSupabaseClient();
1699
+ const { data, error } = await supabase.auth.getUser(token);
1700
+ if (error || !data.user) {
1701
+ spinner.fail("Invalid token");
1702
+ console.log(import_chalk2.default.red("The token is invalid or expired. Please try again."));
1703
+ return;
1704
+ }
1705
+ const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
1706
+ access_token: token,
1707
+ refresh_token: ""
1708
+ // Will need to handle this differently
1709
+ });
1710
+ if (sessionError || !sessionData.session) {
1711
+ spinner.fail("Failed to establish session");
1712
+ return;
1713
+ }
1714
+ saveSession(sessionData.session);
1715
+ await fetchAndSaveSubscription();
1716
+ spinner.succeed(`Logged in as ${import_chalk2.default.green(data.user.email)}`);
1717
+ } else {
1718
+ let email = options.email;
1719
+ if (!email) {
1720
+ const answers = await import_inquirer.default.prompt([
1721
+ {
1722
+ type: "input",
1723
+ name: "email",
1724
+ message: "Email:",
1725
+ validate: (input) => {
1726
+ if (!input.includes("@")) return "Please enter a valid email";
1727
+ return true;
1728
+ }
1729
+ }
1730
+ ]);
1731
+ email = answers.email;
1732
+ }
1733
+ const { password } = await import_inquirer.default.prompt([
1734
+ {
1735
+ type: "password",
1736
+ name: "password",
1737
+ message: "Password:",
1738
+ mask: "*"
1739
+ }
1740
+ ]);
1741
+ spinner.start("Signing in...");
1742
+ const supabase = getSupabaseClient();
1743
+ const { data, error } = await supabase.auth.signInWithPassword({
1744
+ email,
1745
+ password
1746
+ });
1747
+ if (error) {
1748
+ spinner.fail("Login failed");
1749
+ console.log(import_chalk2.default.red(`
1750
+ ${error.message}`));
1751
+ if (error.message.includes("Invalid login")) {
1752
+ console.log(import_chalk2.default.dim("\nDon't have an account? Sign up at:"));
1753
+ console.log(import_chalk2.default.cyan(` ${WEB_APP_URL}/signup`));
1754
+ }
1755
+ return;
1756
+ }
1757
+ if (!data.session) {
1758
+ spinner.fail("No session returned");
1759
+ return;
1760
+ }
1761
+ saveSession(data.session);
1762
+ await fetchAndSaveSubscription();
1763
+ spinner.succeed(`Logged in as ${import_chalk2.default.green(data.user?.email)}`);
1764
+ const { planName, sitesLimit } = getUserInfo();
1765
+ console.log(import_chalk2.default.dim(`
1766
+ Plan: ${planName} (${sitesLimit} site${sitesLimit !== 1 ? "s" : ""})`));
1767
+ }
1768
+ } catch (err) {
1769
+ spinner.fail("Login failed");
1770
+ console.error(import_chalk2.default.red(err instanceof Error ? err.message : "Unknown error"));
1771
+ }
1772
+ }
1773
+ async function logout() {
1774
+ if (!isLoggedIn()) {
1775
+ console.log(import_chalk2.default.yellow("Not logged in"));
1776
+ return;
1777
+ }
1778
+ const { email } = getUserInfo();
1779
+ const spinner = (0, import_ora2.default)("Signing out...").start();
1780
+ try {
1781
+ const supabase = getSupabaseClient();
1782
+ await supabase.auth.signOut();
1783
+ clearCredentials();
1784
+ spinner.succeed(`Logged out from ${import_chalk2.default.dim(email)}`);
1785
+ } catch (err) {
1786
+ spinner.fail("Logout failed");
1787
+ console.error(import_chalk2.default.red(err instanceof Error ? err.message : "Unknown error"));
1788
+ }
1789
+ }
1790
+ async function whoami() {
1791
+ if (!isLoggedIn()) {
1792
+ console.log(import_chalk2.default.yellow("Not logged in"));
1793
+ console.log(import_chalk2.default.dim('\nRun "rankcli login" to sign in.'));
1794
+ console.log(import_chalk2.default.dim(`Or sign up at: ${WEB_APP_URL}/signup`));
1795
+ return;
1796
+ }
1797
+ const spinner = (0, import_ora2.default)("Checking account...").start();
1798
+ try {
1799
+ if (isApiKeyAuth()) {
1800
+ const apiKey = getApiKey();
1801
+ if (apiKey) {
1802
+ const result = await validateApiKey(apiKey);
1803
+ if (result.valid && result.subscription) {
1804
+ saveSubscription({
1805
+ planId: result.subscription.planId,
1806
+ planName: result.subscription.planName,
1807
+ sitesLimit: result.subscription.sitesLimit
1808
+ });
1809
+ }
1810
+ }
1811
+ } else {
1812
+ await fetchAndSaveSubscription();
1813
+ }
1814
+ const { email, planName, sitesLimit } = getUserInfo();
1815
+ const authMethod = isApiKeyAuth() ? "API Key" : "Session";
1816
+ spinner.stop();
1817
+ console.log(import_chalk2.default.bold("\nRankCLI Account"));
1818
+ console.log("\u2500".repeat(40));
1819
+ console.log(` Email: ${import_chalk2.default.cyan(email || "N/A")}`);
1820
+ console.log(` Plan: ${import_chalk2.default.green(planName)}`);
1821
+ console.log(` Sites: ${sitesLimit} allowed`);
1822
+ console.log(` Auth: ${import_chalk2.default.dim(authMethod)}`);
1823
+ console.log("\u2500".repeat(40));
1824
+ console.log(import_chalk2.default.dim(`
1825
+ Manage at: ${WEB_APP_URL}/account`));
1826
+ } catch (err) {
1827
+ spinner.fail("Failed to fetch account info");
1828
+ console.error(import_chalk2.default.red(err instanceof Error ? err.message : "Unknown error"));
1829
+ }
1830
+ }
1831
+ async function fetchAndSaveSubscription() {
1832
+ const supabase = await getAuthenticatedClient();
1833
+ if (!supabase) return;
1834
+ const { userId } = getUserInfo();
1835
+ if (!userId) return;
1836
+ const { data: subscription } = await supabase.from("user_subscriptions").select(`
1837
+ plan:subscription_plans (
1838
+ plan_id,
1839
+ name,
1840
+ sites_limit
1841
+ )
1842
+ `).eq("user_id", userId).single();
1843
+ if (subscription?.plan) {
1844
+ const plan = subscription.plan;
1845
+ saveSubscription({
1846
+ planId: plan.plan_id,
1847
+ planName: plan.name,
1848
+ sitesLimit: plan.sites_limit
1849
+ });
1850
+ } else {
1851
+ saveSubscription({
1852
+ planId: void 0,
1853
+ planName: "Free",
1854
+ sitesLimit: 1
1855
+ });
1856
+ }
1857
+ }
1858
+
1859
+ // src/index.ts
1860
+ var program = new import_commander.Command();
1861
+ program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version("0.0.1");
1862
+ program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "Email address").option("-b, --browser", "Open browser for login").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
1863
+ program.command("logout").description("Log out of your RankCLI account").action(logout);
1864
+ program.command("whoami").description("Show current logged in user").action(whoami);
1865
+ program.command("init").description("Initialize SEO Autopilot in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
1866
+ program.command("analyze").description("AI analyzes your codebase for SEO opportunities").option("-v, --verbose", "Show detailed output").action(analyze);
1867
+ program.command("audit").description("Run a comprehensive SEO audit (170+ 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);
1868
+ 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);
1869
+ program.command("keywords").description("AI-powered keyword research with actionable suggestions").option("-u, --url <url>", "Your website URL").option("-s, --seed <keywords>", "Seed keywords (comma-separated)").option("--auto", "Auto-extract seed keywords from your page").option("--quick", "Quick mode - skip questions, use defaults").option("--ai", "Use GPT-4 for enhanced analysis with free tool ideas").option("--local", "Force local processing (skip cloud worker)").option("--competitor", "Competitor gap analysis mode (SpyFu Kombat-style)").option("-c, --competitors <domains>", "Competitor domains (comma-separated)").action(async (options) => {
1870
+ if (options.ai && options.url) {
1871
+ await keywordsAI(options.url, {
1872
+ forceLocal: options.local,
1873
+ openaiApiKey: process.env.OPENAI_API_KEY
1874
+ });
1875
+ } else if (options.competitor && options.url && options.seed) {
1876
+ const seeds = options.seed.split(",").map((s) => s.trim());
1877
+ const competitors = options.competitors ? options.competitors.split(",").map((s) => s.trim()) : void 0;
1878
+ await keywordsCompetitor(options.url, seeds, competitors);
1879
+ } else if (options.quick && options.url) {
1880
+ await keywordsQuick(options.url, options.seed);
1881
+ } else {
1882
+ await keywords(options);
1883
+ }
1884
+ });
1885
+ program.command("content").description("Analyze content for readability, keyword density, and featured snippets").option("-u, --url <url>", "URL to analyze").option("-k, --keyword <keyword>", "Target keyword for analysis").option("--headline <headline>", "Analyze a headline").option("--mode <mode>", "Analysis mode (full, readability, headline, snippet, density)", "full").action(analyzeContent);
1886
+ program.command("intent").description("Classify search intent for a keyword").argument("<keyword>", "Keyword to analyze").action(analyzeIntent);
1887
+ program.command("competitors").description("Analyze competitor SEO strategies").option("-d, --domain <domain>", "Competitor domain to analyze").action(() => {
1888
+ console.log("Competitor analysis coming soon...");
1889
+ });
1890
+ program.command("rank").description("Check current keyword rankings").action(() => {
1891
+ console.log("Rank tracking coming soon...");
1892
+ });
1893
+ program.command("report").description("Generate SEO performance report").option("-f, --format <format>", "Report format (pdf, html, json)", "html").action(() => {
1894
+ console.log("Report generation coming soon...");
1895
+ });
1896
+ program.command("watch").description("Continuous monitoring mode").action(() => {
1897
+ console.log("Watch mode coming soon...");
1898
+ });
1899
+ program.command("setup").description("Set up SEO tracking (GA4, Search Console, GitHub Actions)").option("--ga4 <id>", "Google Analytics 4 Measurement ID (G-XXXXXXXXXX)").option("--gsc <code>", "Google Search Console verification code").option("--github-action", "Set up automated GitHub Action").option("--schedule <freq>", "GitHub Action schedule (daily, weekly, monthly)", "weekly").option("--all", "Interactive setup wizard").action(setup);
1900
+ program.command("gsc-setup").description("Show Google Search Console API setup instructions").action(showGSCInstructions);
1901
+ program.parse();