@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.
- package/README.md +242 -0
- package/dist/analyzer-2CSWIQGD.mjs +6 -0
- package/dist/chunk-YNZYHEYM.mjs +774 -0
- package/dist/index.d.mts +4012 -0
- package/dist/index.d.ts +4012 -0
- package/dist/index.js +29672 -0
- package/dist/index.mjs +28602 -0
- package/package.json +53 -0
- package/scripts/build-deno.ts +134 -0
- package/src/audit/ai/analyzer.ts +347 -0
- package/src/audit/ai/index.ts +29 -0
- package/src/audit/ai/prompts/content-analysis.ts +271 -0
- package/src/audit/ai/types.ts +179 -0
- package/src/audit/checks/additional-checks.ts +439 -0
- package/src/audit/checks/ai-citation-worthiness.ts +399 -0
- package/src/audit/checks/ai-content-structure.ts +325 -0
- package/src/audit/checks/ai-readiness.ts +339 -0
- package/src/audit/checks/anchor-text.ts +179 -0
- package/src/audit/checks/answer-conciseness.ts +322 -0
- package/src/audit/checks/asset-minification.ts +270 -0
- package/src/audit/checks/bing-optimization.ts +206 -0
- package/src/audit/checks/brand-mention-optimization.ts +349 -0
- package/src/audit/checks/caching-headers.ts +305 -0
- package/src/audit/checks/canonical-advanced.ts +150 -0
- package/src/audit/checks/canonical-domain.ts +196 -0
- package/src/audit/checks/citation-quality.ts +358 -0
- package/src/audit/checks/client-rendering.ts +542 -0
- package/src/audit/checks/color-contrast.ts +342 -0
- package/src/audit/checks/content-freshness.ts +170 -0
- package/src/audit/checks/content-science.ts +589 -0
- package/src/audit/checks/conversion-elements.ts +526 -0
- package/src/audit/checks/crawlability.ts +220 -0
- package/src/audit/checks/directory-listing.ts +172 -0
- package/src/audit/checks/dom-analysis.ts +191 -0
- package/src/audit/checks/dom-size.ts +246 -0
- package/src/audit/checks/duplicate-content.ts +194 -0
- package/src/audit/checks/eeat-signals.ts +990 -0
- package/src/audit/checks/entity-seo.ts +396 -0
- package/src/audit/checks/featured-snippet.ts +473 -0
- package/src/audit/checks/freshness-signals.ts +443 -0
- package/src/audit/checks/funnel-intent.ts +463 -0
- package/src/audit/checks/hreflang.ts +174 -0
- package/src/audit/checks/html-compliance.ts +302 -0
- package/src/audit/checks/image-dimensions.ts +167 -0
- package/src/audit/checks/images.ts +160 -0
- package/src/audit/checks/indexnow.ts +275 -0
- package/src/audit/checks/interactive-tools.ts +475 -0
- package/src/audit/checks/internal-link-graph.ts +436 -0
- package/src/audit/checks/keyword-analysis.ts +239 -0
- package/src/audit/checks/keyword-cannibalization.ts +385 -0
- package/src/audit/checks/keyword-placement.ts +471 -0
- package/src/audit/checks/links.ts +203 -0
- package/src/audit/checks/llms-txt.ts +224 -0
- package/src/audit/checks/local-seo.ts +296 -0
- package/src/audit/checks/mobile.ts +167 -0
- package/src/audit/checks/modern-images.ts +226 -0
- package/src/audit/checks/navboost-signals.ts +395 -0
- package/src/audit/checks/on-page.ts +209 -0
- package/src/audit/checks/page-resources.ts +285 -0
- package/src/audit/checks/pagination.ts +180 -0
- package/src/audit/checks/performance.ts +153 -0
- package/src/audit/checks/platform-presence.ts +580 -0
- package/src/audit/checks/redirect-analysis.ts +153 -0
- package/src/audit/checks/redirect-chain.ts +389 -0
- package/src/audit/checks/resource-hints.ts +420 -0
- package/src/audit/checks/responsive-css.ts +247 -0
- package/src/audit/checks/responsive-images.ts +396 -0
- package/src/audit/checks/review-ecosystem.ts +415 -0
- package/src/audit/checks/robots-validation.ts +373 -0
- package/src/audit/checks/security-headers.ts +172 -0
- package/src/audit/checks/security.ts +144 -0
- package/src/audit/checks/serp-preview.ts +251 -0
- package/src/audit/checks/site-maturity.ts +444 -0
- package/src/audit/checks/social-meta.test.ts +275 -0
- package/src/audit/checks/social-meta.ts +134 -0
- package/src/audit/checks/soft-404.ts +151 -0
- package/src/audit/checks/structured-data.ts +238 -0
- package/src/audit/checks/tech-detection.ts +496 -0
- package/src/audit/checks/topical-clusters.ts +435 -0
- package/src/audit/checks/tracker-bloat.ts +462 -0
- package/src/audit/checks/tracking-verification.test.ts +371 -0
- package/src/audit/checks/tracking-verification.ts +636 -0
- package/src/audit/checks/url-safety.ts +682 -0
- package/src/audit/deno-entry.ts +66 -0
- package/src/audit/discovery/index.ts +15 -0
- package/src/audit/discovery/link-crawler.ts +232 -0
- package/src/audit/discovery/repo-routes.ts +347 -0
- package/src/audit/engine.ts +620 -0
- package/src/audit/fixes/index.ts +209 -0
- package/src/audit/fixes/social-meta-fixes.test.ts +329 -0
- package/src/audit/fixes/social-meta-fixes.ts +463 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/runner.test.ts +299 -0
- package/src/audit/runner.ts +130 -0
- package/src/audit/types.ts +1953 -0
- package/src/content/featured-snippet.ts +367 -0
- package/src/content/generator.test.ts +534 -0
- package/src/content/generator.ts +501 -0
- package/src/content/headline.ts +317 -0
- package/src/content/index.ts +62 -0
- package/src/content/intent.ts +258 -0
- package/src/content/keyword-density.ts +349 -0
- package/src/content/readability.ts +262 -0
- package/src/executor.ts +336 -0
- package/src/fixer.ts +416 -0
- package/src/frameworks/detector.test.ts +248 -0
- package/src/frameworks/detector.ts +371 -0
- package/src/frameworks/index.ts +68 -0
- package/src/frameworks/recipes/angular.yaml +171 -0
- package/src/frameworks/recipes/astro.yaml +206 -0
- package/src/frameworks/recipes/django.yaml +180 -0
- package/src/frameworks/recipes/laravel.yaml +137 -0
- package/src/frameworks/recipes/nextjs.yaml +268 -0
- package/src/frameworks/recipes/nuxt.yaml +175 -0
- package/src/frameworks/recipes/rails.yaml +188 -0
- package/src/frameworks/recipes/react.yaml +202 -0
- package/src/frameworks/recipes/sveltekit.yaml +154 -0
- package/src/frameworks/recipes/vue.yaml +137 -0
- package/src/frameworks/recipes/wordpress.yaml +209 -0
- package/src/frameworks/suggestion-engine.ts +320 -0
- package/src/geo/geo-content.test.ts +305 -0
- package/src/geo/geo-content.ts +266 -0
- package/src/geo/geo-history.test.ts +473 -0
- package/src/geo/geo-history.ts +433 -0
- package/src/geo/geo-tracker.test.ts +359 -0
- package/src/geo/geo-tracker.ts +411 -0
- package/src/geo/index.ts +10 -0
- package/src/git/commit-helper.test.ts +261 -0
- package/src/git/commit-helper.ts +329 -0
- package/src/git/index.ts +12 -0
- package/src/git/pr-helper.test.ts +284 -0
- package/src/git/pr-helper.ts +307 -0
- package/src/index.ts +66 -0
- package/src/keywords/ai-keyword-engine.ts +1062 -0
- package/src/keywords/ai-summarizer.ts +387 -0
- package/src/keywords/ci-mode.ts +555 -0
- package/src/keywords/engine.ts +359 -0
- package/src/keywords/index.ts +151 -0
- package/src/keywords/llm-judge.ts +357 -0
- package/src/keywords/nlp-analysis.ts +706 -0
- package/src/keywords/prioritizer.ts +295 -0
- package/src/keywords/site-crawler.ts +342 -0
- package/src/keywords/sources/autocomplete.ts +139 -0
- package/src/keywords/sources/competitive-search.ts +450 -0
- package/src/keywords/sources/competitor-analysis.ts +374 -0
- package/src/keywords/sources/dataforseo.ts +206 -0
- package/src/keywords/sources/free-sources.ts +294 -0
- package/src/keywords/sources/gsc.ts +123 -0
- package/src/keywords/topic-grouping.ts +327 -0
- package/src/keywords/types.ts +144 -0
- package/src/keywords/wizard.ts +457 -0
- package/src/loader.ts +40 -0
- package/src/reports/index.ts +7 -0
- package/src/reports/report-generator.test.ts +293 -0
- package/src/reports/report-generator.ts +713 -0
- package/src/scheduler/alerts.test.ts +458 -0
- package/src/scheduler/alerts.ts +328 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/scheduled-audit.test.ts +377 -0
- package/src/scheduler/scheduled-audit.ts +149 -0
- package/src/test/integration-test.ts +325 -0
- package/src/tools/analyzer.ts +373 -0
- package/src/tools/crawl.ts +293 -0
- package/src/tools/files.ts +301 -0
- package/src/tools/h1-fixer.ts +249 -0
- package/src/tools/index.ts +67 -0
- package/src/tracking/github-action.ts +326 -0
- package/src/tracking/google-analytics.ts +265 -0
- package/src/tracking/index.ts +45 -0
- package/src/tracking/report-generator.ts +386 -0
- package/src/tracking/search-console.ts +335 -0
- package/src/types.ts +134 -0
- package/src/utils/http.ts +302 -0
- package/src/wasm-adapter.ts +297 -0
- package/src/wasm-entry.ts +14 -0
- package/tsconfig.json +17 -0
- package/tsup.wasm.config.ts +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Commit Helper
|
|
3
|
+
*
|
|
4
|
+
* Creates conventional commits for SEO fixes with proper co-authoring.
|
|
5
|
+
* Supports GitHub Pages and various static site generators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync, exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
// System name is configurable (TBD - SEO Autopilot is temporary)
|
|
14
|
+
const SYSTEM_NAME = process.env.SEO_SYSTEM_NAME || 'SEO Autopilot';
|
|
15
|
+
const SYSTEM_EMAIL = process.env.SEO_SYSTEM_EMAIL || 'bot@seo-autopilot.dev';
|
|
16
|
+
|
|
17
|
+
export interface CommitConfig {
|
|
18
|
+
systemName?: string;
|
|
19
|
+
systemEmail?: string;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConventionalCommit {
|
|
24
|
+
type: 'fix' | 'feat' | 'docs' | 'style' | 'refactor' | 'perf' | 'chore';
|
|
25
|
+
scope?: string;
|
|
26
|
+
description: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
breaking?: boolean;
|
|
29
|
+
issues?: string[]; // Issue codes like TITLE_MISSING
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SEOFixCommit {
|
|
33
|
+
category: string;
|
|
34
|
+
issues: string[];
|
|
35
|
+
filesChanged: string[];
|
|
36
|
+
description: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Maps SEO issue categories to conventional commit types and scopes
|
|
41
|
+
*/
|
|
42
|
+
const CATEGORY_MAPPING: Record<string, { type: ConventionalCommit['type']; scope: string }> = {
|
|
43
|
+
'crawlability': { type: 'fix', scope: 'seo' },
|
|
44
|
+
'indexability': { type: 'fix', scope: 'seo' },
|
|
45
|
+
'on-page': { type: 'fix', scope: 'meta' },
|
|
46
|
+
'content': { type: 'fix', scope: 'content' },
|
|
47
|
+
'links': { type: 'fix', scope: 'links' },
|
|
48
|
+
'images': { type: 'fix', scope: 'images' },
|
|
49
|
+
'structured-data': { type: 'feat', scope: 'schema' },
|
|
50
|
+
'performance': { type: 'perf', scope: 'web' },
|
|
51
|
+
'security': { type: 'fix', scope: 'security' },
|
|
52
|
+
'mobile': { type: 'fix', scope: 'mobile' },
|
|
53
|
+
'international': { type: 'fix', scope: 'i18n' },
|
|
54
|
+
'social-meta': { type: 'feat', scope: 'og' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Formats a conventional commit message
|
|
59
|
+
*/
|
|
60
|
+
export function formatConventionalCommit(commit: ConventionalCommit): string {
|
|
61
|
+
let message = commit.type;
|
|
62
|
+
|
|
63
|
+
if (commit.scope) {
|
|
64
|
+
message += `(${commit.scope})`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (commit.breaking) {
|
|
68
|
+
message += '!';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
message += `: ${commit.description}`;
|
|
72
|
+
|
|
73
|
+
if (commit.body) {
|
|
74
|
+
message += `\n\n${commit.body}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (commit.issues && commit.issues.length > 0) {
|
|
78
|
+
message += `\n\nIssue codes: ${commit.issues.join(', ')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return message;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Formats a commit message for SEO fixes
|
|
86
|
+
*/
|
|
87
|
+
export function formatSEOCommitMessage(fix: SEOFixCommit, config?: CommitConfig): string {
|
|
88
|
+
const mapping = CATEGORY_MAPPING[fix.category] || { type: 'fix', scope: 'seo' };
|
|
89
|
+
const systemName = config?.systemName || SYSTEM_NAME;
|
|
90
|
+
const systemEmail = config?.systemEmail || SYSTEM_EMAIL;
|
|
91
|
+
|
|
92
|
+
const commit: ConventionalCommit = {
|
|
93
|
+
type: mapping.type,
|
|
94
|
+
scope: mapping.scope,
|
|
95
|
+
description: fix.description,
|
|
96
|
+
body: `Files changed:\n${fix.filesChanged.map(f => ` - ${f}`).join('\n')}`,
|
|
97
|
+
issues: fix.issues,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let message = formatConventionalCommit(commit);
|
|
101
|
+
|
|
102
|
+
// Add co-author trailer
|
|
103
|
+
message += `\n\nCo-Authored-By: ${systemName} <${systemEmail}>`;
|
|
104
|
+
|
|
105
|
+
return message;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a git commit with the SEO fix
|
|
110
|
+
*/
|
|
111
|
+
export async function createSEOCommit(
|
|
112
|
+
fix: SEOFixCommit,
|
|
113
|
+
config?: CommitConfig
|
|
114
|
+
): Promise<{ success: boolean; hash?: string; error?: string }> {
|
|
115
|
+
const message = formatSEOCommitMessage(fix, config);
|
|
116
|
+
|
|
117
|
+
if (config?.dryRun) {
|
|
118
|
+
console.log('DRY RUN - Would create commit:');
|
|
119
|
+
console.log('---');
|
|
120
|
+
console.log(message);
|
|
121
|
+
console.log('---');
|
|
122
|
+
return { success: true, hash: 'dry-run' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Stage the files
|
|
127
|
+
for (const file of fix.filesChanged) {
|
|
128
|
+
await execAsync(`git add "${file}"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create the commit using heredoc for proper message formatting
|
|
132
|
+
const { stdout } = await execAsync(`git commit -m "$(cat <<'EOF'
|
|
133
|
+
${message}
|
|
134
|
+
EOF
|
|
135
|
+
)"`);
|
|
136
|
+
|
|
137
|
+
// Extract commit hash
|
|
138
|
+
const hashMatch = stdout.match(/\[[\w-]+ ([a-f0-9]+)\]/);
|
|
139
|
+
const hash = hashMatch ? hashMatch[1] : undefined;
|
|
140
|
+
|
|
141
|
+
return { success: true, hash };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { success: false, error: (error as Error).message };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates multiple commits, one per category
|
|
149
|
+
*/
|
|
150
|
+
export async function createSEOCommits(
|
|
151
|
+
fixes: SEOFixCommit[],
|
|
152
|
+
config?: CommitConfig
|
|
153
|
+
): Promise<{ commits: Array<{ fix: SEOFixCommit; result: Awaited<ReturnType<typeof createSEOCommit>> }> }> {
|
|
154
|
+
const commits = [];
|
|
155
|
+
|
|
156
|
+
for (const fix of fixes) {
|
|
157
|
+
const result = await createSEOCommit(fix, config);
|
|
158
|
+
commits.push({ fix, result });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { commits };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks if the current directory is a git repository
|
|
166
|
+
*/
|
|
167
|
+
export function isGitRepo(dir?: string): boolean {
|
|
168
|
+
try {
|
|
169
|
+
const cwd = dir ? { cwd: dir } : undefined;
|
|
170
|
+
execSync('git rev-parse --is-inside-work-tree', { ...cwd, stdio: 'ignore' });
|
|
171
|
+
return true;
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Initializes a git repository if not already initialized
|
|
179
|
+
*/
|
|
180
|
+
export async function ensureGitRepo(dir?: string): Promise<void> {
|
|
181
|
+
if (!isGitRepo(dir)) {
|
|
182
|
+
const cwd = dir ? { cwd: dir } : undefined;
|
|
183
|
+
await execAsync('git init', cwd);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Gets the current git user config
|
|
189
|
+
*/
|
|
190
|
+
export function getGitUser(): { name?: string; email?: string } {
|
|
191
|
+
try {
|
|
192
|
+
const name = execSync('git config user.name', { encoding: 'utf8' }).trim();
|
|
193
|
+
const email = execSync('git config user.email', { encoding: 'utf8' }).trim();
|
|
194
|
+
return { name, email };
|
|
195
|
+
} catch {
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Detects if this is a GitHub Pages repository
|
|
202
|
+
*/
|
|
203
|
+
export function detectGitHubPages(dir: string): {
|
|
204
|
+
isGitHubPages: boolean;
|
|
205
|
+
type?: 'jekyll' | 'static' | 'docs' | 'custom';
|
|
206
|
+
baseUrl?: string;
|
|
207
|
+
} {
|
|
208
|
+
const fs = require('fs');
|
|
209
|
+
const path = require('path');
|
|
210
|
+
|
|
211
|
+
// Check for common GitHub Pages indicators
|
|
212
|
+
const checks = [
|
|
213
|
+
{ file: '_config.yml', type: 'jekyll' as const },
|
|
214
|
+
{ file: 'docs/index.html', type: 'docs' as const },
|
|
215
|
+
{ file: 'CNAME', type: 'custom' as const },
|
|
216
|
+
{ file: '.nojekyll', type: 'static' as const },
|
|
217
|
+
{ file: 'index.html', type: 'static' as const },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
for (const check of checks) {
|
|
221
|
+
const filePath = path.join(dir, check.file);
|
|
222
|
+
if (fs.existsSync(filePath)) {
|
|
223
|
+
// Try to extract base URL from _config.yml
|
|
224
|
+
if (check.file === '_config.yml') {
|
|
225
|
+
try {
|
|
226
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
227
|
+
const baseUrlMatch = content.match(/baseurl:\s*["']?([^"'\n]+)/);
|
|
228
|
+
const urlMatch = content.match(/url:\s*["']?([^"'\n]+)/);
|
|
229
|
+
return {
|
|
230
|
+
isGitHubPages: true,
|
|
231
|
+
type: check.type,
|
|
232
|
+
baseUrl: urlMatch ? urlMatch[1] + (baseUrlMatch?.[1] || '') : undefined,
|
|
233
|
+
};
|
|
234
|
+
} catch {
|
|
235
|
+
// Continue
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for CNAME (custom domain)
|
|
240
|
+
if (check.file === 'CNAME') {
|
|
241
|
+
try {
|
|
242
|
+
const cname = fs.readFileSync(path.join(dir, 'CNAME'), 'utf8').trim();
|
|
243
|
+
return {
|
|
244
|
+
isGitHubPages: true,
|
|
245
|
+
type: check.type,
|
|
246
|
+
baseUrl: `https://${cname}`,
|
|
247
|
+
};
|
|
248
|
+
} catch {
|
|
249
|
+
// Continue
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { isGitHubPages: true, type: check.type };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if remote is github.io
|
|
258
|
+
try {
|
|
259
|
+
const remote = execSync('git remote get-url origin', { cwd: dir, encoding: 'utf8' }).trim();
|
|
260
|
+
if (remote.includes('github.io') || remote.includes('.github.')) {
|
|
261
|
+
return { isGitHubPages: true, type: 'static' };
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Not a git repo or no remote
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { isGitHubPages: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generates a summary of commits made
|
|
272
|
+
*/
|
|
273
|
+
export function generateCommitSummary(
|
|
274
|
+
commits: Array<{ fix: SEOFixCommit; result: { success: boolean; hash?: string; error?: string } }>
|
|
275
|
+
): string {
|
|
276
|
+
const successful = commits.filter(c => c.result.success);
|
|
277
|
+
const failed = commits.filter(c => !c.result.success);
|
|
278
|
+
|
|
279
|
+
let summary = `## SEO Fix Commits Summary\n\n`;
|
|
280
|
+
summary += `✅ ${successful.length} commits created\n`;
|
|
281
|
+
if (failed.length > 0) {
|
|
282
|
+
summary += `❌ ${failed.length} commits failed\n`;
|
|
283
|
+
}
|
|
284
|
+
summary += '\n';
|
|
285
|
+
|
|
286
|
+
if (successful.length > 0) {
|
|
287
|
+
summary += `### Commits Created\n\n`;
|
|
288
|
+
for (const { fix, result } of successful) {
|
|
289
|
+
const mapping = CATEGORY_MAPPING[fix.category] || { type: 'fix', scope: 'seo' };
|
|
290
|
+
summary += `- \`${result.hash?.slice(0, 7) || 'N/A'}\` ${mapping.type}(${mapping.scope}): ${fix.description}\n`;
|
|
291
|
+
summary += ` - Files: ${fix.filesChanged.join(', ')}\n`;
|
|
292
|
+
summary += ` - Issues: ${fix.issues.join(', ')}\n`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (failed.length > 0) {
|
|
297
|
+
summary += `\n### Failed Commits\n\n`;
|
|
298
|
+
for (const { fix, result } of failed) {
|
|
299
|
+
summary += `- ❌ ${fix.description}: ${result.error}\n`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return summary;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Export types for conventional commit categories
|
|
307
|
+
export const COMMIT_TYPES = [
|
|
308
|
+
{ type: 'fix', description: 'Bug fix (SEO issue correction)' },
|
|
309
|
+
{ type: 'feat', description: 'New feature (schema, OG tags, etc.)' },
|
|
310
|
+
{ type: 'docs', description: 'Documentation update' },
|
|
311
|
+
{ type: 'style', description: 'Code style changes (formatting)' },
|
|
312
|
+
{ type: 'refactor', description: 'Code restructuring' },
|
|
313
|
+
{ type: 'perf', description: 'Performance improvement' },
|
|
314
|
+
{ type: 'chore', description: 'Maintenance task' },
|
|
315
|
+
] as const;
|
|
316
|
+
|
|
317
|
+
export const SEO_SCOPES = [
|
|
318
|
+
'seo', // General SEO
|
|
319
|
+
'meta', // Meta tags (title, description)
|
|
320
|
+
'og', // Open Graph / Twitter Cards
|
|
321
|
+
'schema', // Structured data / JSON-LD
|
|
322
|
+
'images', // Image optimization
|
|
323
|
+
'links', // Link fixes
|
|
324
|
+
'content', // Content optimization
|
|
325
|
+
'security', // Security headers
|
|
326
|
+
'mobile', // Mobile optimization
|
|
327
|
+
'i18n', // Internationalization
|
|
328
|
+
'perf', // Performance
|
|
329
|
+
] as const;
|
package/src/git/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git integration module
|
|
3
|
+
*
|
|
4
|
+
* Provides git operations for SEO fixes including:
|
|
5
|
+
* - Conventional commit formatting
|
|
6
|
+
* - Co-author support
|
|
7
|
+
* - GitHub Pages detection
|
|
8
|
+
* - Pull request generation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from './commit-helper.js';
|
|
12
|
+
export * from './pr-helper.js';
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
formatPRTitle,
|
|
4
|
+
formatPRBody,
|
|
5
|
+
generatePRDescription,
|
|
6
|
+
createPullRequest,
|
|
7
|
+
type PRConfig,
|
|
8
|
+
type SEOFixSummary,
|
|
9
|
+
type PRResult,
|
|
10
|
+
} from './pr-helper.js';
|
|
11
|
+
|
|
12
|
+
describe('pr-helper', () => {
|
|
13
|
+
describe('formatPRTitle', () => {
|
|
14
|
+
it('formats title for single category fix', () => {
|
|
15
|
+
const fixes: SEOFixSummary[] = [
|
|
16
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 2 },
|
|
17
|
+
];
|
|
18
|
+
expect(formatPRTitle(fixes)).toBe('fix(meta): SEO improvements - 1 issue fixed');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('formats title for multiple issues in same category', () => {
|
|
22
|
+
const fixes: SEOFixSummary[] = [
|
|
23
|
+
{ category: 'on-page', issues: ['TITLE_MISSING', 'META_DESC_MISSING'], filesCount: 2 },
|
|
24
|
+
];
|
|
25
|
+
expect(formatPRTitle(fixes)).toBe('fix(meta): SEO improvements - 2 issues fixed');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('formats title for multiple categories', () => {
|
|
29
|
+
const fixes: SEOFixSummary[] = [
|
|
30
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
31
|
+
{ category: 'social-meta', issues: ['OG_TITLE_MISSING', 'OG_IMAGE_MISSING'], filesCount: 1 },
|
|
32
|
+
];
|
|
33
|
+
expect(formatPRTitle(fixes)).toBe('fix(seo): SEO improvements - 3 issues fixed');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles empty fixes array', () => {
|
|
37
|
+
expect(formatPRTitle([])).toBe('fix(seo): SEO audit fixes');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('uses feat for new features like schema', () => {
|
|
41
|
+
const fixes: SEOFixSummary[] = [
|
|
42
|
+
{ category: 'structured-data', issues: ['SCHEMA_MISSING'], filesCount: 1 },
|
|
43
|
+
];
|
|
44
|
+
expect(formatPRTitle(fixes)).toBe('feat(schema): SEO improvements - 1 issue fixed');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('formatPRBody', () => {
|
|
49
|
+
it('generates body with summary section', () => {
|
|
50
|
+
const fixes: SEOFixSummary[] = [
|
|
51
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 2 },
|
|
52
|
+
];
|
|
53
|
+
const body = formatPRBody(fixes);
|
|
54
|
+
|
|
55
|
+
expect(body).toContain('## Summary');
|
|
56
|
+
expect(body).toContain('1 SEO issue');
|
|
57
|
+
expect(body).toContain('2 files');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('includes issue codes in body', () => {
|
|
61
|
+
const fixes: SEOFixSummary[] = [
|
|
62
|
+
{ category: 'on-page', issues: ['TITLE_MISSING', 'META_DESC_MISSING'], filesCount: 2 },
|
|
63
|
+
];
|
|
64
|
+
const body = formatPRBody(fixes);
|
|
65
|
+
|
|
66
|
+
expect(body).toContain('TITLE_MISSING');
|
|
67
|
+
expect(body).toContain('META_DESC_MISSING');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes category breakdown', () => {
|
|
71
|
+
const fixes: SEOFixSummary[] = [
|
|
72
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
73
|
+
{ category: 'social-meta', issues: ['OG_TITLE_MISSING'], filesCount: 1 },
|
|
74
|
+
];
|
|
75
|
+
const body = formatPRBody(fixes);
|
|
76
|
+
|
|
77
|
+
expect(body).toContain('On-Page');
|
|
78
|
+
expect(body).toContain('Social Meta');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('includes test plan section', () => {
|
|
82
|
+
const fixes: SEOFixSummary[] = [
|
|
83
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
84
|
+
];
|
|
85
|
+
const body = formatPRBody(fixes);
|
|
86
|
+
|
|
87
|
+
expect(body).toContain('## Test plan');
|
|
88
|
+
expect(body).toContain('- [ ]');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('includes generated by footer', () => {
|
|
92
|
+
const fixes: SEOFixSummary[] = [];
|
|
93
|
+
const body = formatPRBody(fixes);
|
|
94
|
+
|
|
95
|
+
expect(body).toContain('Generated with');
|
|
96
|
+
expect(body).toContain('SEO Autopilot');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('includes score improvement if provided', () => {
|
|
100
|
+
const fixes: SEOFixSummary[] = [
|
|
101
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
102
|
+
];
|
|
103
|
+
const body = formatPRBody(fixes, { scoreBefore: 65, scoreAfter: 82 });
|
|
104
|
+
|
|
105
|
+
expect(body).toContain('65');
|
|
106
|
+
expect(body).toContain('82');
|
|
107
|
+
expect(body).toContain('+17');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('generatePRDescription', () => {
|
|
112
|
+
it('generates full PR description with title and body', () => {
|
|
113
|
+
const fixes: SEOFixSummary[] = [
|
|
114
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
115
|
+
];
|
|
116
|
+
const description = generatePRDescription(fixes);
|
|
117
|
+
|
|
118
|
+
expect(description.title).toBeDefined();
|
|
119
|
+
expect(description.body).toBeDefined();
|
|
120
|
+
expect(description.title).toContain('fix');
|
|
121
|
+
expect(description.body).toContain('## Summary');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes labels suggestion', () => {
|
|
125
|
+
const fixes: SEOFixSummary[] = [
|
|
126
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
127
|
+
];
|
|
128
|
+
const description = generatePRDescription(fixes);
|
|
129
|
+
|
|
130
|
+
expect(description.labels).toContain('seo');
|
|
131
|
+
expect(description.labels).toContain('automated');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('suggests reviewers if configured', () => {
|
|
135
|
+
const fixes: SEOFixSummary[] = [
|
|
136
|
+
{ category: 'on-page', issues: ['TITLE_MISSING'], filesCount: 1 },
|
|
137
|
+
];
|
|
138
|
+
const config: PRConfig = {
|
|
139
|
+
reviewers: ['alice', 'bob'],
|
|
140
|
+
};
|
|
141
|
+
const description = generatePRDescription(fixes, config);
|
|
142
|
+
|
|
143
|
+
expect(description.reviewers).toEqual(['alice', 'bob']);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('createPullRequest', () => {
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
vi.resetAllMocks();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('returns PR URL on success', async () => {
|
|
153
|
+
const mockExec = vi.fn().mockResolvedValue({
|
|
154
|
+
stdout: 'https://github.com/owner/repo/pull/123\n',
|
|
155
|
+
stderr: '',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await createPullRequest({
|
|
159
|
+
title: 'Test PR',
|
|
160
|
+
body: 'Test body',
|
|
161
|
+
baseBranch: 'main',
|
|
162
|
+
headBranch: 'seo-fixes',
|
|
163
|
+
}, { execAsync: mockExec });
|
|
164
|
+
|
|
165
|
+
expect(result.success).toBe(true);
|
|
166
|
+
expect(result.url).toBe('https://github.com/owner/repo/pull/123');
|
|
167
|
+
expect(result.number).toBe(123);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('handles gh cli errors', async () => {
|
|
171
|
+
const mockExec = vi.fn().mockRejectedValue(new Error('gh: not logged in'));
|
|
172
|
+
|
|
173
|
+
const result = await createPullRequest({
|
|
174
|
+
title: 'Test PR',
|
|
175
|
+
body: 'Test body',
|
|
176
|
+
baseBranch: 'main',
|
|
177
|
+
headBranch: 'seo-fixes',
|
|
178
|
+
}, { execAsync: mockExec });
|
|
179
|
+
|
|
180
|
+
expect(result.success).toBe(false);
|
|
181
|
+
expect(result.error).toContain('not logged in');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('creates branch if needed', async () => {
|
|
185
|
+
const commands: string[] = [];
|
|
186
|
+
const mockExec = vi.fn().mockImplementation((cmd: string) => {
|
|
187
|
+
commands.push(cmd);
|
|
188
|
+
if (cmd.includes('gh pr create')) {
|
|
189
|
+
return Promise.resolve({ stdout: 'https://github.com/owner/repo/pull/456\n', stderr: '' });
|
|
190
|
+
}
|
|
191
|
+
return Promise.resolve({ stdout: '', stderr: '' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await createPullRequest({
|
|
195
|
+
title: 'Test PR',
|
|
196
|
+
body: 'Test body',
|
|
197
|
+
baseBranch: 'main',
|
|
198
|
+
headBranch: 'seo-fixes',
|
|
199
|
+
createBranch: true,
|
|
200
|
+
}, { execAsync: mockExec });
|
|
201
|
+
|
|
202
|
+
expect(commands.some(c => c.includes('checkout -b'))).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('supports dry run mode', async () => {
|
|
206
|
+
const mockExec = vi.fn();
|
|
207
|
+
|
|
208
|
+
const result = await createPullRequest({
|
|
209
|
+
title: 'Test PR',
|
|
210
|
+
body: 'Test body',
|
|
211
|
+
baseBranch: 'main',
|
|
212
|
+
headBranch: 'seo-fixes',
|
|
213
|
+
dryRun: true,
|
|
214
|
+
}, { execAsync: mockExec });
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(true);
|
|
217
|
+
expect(result.dryRun).toBe(true);
|
|
218
|
+
expect(mockExec).not.toHaveBeenCalledWith(expect.stringContaining('gh pr create'));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('includes labels in gh command', async () => {
|
|
222
|
+
let capturedCommand = '';
|
|
223
|
+
const mockExec = vi.fn().mockImplementation((cmd: string) => {
|
|
224
|
+
capturedCommand = cmd;
|
|
225
|
+
return Promise.resolve({ stdout: 'https://github.com/owner/repo/pull/789\n', stderr: '' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await createPullRequest({
|
|
229
|
+
title: 'Test PR',
|
|
230
|
+
body: 'Test body',
|
|
231
|
+
baseBranch: 'main',
|
|
232
|
+
headBranch: 'seo-fixes',
|
|
233
|
+
labels: ['seo', 'automated'],
|
|
234
|
+
}, { execAsync: mockExec });
|
|
235
|
+
|
|
236
|
+
expect(capturedCommand).toContain('--label');
|
|
237
|
+
expect(capturedCommand).toContain('seo');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('integration scenarios', () => {
|
|
242
|
+
it('generates complete PR for typical SEO audit', () => {
|
|
243
|
+
const fixes: SEOFixSummary[] = [
|
|
244
|
+
{
|
|
245
|
+
category: 'on-page',
|
|
246
|
+
issues: ['TITLE_MISSING', 'META_DESC_MISSING', 'H1_MISSING'],
|
|
247
|
+
filesCount: 5
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
category: 'social-meta',
|
|
251
|
+
issues: ['OG_TITLE_MISSING', 'OG_IMAGE_MISSING', 'TWITTER_CARD_MISSING'],
|
|
252
|
+
filesCount: 5
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
category: 'crawlability',
|
|
256
|
+
issues: ['ROBOTS_TXT_MISSING', 'SITEMAP_MISSING'],
|
|
257
|
+
filesCount: 2
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const description = generatePRDescription(fixes, {
|
|
262
|
+
scoreBefore: 45,
|
|
263
|
+
scoreAfter: 78,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(description.title).toContain('8 issues fixed');
|
|
267
|
+
expect(description.body).toContain('+33');
|
|
268
|
+
expect(description.body).toContain('ROBOTS_TXT_MISSING');
|
|
269
|
+
expect(description.body).toContain('OG_TITLE_MISSING');
|
|
270
|
+
expect(description.labels).toContain('seo');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles single-file schema addition', () => {
|
|
274
|
+
const fixes: SEOFixSummary[] = [
|
|
275
|
+
{ category: 'structured-data', issues: ['SCHEMA_MISSING'], filesCount: 1 },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const description = generatePRDescription(fixes);
|
|
279
|
+
|
|
280
|
+
expect(description.title).toContain('feat(schema)');
|
|
281
|
+
expect(description.body).toContain('SCHEMA_MISSING');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|