@rankcli/agent-runtime 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.
Files changed (178) hide show
  1. package/README.md +242 -0
  2. package/dist/analyzer-2CSWIQGD.mjs +6 -0
  3. package/dist/chunk-YNZYHEYM.mjs +774 -0
  4. package/dist/index.d.mts +4012 -0
  5. package/dist/index.d.ts +4012 -0
  6. package/dist/index.js +29672 -0
  7. package/dist/index.mjs +28602 -0
  8. package/package.json +53 -0
  9. package/scripts/build-deno.ts +134 -0
  10. package/src/audit/ai/analyzer.ts +347 -0
  11. package/src/audit/ai/index.ts +29 -0
  12. package/src/audit/ai/prompts/content-analysis.ts +271 -0
  13. package/src/audit/ai/types.ts +179 -0
  14. package/src/audit/checks/additional-checks.ts +439 -0
  15. package/src/audit/checks/ai-citation-worthiness.ts +399 -0
  16. package/src/audit/checks/ai-content-structure.ts +325 -0
  17. package/src/audit/checks/ai-readiness.ts +339 -0
  18. package/src/audit/checks/anchor-text.ts +179 -0
  19. package/src/audit/checks/answer-conciseness.ts +322 -0
  20. package/src/audit/checks/asset-minification.ts +270 -0
  21. package/src/audit/checks/bing-optimization.ts +206 -0
  22. package/src/audit/checks/brand-mention-optimization.ts +349 -0
  23. package/src/audit/checks/caching-headers.ts +305 -0
  24. package/src/audit/checks/canonical-advanced.ts +150 -0
  25. package/src/audit/checks/canonical-domain.ts +196 -0
  26. package/src/audit/checks/citation-quality.ts +358 -0
  27. package/src/audit/checks/client-rendering.ts +542 -0
  28. package/src/audit/checks/color-contrast.ts +342 -0
  29. package/src/audit/checks/content-freshness.ts +170 -0
  30. package/src/audit/checks/content-science.ts +589 -0
  31. package/src/audit/checks/conversion-elements.ts +526 -0
  32. package/src/audit/checks/crawlability.ts +220 -0
  33. package/src/audit/checks/directory-listing.ts +172 -0
  34. package/src/audit/checks/dom-analysis.ts +191 -0
  35. package/src/audit/checks/dom-size.ts +246 -0
  36. package/src/audit/checks/duplicate-content.ts +194 -0
  37. package/src/audit/checks/eeat-signals.ts +990 -0
  38. package/src/audit/checks/entity-seo.ts +396 -0
  39. package/src/audit/checks/featured-snippet.ts +473 -0
  40. package/src/audit/checks/freshness-signals.ts +443 -0
  41. package/src/audit/checks/funnel-intent.ts +463 -0
  42. package/src/audit/checks/hreflang.ts +174 -0
  43. package/src/audit/checks/html-compliance.ts +302 -0
  44. package/src/audit/checks/image-dimensions.ts +167 -0
  45. package/src/audit/checks/images.ts +160 -0
  46. package/src/audit/checks/indexnow.ts +275 -0
  47. package/src/audit/checks/interactive-tools.ts +475 -0
  48. package/src/audit/checks/internal-link-graph.ts +436 -0
  49. package/src/audit/checks/keyword-analysis.ts +239 -0
  50. package/src/audit/checks/keyword-cannibalization.ts +385 -0
  51. package/src/audit/checks/keyword-placement.ts +471 -0
  52. package/src/audit/checks/links.ts +203 -0
  53. package/src/audit/checks/llms-txt.ts +224 -0
  54. package/src/audit/checks/local-seo.ts +296 -0
  55. package/src/audit/checks/mobile.ts +167 -0
  56. package/src/audit/checks/modern-images.ts +226 -0
  57. package/src/audit/checks/navboost-signals.ts +395 -0
  58. package/src/audit/checks/on-page.ts +209 -0
  59. package/src/audit/checks/page-resources.ts +285 -0
  60. package/src/audit/checks/pagination.ts +180 -0
  61. package/src/audit/checks/performance.ts +153 -0
  62. package/src/audit/checks/platform-presence.ts +580 -0
  63. package/src/audit/checks/redirect-analysis.ts +153 -0
  64. package/src/audit/checks/redirect-chain.ts +389 -0
  65. package/src/audit/checks/resource-hints.ts +420 -0
  66. package/src/audit/checks/responsive-css.ts +247 -0
  67. package/src/audit/checks/responsive-images.ts +396 -0
  68. package/src/audit/checks/review-ecosystem.ts +415 -0
  69. package/src/audit/checks/robots-validation.ts +373 -0
  70. package/src/audit/checks/security-headers.ts +172 -0
  71. package/src/audit/checks/security.ts +144 -0
  72. package/src/audit/checks/serp-preview.ts +251 -0
  73. package/src/audit/checks/site-maturity.ts +444 -0
  74. package/src/audit/checks/social-meta.test.ts +275 -0
  75. package/src/audit/checks/social-meta.ts +134 -0
  76. package/src/audit/checks/soft-404.ts +151 -0
  77. package/src/audit/checks/structured-data.ts +238 -0
  78. package/src/audit/checks/tech-detection.ts +496 -0
  79. package/src/audit/checks/topical-clusters.ts +435 -0
  80. package/src/audit/checks/tracker-bloat.ts +462 -0
  81. package/src/audit/checks/tracking-verification.test.ts +371 -0
  82. package/src/audit/checks/tracking-verification.ts +636 -0
  83. package/src/audit/checks/url-safety.ts +682 -0
  84. package/src/audit/deno-entry.ts +66 -0
  85. package/src/audit/discovery/index.ts +15 -0
  86. package/src/audit/discovery/link-crawler.ts +232 -0
  87. package/src/audit/discovery/repo-routes.ts +347 -0
  88. package/src/audit/engine.ts +620 -0
  89. package/src/audit/fixes/index.ts +209 -0
  90. package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
  91. package/src/audit/fixes/social-meta-fixes.ts +463 -0
  92. package/src/audit/index.ts +74 -0
  93. package/src/audit/runner.test.ts +299 -0
  94. package/src/audit/runner.ts +130 -0
  95. package/src/audit/types.ts +1953 -0
  96. package/src/content/featured-snippet.ts +367 -0
  97. package/src/content/generator.test.ts +534 -0
  98. package/src/content/generator.ts +501 -0
  99. package/src/content/headline.ts +317 -0
  100. package/src/content/index.ts +62 -0
  101. package/src/content/intent.ts +258 -0
  102. package/src/content/keyword-density.ts +349 -0
  103. package/src/content/readability.ts +262 -0
  104. package/src/executor.ts +336 -0
  105. package/src/fixer.ts +416 -0
  106. package/src/frameworks/detector.test.ts +248 -0
  107. package/src/frameworks/detector.ts +371 -0
  108. package/src/frameworks/index.ts +68 -0
  109. package/src/frameworks/recipes/angular.yaml +171 -0
  110. package/src/frameworks/recipes/astro.yaml +206 -0
  111. package/src/frameworks/recipes/django.yaml +180 -0
  112. package/src/frameworks/recipes/laravel.yaml +137 -0
  113. package/src/frameworks/recipes/nextjs.yaml +268 -0
  114. package/src/frameworks/recipes/nuxt.yaml +175 -0
  115. package/src/frameworks/recipes/rails.yaml +188 -0
  116. package/src/frameworks/recipes/react.yaml +202 -0
  117. package/src/frameworks/recipes/sveltekit.yaml +154 -0
  118. package/src/frameworks/recipes/vue.yaml +137 -0
  119. package/src/frameworks/recipes/wordpress.yaml +209 -0
  120. package/src/frameworks/suggestion-engine.ts +320 -0
  121. package/src/geo/geo-content.test.ts +305 -0
  122. package/src/geo/geo-content.ts +266 -0
  123. package/src/geo/geo-history.test.ts +473 -0
  124. package/src/geo/geo-history.ts +433 -0
  125. package/src/geo/geo-tracker.test.ts +359 -0
  126. package/src/geo/geo-tracker.ts +411 -0
  127. package/src/geo/index.ts +10 -0
  128. package/src/git/commit-helper.test.ts +261 -0
  129. package/src/git/commit-helper.ts +329 -0
  130. package/src/git/index.ts +12 -0
  131. package/src/git/pr-helper.test.ts +284 -0
  132. package/src/git/pr-helper.ts +307 -0
  133. package/src/index.ts +66 -0
  134. package/src/keywords/ai-keyword-engine.ts +1062 -0
  135. package/src/keywords/ai-summarizer.ts +387 -0
  136. package/src/keywords/ci-mode.ts +555 -0
  137. package/src/keywords/engine.ts +359 -0
  138. package/src/keywords/index.ts +151 -0
  139. package/src/keywords/llm-judge.ts +357 -0
  140. package/src/keywords/nlp-analysis.ts +706 -0
  141. package/src/keywords/prioritizer.ts +295 -0
  142. package/src/keywords/site-crawler.ts +342 -0
  143. package/src/keywords/sources/autocomplete.ts +139 -0
  144. package/src/keywords/sources/competitive-search.ts +450 -0
  145. package/src/keywords/sources/competitor-analysis.ts +374 -0
  146. package/src/keywords/sources/dataforseo.ts +206 -0
  147. package/src/keywords/sources/free-sources.ts +294 -0
  148. package/src/keywords/sources/gsc.ts +123 -0
  149. package/src/keywords/topic-grouping.ts +327 -0
  150. package/src/keywords/types.ts +144 -0
  151. package/src/keywords/wizard.ts +457 -0
  152. package/src/loader.ts +40 -0
  153. package/src/reports/index.ts +7 -0
  154. package/src/reports/report-generator.test.ts +293 -0
  155. package/src/reports/report-generator.ts +713 -0
  156. package/src/scheduler/alerts.test.ts +458 -0
  157. package/src/scheduler/alerts.ts +328 -0
  158. package/src/scheduler/index.ts +8 -0
  159. package/src/scheduler/scheduled-audit.test.ts +377 -0
  160. package/src/scheduler/scheduled-audit.ts +149 -0
  161. package/src/test/integration-test.ts +325 -0
  162. package/src/tools/analyzer.ts +373 -0
  163. package/src/tools/crawl.ts +293 -0
  164. package/src/tools/files.ts +301 -0
  165. package/src/tools/h1-fixer.ts +249 -0
  166. package/src/tools/index.ts +67 -0
  167. package/src/tracking/github-action.ts +326 -0
  168. package/src/tracking/google-analytics.ts +265 -0
  169. package/src/tracking/index.ts +45 -0
  170. package/src/tracking/report-generator.ts +386 -0
  171. package/src/tracking/search-console.ts +335 -0
  172. package/src/types.ts +134 -0
  173. package/src/utils/http.ts +302 -0
  174. package/src/wasm-adapter.ts +297 -0
  175. package/src/wasm-entry.ts +14 -0
  176. package/tsconfig.json +17 -0
  177. package/tsup.wasm.config.ts +26 -0
  178. package/vitest.config.ts +15 -0
package/src/fixer.ts ADDED
@@ -0,0 +1,416 @@
1
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import type { SEOIssue, Fix, FrameworkInfo } from './types.js';
4
+ import { detectFramework, findHtmlEntry, readFile } from './tools/files.js';
5
+ import { generateH1Fix } from './tools/h1-fixer.js';
6
+
7
+ export interface FixGeneratorOptions {
8
+ cwd: string;
9
+ url?: string;
10
+ framework?: FrameworkInfo;
11
+ }
12
+
13
+ export interface GeneratedFix extends Fix {
14
+ applied?: boolean;
15
+ skipped?: boolean;
16
+ skipReason?: string;
17
+ }
18
+
19
+ // Generate fixes for SEO issues based on framework
20
+ export async function generateFixes(
21
+ issues: SEOIssue[],
22
+ options: FixGeneratorOptions
23
+ ): Promise<GeneratedFix[]> {
24
+ const { cwd, url } = options;
25
+
26
+ // Detect framework if not provided
27
+ let framework = options.framework;
28
+ if (!framework) {
29
+ const result = await detectFramework({ cwd });
30
+ framework = result.success ? (result.data as FrameworkInfo) : { name: 'Unknown', metaPattern: 'html-head' as const };
31
+ }
32
+
33
+ const fixes: GeneratedFix[] = [];
34
+
35
+ // Find the HTML entry point for Vite/React apps
36
+ const htmlEntry = await findHtmlEntry({ cwd });
37
+ const htmlPath = htmlEntry.success ? (htmlEntry.data as { path: string; content: string }).path : 'index.html';
38
+ const htmlContent = htmlEntry.success ? (htmlEntry.data as { path: string; content: string }).content : '';
39
+
40
+ for (const issue of issues) {
41
+ const fix = await generateFixForIssue(issue, {
42
+ cwd,
43
+ url,
44
+ framework,
45
+ htmlPath,
46
+ htmlContent,
47
+ });
48
+
49
+ if (fix) {
50
+ fixes.push(fix);
51
+ }
52
+ }
53
+
54
+ return fixes;
55
+ }
56
+
57
+ interface FixContext {
58
+ cwd: string;
59
+ url?: string;
60
+ framework: FrameworkInfo;
61
+ htmlPath: string;
62
+ htmlContent: string;
63
+ }
64
+
65
+ async function generateFixForIssue(
66
+ issue: SEOIssue,
67
+ context: FixContext
68
+ ): Promise<GeneratedFix | null> {
69
+ const { cwd, url, framework, htmlPath, htmlContent } = context;
70
+ const fullUrl = url || 'https://example.com';
71
+ const siteName = new URL(fullUrl).hostname.replace('www.', '');
72
+
73
+ switch (issue.code) {
74
+ case 'MISSING_TITLE':
75
+ return generateTitleFix(context, siteName);
76
+
77
+ case 'MISSING_META_DESC':
78
+ return generateMetaDescFix(context, siteName);
79
+
80
+ case 'MISSING_CANONICAL':
81
+ return generateCanonicalFix(context, fullUrl);
82
+
83
+ case 'MISSING_VIEWPORT':
84
+ return generateViewportFix(context);
85
+
86
+ case 'MISSING_OG_TAGS':
87
+ return generateOGFix(context, siteName, fullUrl);
88
+
89
+ case 'MISSING_TWITTER_CARD':
90
+ return generateTwitterFix(context, siteName);
91
+
92
+ case 'MISSING_SCHEMA':
93
+ return generateSchemaFix(context, siteName, fullUrl);
94
+
95
+ case 'MISSING_ROBOTS':
96
+ return generateRobotsFix(context, fullUrl);
97
+
98
+ case 'MISSING_SITEMAP':
99
+ return generateSitemapFix(context, fullUrl);
100
+
101
+ case 'MISSING_H1':
102
+ return await generateH1Fix({ cwd });
103
+
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function generateTitleFix(context: FixContext, siteName: string): GeneratedFix {
110
+ const { htmlPath, htmlContent, framework } = context;
111
+
112
+ if (framework.metaPattern === 'html-head') {
113
+ // Vite/React - modify index.html
114
+ const titleTag = `<title>${siteName} - Your Product Tagline</title>`;
115
+
116
+ if (htmlContent.includes('<title>')) {
117
+ // Replace existing title
118
+ const before = htmlContent.match(/<title>.*?<\/title>/)?.[0] || '';
119
+ return {
120
+ issue: { code: 'MISSING_TITLE', message: 'Missing or empty title tag', severity: 'critical' },
121
+ file: htmlPath,
122
+ before,
123
+ after: titleTag,
124
+ explanation: 'Updated the title tag with a descriptive title',
125
+ };
126
+ } else {
127
+ // Add title after <head>
128
+ return {
129
+ issue: { code: 'MISSING_TITLE', message: 'Missing title tag', severity: 'critical' },
130
+ file: htmlPath,
131
+ before: '<head>',
132
+ after: `<head>\n ${titleTag}`,
133
+ explanation: 'Added a title tag to the document head',
134
+ };
135
+ }
136
+ }
137
+
138
+ // For other frameworks, provide guidance
139
+ return {
140
+ issue: { code: 'MISSING_TITLE', message: 'Missing title tag', severity: 'critical' },
141
+ file: htmlPath,
142
+ before: null,
143
+ after: `<title>${siteName} - Your Product Tagline</title>`,
144
+ explanation: `Add title using ${framework.name} patterns`,
145
+ };
146
+ }
147
+
148
+ function generateMetaDescFix(context: FixContext, siteName: string): GeneratedFix {
149
+ const { htmlPath, htmlContent, framework } = context;
150
+ const description = `${siteName} - A brief, compelling description of your product or service. Include key features and benefits.`;
151
+
152
+ if (framework.metaPattern === 'html-head') {
153
+ const metaTag = `<meta name="description" content="${description}" />`;
154
+
155
+ if (htmlContent.includes('<title>')) {
156
+ return {
157
+ issue: { code: 'MISSING_META_DESC', message: 'Missing meta description', severity: 'critical' },
158
+ file: htmlPath,
159
+ before: '</title>',
160
+ after: `</title>\n ${metaTag}`,
161
+ explanation: 'Added meta description after the title tag',
162
+ };
163
+ } else {
164
+ return {
165
+ issue: { code: 'MISSING_META_DESC', message: 'Missing meta description', severity: 'critical' },
166
+ file: htmlPath,
167
+ before: '<head>',
168
+ after: `<head>\n ${metaTag}`,
169
+ explanation: 'Added meta description to the document head',
170
+ };
171
+ }
172
+ }
173
+
174
+ return {
175
+ issue: { code: 'MISSING_META_DESC', message: 'Missing meta description', severity: 'critical' },
176
+ file: htmlPath,
177
+ before: null,
178
+ after: `<meta name="description" content="${description}" />`,
179
+ explanation: `Add meta description using ${framework.name} patterns`,
180
+ };
181
+ }
182
+
183
+ function generateCanonicalFix(context: FixContext, url: string): GeneratedFix {
184
+ const { htmlPath, htmlContent, framework } = context;
185
+ const canonicalTag = `<link rel="canonical" href="${url}" />`;
186
+
187
+ // Check if canonical already exists
188
+ if (htmlContent.includes('rel="canonical"') || htmlContent.includes("rel='canonical'")) {
189
+ return {
190
+ issue: { code: 'MISSING_CANONICAL', message: 'No canonical URL specified', severity: 'warning' },
191
+ file: htmlPath,
192
+ before: null,
193
+ after: canonicalTag,
194
+ explanation: 'Canonical URL already exists',
195
+ skipped: true,
196
+ skipReason: 'Canonical tag already present',
197
+ };
198
+ }
199
+
200
+ if (framework.metaPattern === 'html-head') {
201
+ return {
202
+ issue: { code: 'MISSING_CANONICAL', message: 'No canonical URL specified', severity: 'warning' },
203
+ file: htmlPath,
204
+ before: '</head>',
205
+ after: ` ${canonicalTag}\n </head>`,
206
+ explanation: 'Added canonical URL to prevent duplicate content issues',
207
+ };
208
+ }
209
+
210
+ return {
211
+ issue: { code: 'MISSING_CANONICAL', message: 'No canonical URL specified', severity: 'warning' },
212
+ file: htmlPath,
213
+ before: null,
214
+ after: canonicalTag,
215
+ explanation: `Add canonical URL using ${framework.name} patterns`,
216
+ };
217
+ }
218
+
219
+ function generateViewportFix(context: FixContext): GeneratedFix {
220
+ const { htmlPath, htmlContent, framework } = context;
221
+ const viewportTag = '<meta name="viewport" content="width=device-width, initial-scale=1.0" />';
222
+
223
+ if (framework.metaPattern === 'html-head' && !htmlContent.includes('viewport')) {
224
+ return {
225
+ issue: { code: 'MISSING_VIEWPORT', message: 'Missing viewport meta tag', severity: 'critical' },
226
+ file: htmlPath,
227
+ before: '<head>',
228
+ after: `<head>\n ${viewportTag}`,
229
+ explanation: 'Added viewport meta tag for mobile responsiveness',
230
+ };
231
+ }
232
+
233
+ return {
234
+ issue: { code: 'MISSING_VIEWPORT', message: 'Missing viewport meta tag', severity: 'critical' },
235
+ file: htmlPath,
236
+ before: null,
237
+ after: viewportTag,
238
+ explanation: 'Add viewport meta tag for mobile-first indexing',
239
+ };
240
+ }
241
+
242
+ function generateOGFix(context: FixContext, siteName: string, url: string): GeneratedFix {
243
+ const { htmlPath, htmlContent } = context;
244
+
245
+ // Check which OG tags are missing
246
+ const missingTags: string[] = [];
247
+ if (!htmlContent.includes('og:image')) {
248
+ missingTags.push(`<meta property="og:image" content="${url}/og-image.png" />`);
249
+ }
250
+ if (!htmlContent.includes('og:url')) {
251
+ missingTags.push(`<meta property="og:url" content="${url}" />`);
252
+ }
253
+
254
+ if (missingTags.length === 0) {
255
+ return {
256
+ issue: { code: 'MISSING_OG_TAGS', message: 'Missing Open Graph tags', severity: 'warning' },
257
+ file: htmlPath,
258
+ before: null,
259
+ after: '',
260
+ explanation: 'All essential OG tags are present',
261
+ skipped: true,
262
+ skipReason: 'OG tags already exist',
263
+ };
264
+ }
265
+
266
+ const ogTags = `<!-- SEO Autopilot: Missing OG tags -->\n ${missingTags.join('\n ')}`;
267
+
268
+ return {
269
+ issue: { code: 'MISSING_OG_TAGS', message: 'Missing Open Graph tags', severity: 'warning' },
270
+ file: htmlPath,
271
+ before: '</head>',
272
+ after: ` ${ogTags}\n </head>`,
273
+ explanation: `Added missing Open Graph meta tags: ${missingTags.length} tag(s)`,
274
+ };
275
+ }
276
+
277
+ function generateTwitterFix(context: FixContext, siteName: string): GeneratedFix {
278
+ const { htmlPath } = context;
279
+
280
+ const twitterTags = `<!-- Twitter -->
281
+ <meta name="twitter:card" content="summary_large_image" />
282
+ <meta name="twitter:title" content="${siteName} - Your Product Tagline" />
283
+ <meta name="twitter:description" content="A compelling description for Twitter sharing." />
284
+ <meta name="twitter:image" content="/og-image.png" />`;
285
+
286
+ return {
287
+ issue: { code: 'MISSING_TWITTER_CARD', message: 'Missing Twitter Card meta tags', severity: 'info' },
288
+ file: htmlPath,
289
+ before: '</head>',
290
+ after: ` ${twitterTags}\n </head>`,
291
+ explanation: 'Added Twitter Card meta tags for better X/Twitter sharing',
292
+ };
293
+ }
294
+
295
+ function generateSchemaFix(context: FixContext, siteName: string, url: string): GeneratedFix {
296
+ const { htmlPath, framework } = context;
297
+
298
+ const schema = {
299
+ '@context': 'https://schema.org',
300
+ '@type': 'SoftwareApplication',
301
+ 'name': siteName,
302
+ 'url': url,
303
+ 'applicationCategory': 'WebApplication',
304
+ 'operatingSystem': 'Web Browser',
305
+ 'offers': {
306
+ '@type': 'Offer',
307
+ 'price': '0',
308
+ 'priceCurrency': 'USD',
309
+ },
310
+ };
311
+
312
+ const schemaScript = `<script type="application/ld+json">
313
+ ${JSON.stringify(schema, null, 2)}
314
+ </script>`;
315
+
316
+ return {
317
+ issue: { code: 'MISSING_SCHEMA', message: 'No JSON-LD structured data found', severity: 'warning' },
318
+ file: htmlPath,
319
+ before: '</head>',
320
+ after: ` ${schemaScript}\n </head>`,
321
+ explanation: 'Added JSON-LD structured data for rich search results',
322
+ };
323
+ }
324
+
325
+ function generateRobotsFix(context: FixContext, url: string): GeneratedFix {
326
+ const robotsTxt = `# robots.txt for ${new URL(url).hostname}
327
+ User-agent: *
328
+ Allow: /
329
+
330
+ # Sitemap
331
+ Sitemap: ${url}/sitemap.xml`;
332
+
333
+ return {
334
+ issue: { code: 'MISSING_ROBOTS', message: 'No robots.txt file found', severity: 'warning' },
335
+ file: 'public/robots.txt',
336
+ before: null,
337
+ after: robotsTxt,
338
+ explanation: 'Created robots.txt to provide crawling instructions to search engines',
339
+ };
340
+ }
341
+
342
+ function generateSitemapFix(context: FixContext, url: string): GeneratedFix {
343
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
344
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
345
+ <url>
346
+ <loc>${url}</loc>
347
+ <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>
348
+ <priority>1.0</priority>
349
+ </url>
350
+ </urlset>`;
351
+
352
+ return {
353
+ issue: { code: 'MISSING_SITEMAP', message: 'No sitemap.xml found', severity: 'warning' },
354
+ file: 'public/sitemap.xml',
355
+ before: null,
356
+ after: sitemap,
357
+ explanation: 'Created sitemap.xml to help search engines discover all pages',
358
+ };
359
+ }
360
+
361
+ // Apply fixes to the codebase
362
+ export async function applyFixes(
363
+ fixes: GeneratedFix[],
364
+ options: { cwd: string; dryRun?: boolean }
365
+ ): Promise<{ applied: GeneratedFix[]; skipped: GeneratedFix[] }> {
366
+ const { cwd, dryRun = false } = options;
367
+ const applied: GeneratedFix[] = [];
368
+ const skipped: GeneratedFix[] = [];
369
+
370
+ for (const fix of fixes) {
371
+ if (fix.skipped) {
372
+ skipped.push(fix);
373
+ continue;
374
+ }
375
+
376
+ try {
377
+ const filePath = join(cwd, fix.file);
378
+
379
+ if (dryRun) {
380
+ applied.push({ ...fix, applied: false });
381
+ continue;
382
+ }
383
+
384
+ if (fix.before === null) {
385
+ // Create new file
386
+ const dir = join(cwd, fix.file.split('/').slice(0, -1).join('/'));
387
+ if (dir !== cwd && !existsSync(dir)) {
388
+ const { mkdirSync } = await import('fs');
389
+ mkdirSync(dir, { recursive: true });
390
+ }
391
+ writeFileSync(filePath, fix.after, 'utf-8');
392
+ applied.push({ ...fix, applied: true });
393
+ } else if (existsSync(filePath)) {
394
+ // Modify existing file
395
+ let content = readFileSync(filePath, 'utf-8');
396
+ if (content.includes(fix.before)) {
397
+ content = content.replace(fix.before, fix.after);
398
+ writeFileSync(filePath, content, 'utf-8');
399
+ applied.push({ ...fix, applied: true });
400
+ } else {
401
+ skipped.push({ ...fix, skipped: true, skipReason: 'Pattern not found in file' });
402
+ }
403
+ } else {
404
+ skipped.push({ ...fix, skipped: true, skipReason: 'File not found' });
405
+ }
406
+ } catch (error) {
407
+ skipped.push({
408
+ ...fix,
409
+ skipped: true,
410
+ skipReason: error instanceof Error ? error.message : 'Unknown error',
411
+ });
412
+ }
413
+ }
414
+
415
+ return { applied, skipped };
416
+ }
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ detectFromHtml,
4
+ detectFromPackageJson,
5
+ detectFromHeaders,
6
+ detectFramework,
7
+ getFrameworkDisplayName,
8
+ } from './detector.js';
9
+
10
+ describe('Framework Detector', () => {
11
+ describe('detectFromHtml', () => {
12
+ it('detects Next.js from __next data', () => {
13
+ const html = '<div id="__next"><h1>Hello</h1></div>';
14
+ const result = detectFromHtml(html);
15
+ expect(result?.framework).toBe('nextjs');
16
+ expect(result?.confidence).toBe('high');
17
+ });
18
+
19
+ it('detects Next.js from _next scripts', () => {
20
+ const html = '<script src="/_next/static/chunks/main.js"></script>';
21
+ const result = detectFromHtml(html);
22
+ expect(result?.framework).toBe('nextjs');
23
+ });
24
+
25
+ it('detects Nuxt from __nuxt data', () => {
26
+ const html = '<div id="__nuxt"><h1>Hello</h1></div>';
27
+ const result = detectFromHtml(html);
28
+ expect(result?.framework).toBe('nuxt');
29
+ });
30
+
31
+ it('detects Gatsby from ___gatsby', () => {
32
+ const html = '<div id="___gatsby"><h1>Hello</h1></div>';
33
+ const result = detectFromHtml(html);
34
+ expect(result?.framework).toBe('gatsby');
35
+ });
36
+
37
+ it('detects Astro from data-astro attributes', () => {
38
+ const html = '<div data-astro-cid-xyz><h1>Hello</h1></div>';
39
+ const result = detectFromHtml(html);
40
+ expect(result?.framework).toBe('astro');
41
+ });
42
+
43
+ it('detects SvelteKit from __sveltekit', () => {
44
+ const html = '<div id="__sveltekit"><h1>Hello</h1></div>';
45
+ const result = detectFromHtml(html);
46
+ expect(result?.framework).toBe('sveltekit');
47
+ });
48
+
49
+ it('detects React from data-reactroot', () => {
50
+ const html = '<div data-reactroot><h1>Hello</h1></div>';
51
+ const result = detectFromHtml(html);
52
+ expect(result?.framework).toBe('react');
53
+ });
54
+
55
+ it('detects Vue from data-v- attributes', () => {
56
+ const html = '<div data-v-abc123><h1>Hello</h1></div>';
57
+ const result = detectFromHtml(html);
58
+ expect(result?.framework).toBe('vue');
59
+ });
60
+
61
+ it('detects Angular from ng-version', () => {
62
+ const html = '<app-root ng-version="17.0.0"><h1>Hello</h1></app-root>';
63
+ const result = detectFromHtml(html);
64
+ expect(result?.framework).toBe('angular');
65
+ });
66
+
67
+ it('detects Angular from _ngcontent', () => {
68
+ const html = '<div _ngcontent-abc-c123><h1>Hello</h1></div>';
69
+ const result = detectFromHtml(html);
70
+ expect(result?.framework).toBe('angular');
71
+ });
72
+
73
+ it('detects WordPress from wp-content', () => {
74
+ const html = '<link rel="stylesheet" href="/wp-content/themes/theme/style.css">';
75
+ const result = detectFromHtml(html);
76
+ expect(result?.framework).toBe('wordpress');
77
+ });
78
+
79
+ it('detects Rails from turbolinks', () => {
80
+ const html = '<script src="/assets/turbolinks.js"></script>';
81
+ const result = detectFromHtml(html);
82
+ expect(result?.framework).toBe('rails');
83
+ });
84
+
85
+ it('returns null for unknown HTML', () => {
86
+ const html = '<html><body><h1>Hello</h1></body></html>';
87
+ const result = detectFromHtml(html);
88
+ expect(result).toBeNull();
89
+ });
90
+ });
91
+
92
+ describe('detectFromPackageJson', () => {
93
+ it('detects Next.js from dependencies', () => {
94
+ const packageJson = {
95
+ dependencies: { next: '^14.0.0', react: '^18.0.0' },
96
+ };
97
+ const result = detectFromPackageJson(packageJson);
98
+ expect(result?.framework).toBe('nextjs');
99
+ expect(result?.version).toBe('^14.0.0');
100
+ expect(result?.meta_framework).toBe('react');
101
+ });
102
+
103
+ it('detects Nuxt from dependencies', () => {
104
+ const packageJson = {
105
+ dependencies: { nuxt: '^3.0.0', vue: '^3.0.0' },
106
+ };
107
+ const result = detectFromPackageJson(packageJson);
108
+ expect(result?.framework).toBe('nuxt');
109
+ expect(result?.meta_framework).toBe('vue');
110
+ });
111
+
112
+ it('detects React from devDependencies', () => {
113
+ const packageJson = {
114
+ devDependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' },
115
+ };
116
+ const result = detectFromPackageJson(packageJson);
117
+ expect(result?.framework).toBe('react');
118
+ });
119
+
120
+ it('detects Astro', () => {
121
+ const packageJson = {
122
+ dependencies: { astro: '^4.0.0' },
123
+ };
124
+ const result = detectFromPackageJson(packageJson);
125
+ expect(result?.framework).toBe('astro');
126
+ });
127
+
128
+ it('detects SvelteKit', () => {
129
+ const packageJson = {
130
+ devDependencies: { '@sveltejs/kit': '^2.0.0' },
131
+ };
132
+ const result = detectFromPackageJson(packageJson);
133
+ expect(result?.framework).toBe('sveltekit');
134
+ expect(result?.meta_framework).toBe('svelte');
135
+ });
136
+
137
+ it('detects Angular', () => {
138
+ const packageJson = {
139
+ dependencies: { '@angular/core': '^17.0.0' },
140
+ };
141
+ const result = detectFromPackageJson(packageJson);
142
+ expect(result?.framework).toBe('angular');
143
+ });
144
+
145
+ it('returns null for empty package.json', () => {
146
+ const packageJson = {};
147
+ const result = detectFromPackageJson(packageJson);
148
+ expect(result).toBeNull();
149
+ });
150
+ });
151
+
152
+ describe('detectFromHeaders', () => {
153
+ it('detects Next.js from X-Powered-By', () => {
154
+ const headers = { 'x-powered-by': 'Next.js' };
155
+ const result = detectFromHeaders(headers);
156
+ expect(result?.framework).toBe('nextjs');
157
+ });
158
+
159
+ it('detects Nuxt from X-Powered-By', () => {
160
+ const headers = { 'x-powered-by': 'Nuxt' };
161
+ const result = detectFromHeaders(headers);
162
+ expect(result?.framework).toBe('nuxt');
163
+ });
164
+
165
+ it('detects Rails from Phusion Passenger', () => {
166
+ const headers = { 'x-powered-by': 'Phusion Passenger' };
167
+ const result = detectFromHeaders(headers);
168
+ expect(result?.framework).toBe('rails');
169
+ });
170
+
171
+ it('returns null for generic headers', () => {
172
+ const headers = { 'x-powered-by': 'Express' };
173
+ const result = detectFromHeaders(headers);
174
+ expect(result).toBeNull();
175
+ });
176
+ });
177
+
178
+ describe('detectFramework', () => {
179
+ it('prioritizes package.json over HTML', () => {
180
+ const result = detectFramework({
181
+ packageJson: { dependencies: { next: '^14.0.0' } },
182
+ html: '<div data-reactroot>',
183
+ });
184
+ expect(result.framework).toBe('nextjs');
185
+ });
186
+
187
+ it('falls back to HTML when no package.json', () => {
188
+ const result = detectFramework({
189
+ html: '<div id="__nuxt">',
190
+ });
191
+ expect(result.framework).toBe('nuxt');
192
+ });
193
+
194
+ it('detects from file patterns', () => {
195
+ const result = detectFramework({
196
+ files: ['next.config.js', 'pages/index.tsx'],
197
+ });
198
+ expect(result.framework).toBe('nextjs');
199
+ });
200
+
201
+ it('detects WordPress from wp-config.php', () => {
202
+ const result = detectFramework({
203
+ files: ['wp-config.php', 'wp-content/themes/theme'],
204
+ });
205
+ expect(result.framework).toBe('wordpress');
206
+ });
207
+
208
+ it('detects Django from manage.py', () => {
209
+ const result = detectFramework({
210
+ files: ['manage.py', 'myapp/settings.py'],
211
+ });
212
+ expect(result.framework).toBe('django');
213
+ });
214
+
215
+ it('detects Rails from Gemfile + routes.rb', () => {
216
+ const result = detectFramework({
217
+ files: ['Gemfile', 'config/routes.rb'],
218
+ });
219
+ expect(result.framework).toBe('rails');
220
+ });
221
+
222
+ it('detects Laravel from artisan', () => {
223
+ const result = detectFramework({
224
+ files: ['artisan', 'app/Http/Controllers'],
225
+ });
226
+ expect(result.framework).toBe('laravel');
227
+ });
228
+
229
+ it('returns unknown for unrecognized input', () => {
230
+ const result = detectFramework({});
231
+ expect(result.framework).toBe('unknown');
232
+ expect(result.confidence).toBe('low');
233
+ });
234
+ });
235
+
236
+ describe('getFrameworkDisplayName', () => {
237
+ it('returns correct display names', () => {
238
+ expect(getFrameworkDisplayName('react')).toBe('React');
239
+ expect(getFrameworkDisplayName('nextjs')).toBe('Next.js');
240
+ expect(getFrameworkDisplayName('vue')).toBe('Vue.js');
241
+ expect(getFrameworkDisplayName('nuxt')).toBe('Nuxt');
242
+ expect(getFrameworkDisplayName('angular')).toBe('Angular');
243
+ expect(getFrameworkDisplayName('rails')).toBe('Ruby on Rails');
244
+ expect(getFrameworkDisplayName('wordpress')).toBe('WordPress');
245
+ expect(getFrameworkDisplayName('unknown')).toBe('Unknown');
246
+ });
247
+ });
248
+ });