cc-codeconductor 0.2.8 → 0.2.10
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 +113 -26
- package/dist/index.js +1752 -81
- package/package.json +1 -1
- package/presets/agy/AGENTS.md +354 -0
- package/presets/agy/README.md +47 -0
- package/presets/agy/hooks.json +30 -0
- package/presets/agy/mcp_config.json +3 -0
- package/presets/agy/rules/commit-style.md +1 -0
- package/presets/agy/rules/graphify.md +14 -0
- package/presets/agy/scripts/post-tool.sh +25 -0
- package/presets/agy/scripts/pre-tool.sh +56 -0
- package/presets/agy/settings.json +8 -0
- package/presets/agy/skills/cc-api-contract/SKILL.md +71 -0
- package/presets/agy/skills/cc-db-migration/SKILL.md +70 -0
- package/presets/agy/skills/cc-feature/SKILL.md +115 -0
- package/presets/agy/skills/cc-fix/SKILL.md +124 -0
- package/presets/agy/skills/cc-pagespeed/SKILL.md +101 -0
- package/presets/agy/skills/cc-refactor/SKILL.md +149 -0
- package/presets/agy/skills/cc-review/SKILL.md +142 -0
- package/presets/agy/skills/cc-tdd-cycle/SKILL.md +226 -0
- package/presets/agy/skills/cc-test-plan/SKILL.md +145 -0
- package/presets/agy/skills/commit/SKILL.md +5 -0
- package/presets/agy/workflows/cc-api-contract.md +71 -0
- package/presets/agy/workflows/cc-db-migration.md +70 -0
- package/presets/agy/workflows/cc-feature.md +115 -0
- package/presets/agy/workflows/cc-fix.md +124 -0
- package/presets/agy/workflows/cc-pagespeed.md +101 -0
- package/presets/agy/workflows/cc-refactor.md +149 -0
- package/presets/agy/workflows/cc-review.md +142 -0
- package/presets/agy/workflows/cc-tdd-cycle.md +226 -0
- package/presets/agy/workflows/cc-test-plan.md +145 -0
- package/presets/agy/workflows/commit.md +1 -0
- package/presets/claude/CLAUDE.md +13 -3
- package/presets/claude/claude.json +6 -0
- package/presets/claude/commands/cc/pagespeed.md +103 -0
- package/presets/claude/settings.json +249 -2
- package/presets/claude/skills/conductor-setup/SKILL.md +125 -0
- package/presets/claude/skills/find-skills/SKILL.md +142 -0
- package/presets/claude/skills/multi-agent-orchestration/README.md +144 -0
- package/presets/claude/skills/multi-agent-orchestration/SKILL.md +579 -0
- package/presets/claude/skills/multi-agent-orchestration/examples/framework_implementations.py +362 -0
- package/presets/claude/skills/multi-agent-orchestration/examples/orchestration_patterns.py +411 -0
- package/presets/claude/skills/multi-agent-orchestration/scripts/agent_communication.py +334 -0
- package/presets/claude/skills/multi-agent-orchestration/scripts/benchmarking.py +341 -0
- package/presets/claude/skills/multi-agent-orchestration/scripts/workflow_management.py +334 -0
- package/presets/claude/skills/pagespeed-insights/SKILL.md +443 -0
- package/presets/claude/skills/pagespeed-insights/reference.md +50 -0
- package/presets/claude/skills/pagespeed-perf/SKILL.md +279 -0
- package/presets/claude/skills/workflow-orchestration-patterns/SKILL.md +98 -0
- package/presets/claude/skills/workflow-orchestration-patterns/references/details.md +223 -0
- package/presets/codex/AGENTS.md +19 -14
- package/presets/codex/skills/conductor-setup/SKILL.md +125 -0
- package/presets/codex/skills/find-skills/SKILL.md +142 -0
- package/presets/codex/skills/multi-agent-orchestration/README.md +144 -0
- package/presets/codex/skills/multi-agent-orchestration/SKILL.md +579 -0
- package/presets/codex/skills/multi-agent-orchestration/examples/framework_implementations.py +362 -0
- package/presets/codex/skills/multi-agent-orchestration/examples/orchestration_patterns.py +411 -0
- package/presets/codex/skills/multi-agent-orchestration/scripts/agent_communication.py +334 -0
- package/presets/codex/skills/multi-agent-orchestration/scripts/benchmarking.py +341 -0
- package/presets/codex/skills/multi-agent-orchestration/scripts/workflow_management.py +334 -0
- package/presets/codex/skills/pagespeed-insights/SKILL.md +443 -0
- package/presets/codex/skills/pagespeed-insights/reference.md +50 -0
- package/presets/codex/skills/pagespeed-perf/SKILL.md +279 -0
- package/presets/codex/skills/workflow-orchestration-patterns/SKILL.md +98 -0
- package/presets/codex/skills/workflow-orchestration-patterns/references/details.md +223 -0
- package/presets/opencode/README.md +11 -10
- package/presets/opencode/commands/cc-pagespeed.md +100 -0
- package/presets/opencode/skills/conductor-setup/SKILL.md +125 -0
- package/presets/opencode/skills/find-skills/SKILL.md +142 -0
- package/presets/opencode/skills/multi-agent-orchestration/README.md +144 -0
- package/presets/opencode/skills/multi-agent-orchestration/SKILL.md +579 -0
- package/presets/opencode/skills/multi-agent-orchestration/examples/framework_implementations.py +362 -0
- package/presets/opencode/skills/multi-agent-orchestration/examples/orchestration_patterns.py +411 -0
- package/presets/opencode/skills/multi-agent-orchestration/scripts/agent_communication.py +334 -0
- package/presets/opencode/skills/multi-agent-orchestration/scripts/benchmarking.py +341 -0
- package/presets/opencode/skills/multi-agent-orchestration/scripts/workflow_management.py +334 -0
- package/presets/opencode/skills/pagespeed-insights/SKILL.md +443 -0
- package/presets/opencode/skills/pagespeed-insights/reference.md +50 -0
- package/presets/opencode/skills/pagespeed-perf/SKILL.md +279 -0
- package/presets/opencode/skills/workflow-orchestration-patterns/SKILL.md +98 -0
- package/presets/opencode/skills/workflow-orchestration-patterns/references/details.md +223 -0
- package/presets/seo-hotel/commands/cc-seo-audit.md +17 -9
- package/presets/seo-hotel/commands/cc-seo-llms.md +64 -0
- package/presets/seo-hotel/seo-hotel.yml +55 -0
- package/presets/seo-hotel/settings.json +225 -0
- package/presets/seo-hotel/skills/find-skills/SKILL.md +142 -0
- package/presets/seo-hotel/skills/pagespeed-insights/SKILL.md +443 -0
- package/presets/seo-hotel/skills/pagespeed-insights/reference.md +50 -0
- package/src/presets/manifests/agy.yml +36 -2
- package/src/presets/manifests/claude.yml +5 -0
- package/src/presets/manifests/opencode.yml +4 -0
- package/src/presets/models/agy.yml +16 -0
package/dist/index.js
CHANGED
|
@@ -7044,7 +7044,7 @@ function getExitCode(error) {
|
|
|
7044
7044
|
// package.json
|
|
7045
7045
|
var package_default = {
|
|
7046
7046
|
name: "cc-codeconductor",
|
|
7047
|
-
version: "0.2.
|
|
7047
|
+
version: "0.2.10",
|
|
7048
7048
|
description: "A multi-agent orchestration framework for AI-assisted software engineering workflows.",
|
|
7049
7049
|
keywords: [
|
|
7050
7050
|
"ai",
|
|
@@ -11371,7 +11371,8 @@ var CodeConductorConfigSchema = exports_external.object({
|
|
|
11371
11371
|
}),
|
|
11372
11372
|
defaults: exports_external.object({
|
|
11373
11373
|
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor", "agy"]),
|
|
11374
|
-
overwrite: exports_external.boolean()
|
|
11374
|
+
overwrite: exports_external.boolean(),
|
|
11375
|
+
locale: exports_external.enum(["en", "es"]).optional().default("en")
|
|
11375
11376
|
}),
|
|
11376
11377
|
presets: exports_external.object({
|
|
11377
11378
|
council: exports_external.object({
|
|
@@ -11640,7 +11641,8 @@ var DEFAULT_CONFIG = {
|
|
|
11640
11641
|
},
|
|
11641
11642
|
defaults: {
|
|
11642
11643
|
target: "opencode",
|
|
11643
|
-
overwrite: false
|
|
11644
|
+
overwrite: false,
|
|
11645
|
+
locale: "en"
|
|
11644
11646
|
},
|
|
11645
11647
|
presets: {
|
|
11646
11648
|
council: {
|
|
@@ -11749,13 +11751,13 @@ function resolveAssets(target) {
|
|
|
11749
11751
|
case "cursor":
|
|
11750
11752
|
return ["agents", "prompts/v0.2.0"];
|
|
11751
11753
|
case "agy":
|
|
11752
|
-
return ["
|
|
11754
|
+
return ["AGENTS.md", "rules", "workflows", "skills", "prompts/v0.2.0"];
|
|
11753
11755
|
}
|
|
11754
11756
|
}
|
|
11755
11757
|
|
|
11756
11758
|
// src/commands/init.command.ts
|
|
11757
11759
|
async function initCommand(options) {
|
|
11758
|
-
const { projectRoot, dryRun, force, global: isGlobal, output } = options;
|
|
11760
|
+
const { projectRoot, dryRun, force, global: isGlobal, output, locale } = options;
|
|
11759
11761
|
const baseDir = isGlobal ? homedir() : projectRoot;
|
|
11760
11762
|
try {
|
|
11761
11763
|
let profile = null;
|
|
@@ -11779,7 +11781,8 @@ async function initCommand(options) {
|
|
|
11779
11781
|
},
|
|
11780
11782
|
defaults: {
|
|
11781
11783
|
target: "opencode",
|
|
11782
|
-
overwrite: force
|
|
11784
|
+
overwrite: force,
|
|
11785
|
+
locale
|
|
11783
11786
|
}
|
|
11784
11787
|
} : {
|
|
11785
11788
|
project: {
|
|
@@ -11788,7 +11791,8 @@ async function initCommand(options) {
|
|
|
11788
11791
|
},
|
|
11789
11792
|
defaults: {
|
|
11790
11793
|
target: "opencode",
|
|
11791
|
-
overwrite: force
|
|
11794
|
+
overwrite: force,
|
|
11795
|
+
locale
|
|
11792
11796
|
}
|
|
11793
11797
|
};
|
|
11794
11798
|
const presetResolution = profile ? resolvePreset("opencode", profile) : null;
|
|
@@ -11904,7 +11908,7 @@ async function fileExists2(path) {
|
|
|
11904
11908
|
}
|
|
11905
11909
|
|
|
11906
11910
|
// src/commands/install.command.ts
|
|
11907
|
-
import { homedir as
|
|
11911
|
+
import { homedir as homedir3 } from "node:os";
|
|
11908
11912
|
import { resolve as resolve7 } from "node:path";
|
|
11909
11913
|
|
|
11910
11914
|
// src/domain/council/council-agent.ts
|
|
@@ -12312,8 +12316,117 @@ async function writeGeneratedFiles(files, options) {
|
|
|
12312
12316
|
|
|
12313
12317
|
// src/core/presets/file-copier.ts
|
|
12314
12318
|
import { mkdir as mkdir4, readdir, readFile as readFile4, stat, writeFile as writeFile4 } from "node:fs/promises";
|
|
12319
|
+
import { homedir as homedir2 } from "node:os";
|
|
12315
12320
|
import { dirname as dirname3, join as join2, relative, resolve as resolve5 } from "node:path";
|
|
12316
12321
|
|
|
12322
|
+
// src/core/i18n/language-instructions.ts
|
|
12323
|
+
var SUPPORTED_LOCALES = ["en", "es"];
|
|
12324
|
+
var LANGUAGE_INSTRUCTIONS = {
|
|
12325
|
+
en: 'Prose/docs/code comments: be terse and direct. Prefer concrete nouns over abstract ones. Omit filler phrases ("note that", "please", "as mentioned"). One idea per sentence.',
|
|
12326
|
+
es: "Spanish prose/docs/reports/Markdown: preserve natural Spanish orthography, including accents, `ñ`, `¿`, `¡`, and normal Unicode. The ASCII-only editing preference does not apply to these artifacts."
|
|
12327
|
+
};
|
|
12328
|
+
var COMMIT_STYLE = {
|
|
12329
|
+
en: `---
|
|
12330
|
+
trigger: always_on
|
|
12331
|
+
description: Rules for generating commit messages using Conventional Commits format
|
|
12332
|
+
---
|
|
12333
|
+
|
|
12334
|
+
## Git Commit Messages
|
|
12335
|
+
|
|
12336
|
+
### Format
|
|
12337
|
+
|
|
12338
|
+
\`\`\`
|
|
12339
|
+
<type>(<scope>): <short description>
|
|
12340
|
+
|
|
12341
|
+
- <detail 1>
|
|
12342
|
+
- <detail 2>
|
|
12343
|
+
\`\`\`
|
|
12344
|
+
|
|
12345
|
+
### Valid Types
|
|
12346
|
+
|
|
12347
|
+
\`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
|
|
12348
|
+
\`revert\`
|
|
12349
|
+
|
|
12350
|
+
### Rules
|
|
12351
|
+
|
|
12352
|
+
- Language: **neutral English** always
|
|
12353
|
+
- Header: maximum 69 characters, no trailing period
|
|
12354
|
+
- Body: concise bullet points, one idea per line
|
|
12355
|
+
- Footer: only for breaking changes or issues
|
|
12356
|
+
- No gerunds ("adding", "fixing")
|
|
12357
|
+
- Use infinitive or imperative ("add", "fix", "update")`,
|
|
12358
|
+
es: `---
|
|
12359
|
+
trigger: always_on
|
|
12360
|
+
description: Reglas para generar mensajes de commit con formato Conventional Commits
|
|
12361
|
+
---
|
|
12362
|
+
|
|
12363
|
+
## Git Commit Messages
|
|
12364
|
+
|
|
12365
|
+
### Formato
|
|
12366
|
+
|
|
12367
|
+
\`\`\`
|
|
12368
|
+
<tipo>(<scope>): <descripcion corta>
|
|
12369
|
+
|
|
12370
|
+
- <detalle 1>
|
|
12371
|
+
- <detalle 2>
|
|
12372
|
+
\`\`\`
|
|
12373
|
+
|
|
12374
|
+
### Tipos validos
|
|
12375
|
+
|
|
12376
|
+
\`feat\` \`fix\` \`docs\` \`style\` \`refactor\` \`test\` \`chore\` \`perf\` \`ci\` \`build\`
|
|
12377
|
+
\`revert\`
|
|
12378
|
+
|
|
12379
|
+
### Reglas
|
|
12380
|
+
|
|
12381
|
+
- Idioma: **inglés neutro** siempre
|
|
12382
|
+
- Encabezado: maximo 69 caracteres, sin punto final
|
|
12383
|
+
- Cuerpo: viñetas concisas, una idea por linea
|
|
12384
|
+
- Footer: solo para breaking changes o issues
|
|
12385
|
+
- Sin gerundios ("agregando", "corrigiendo")
|
|
12386
|
+
- Usar infinitivo o imperativo ("agregar", "corregir", "actualizar")`
|
|
12387
|
+
};
|
|
12388
|
+
var COMMIT_WORKFLOW = {
|
|
12389
|
+
en: `---
|
|
12390
|
+
name: commit
|
|
12391
|
+
description: Generates a commit in English following Conventional Commits based on staged changes
|
|
12392
|
+
---
|
|
12393
|
+
|
|
12394
|
+
// turbo-all
|
|
12395
|
+
|
|
12396
|
+
## Steps
|
|
12397
|
+
|
|
12398
|
+
1. View staged changes: \`git diff --cached --stat\`
|
|
12399
|
+
2. If nothing is staged, suggest \`git add .\` or specific files
|
|
12400
|
+
3. Get full diff: \`git diff --cached\`
|
|
12401
|
+
4. Check recent style: \`git log -n 3 --oneline\`
|
|
12402
|
+
5. Generate message following \`.agents/rules/commit-style.md\`
|
|
12403
|
+
6. Show proposal and confirm with the user
|
|
12404
|
+
7. Execute: \`git commit -m "<message>"\`
|
|
12405
|
+
8. Confirm success: \`git status\``,
|
|
12406
|
+
es: `---
|
|
12407
|
+
name: commit
|
|
12408
|
+
description: Genera un commit en inglés siguiendo Conventional Commits basado en los cambios staged
|
|
12409
|
+
---
|
|
12410
|
+
|
|
12411
|
+
// turbo-all
|
|
12412
|
+
|
|
12413
|
+
## Pasos
|
|
12414
|
+
|
|
12415
|
+
1. Ver cambios staged: \`git diff --cached --stat\`
|
|
12416
|
+
2. Si no hay nada staged, sugiere \`git add .\` o archivos específicos
|
|
12417
|
+
3. Obtener diff completo: \`git diff --cached\`
|
|
12418
|
+
4. Ver estilo reciente: \`git log -n 3 --oneline\`
|
|
12419
|
+
5. Generar mensaje siguiendo \`.agents/rules/commit-style.md\`
|
|
12420
|
+
6. Mostrar propuesta y confirmar con el usuario
|
|
12421
|
+
7. Ejecutar: \`git commit -m "<mensaje>"\`
|
|
12422
|
+
8. Confirmar éxito: \`git status\``
|
|
12423
|
+
};
|
|
12424
|
+
function getLanguageInstruction(locale) {
|
|
12425
|
+
const key = SUPPORTED_LOCALES.includes(locale) ? locale : "en";
|
|
12426
|
+
return `- ${LANGUAGE_INSTRUCTIONS[key]}`;
|
|
12427
|
+
}
|
|
12428
|
+
var LOCALE_PLACEHOLDER = "{{LANGUAGE_INSTRUCTIONS}}";
|
|
12429
|
+
|
|
12317
12430
|
// src/core/filesystem/safe-merger.ts
|
|
12318
12431
|
var MANAGED_BEGIN_MARKER = "<!-- CODECONDUCTOR:BEGIN managed -->";
|
|
12319
12432
|
var MANAGED_END_MARKER = "<!-- CODECONDUCTOR:END managed -->";
|
|
@@ -12410,12 +12523,14 @@ function extractAgentRole(filePath) {
|
|
|
12410
12523
|
];
|
|
12411
12524
|
return knownRoles.includes(name) ? name : null;
|
|
12412
12525
|
}
|
|
12413
|
-
function renderTemplate(content, modelConfig, filePath) {
|
|
12526
|
+
function renderTemplate(content, modelConfig, filePath, locale = "en") {
|
|
12527
|
+
const cleanLocale = locale === "es" || locale === "en" ? locale : "en";
|
|
12528
|
+
const templatedContent = content.replace(/\{\{COMMIT_STYLE\}\}/g, COMMIT_STYLE[cleanLocale]).replace(/\{\{COMMIT_WORKFLOW\}\}/g, COMMIT_WORKFLOW[cleanLocale]);
|
|
12414
12529
|
const agentRole = extractAgentRole(filePath);
|
|
12415
12530
|
if (agentRole && modelConfig.agents[agentRole]) {
|
|
12416
12531
|
const agentModels = modelConfig.agents[agentRole];
|
|
12417
12532
|
const targetModel = agentModels[modelConfig.target];
|
|
12418
|
-
let result2 =
|
|
12533
|
+
let result2 = templatedContent.replace(/\{\{MODEL\}\}/g, targetModel ?? "").replace(/\{\{MODEL_CLAUDE\}\}/g, agentModels.claude ?? "").replace(/\{\{MODEL_OPENCODE\}\}/g, agentModels.opencode ?? "").replace(/\{\{MODEL_CODEX\}\}/g, agentModels.codex ?? "").replace(/\{\{MODEL_GEMINI\}\}/g, agentModels.gemini ?? "").replace(/\{\{MODEL_CURSOR\}\}/g, agentModels.cursor ?? "").replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
|
|
12419
12534
|
if (modelConfig.tools || modelConfig.permissions) {
|
|
12420
12535
|
result2 = substituteToolNames(result2, modelConfig);
|
|
12421
12536
|
}
|
|
@@ -12431,7 +12546,7 @@ function renderTemplate(content, modelConfig, filePath) {
|
|
|
12431
12546
|
"docs",
|
|
12432
12547
|
"repo-explorer"
|
|
12433
12548
|
];
|
|
12434
|
-
let result =
|
|
12549
|
+
let result = templatedContent;
|
|
12435
12550
|
for (const role of knownRoles) {
|
|
12436
12551
|
if (!modelConfig.agents[role])
|
|
12437
12552
|
continue;
|
|
@@ -12445,6 +12560,7 @@ function renderTemplate(content, modelConfig, filePath) {
|
|
|
12445
12560
|
}
|
|
12446
12561
|
}
|
|
12447
12562
|
}
|
|
12563
|
+
result = result.replace(new RegExp(LOCALE_PLACEHOLDER.replace(/[{}]/g, "\\$&"), "g"), getLanguageInstruction(locale));
|
|
12448
12564
|
if (modelConfig.tools || modelConfig.permissions) {
|
|
12449
12565
|
result = substituteToolNames(result, modelConfig);
|
|
12450
12566
|
}
|
|
@@ -12454,12 +12570,12 @@ function substituteToolNames(content, modelConfig) {
|
|
|
12454
12570
|
if (!modelConfig.tools && !modelConfig.permissions)
|
|
12455
12571
|
return content;
|
|
12456
12572
|
const target = modelConfig.target;
|
|
12457
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
12573
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
12458
12574
|
if (!fmMatch)
|
|
12459
12575
|
return content;
|
|
12460
12576
|
const frontmatter = fmMatch[1];
|
|
12461
12577
|
if (target === "opencode" && modelConfig.permissions) {
|
|
12462
|
-
const updatedFrontmatter2 = frontmatter.replace(/^tools:\s*(.+)\n?/m, "");
|
|
12578
|
+
const updatedFrontmatter2 = frontmatter.replace(/^tools:\s*(.+)\r?\n?/m, "");
|
|
12463
12579
|
return content.replace(fmMatch[0], `---
|
|
12464
12580
|
${updatedFrontmatter2}
|
|
12465
12581
|
---`);
|
|
@@ -12481,7 +12597,7 @@ ${updatedFrontmatter2}
|
|
|
12481
12597
|
${updatedFrontmatter}
|
|
12482
12598
|
---`);
|
|
12483
12599
|
}
|
|
12484
|
-
async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTemplate, modelConfig) {
|
|
12600
|
+
async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTemplate, modelConfig, locale) {
|
|
12485
12601
|
if (strategy === "skip") {
|
|
12486
12602
|
return { src: srcPath, dest: destPath, action: "skipped", dryRun };
|
|
12487
12603
|
}
|
|
@@ -12491,7 +12607,7 @@ async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTem
|
|
|
12491
12607
|
} catch (e) {
|
|
12492
12608
|
return { src: srcPath, dest: destPath, action: "error", error: `Cannot read source: ${e}` };
|
|
12493
12609
|
}
|
|
12494
|
-
const incomingContent = isTemplate && modelConfig ? renderTemplate(content, modelConfig, srcPath) : content;
|
|
12610
|
+
const incomingContent = isTemplate && modelConfig ? renderTemplate(content, modelConfig, srcPath, locale) : content;
|
|
12495
12611
|
let finalContent = incomingContent;
|
|
12496
12612
|
let action = "written";
|
|
12497
12613
|
if (strategy === "append") {
|
|
@@ -12543,14 +12659,23 @@ async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTem
|
|
|
12543
12659
|
return { src: srcPath, dest: destPath, action: "error", error: String(e) };
|
|
12544
12660
|
}
|
|
12545
12661
|
}
|
|
12546
|
-
async function copyFromManifest(manifest, presetsDir, baseDir, isGlobal, dryRun, force, modelConfig = null) {
|
|
12662
|
+
async function copyFromManifest(manifest, presetsDir, baseDir, isGlobal, dryRun, force, modelConfig = null, locale = "en") {
|
|
12547
12663
|
const results = [];
|
|
12548
12664
|
for (const entry of manifest.entries) {
|
|
12549
12665
|
const strategy = isGlobal && entry.globalStrategy ? entry.globalStrategy : entry.strategy;
|
|
12550
|
-
|
|
12666
|
+
let resolvedBaseDir = baseDir;
|
|
12667
|
+
let resolvedEntry = entry;
|
|
12668
|
+
if (manifest.target === "agy" && isGlobal) {
|
|
12669
|
+
resolvedBaseDir = join2(homedir2(), ".gemini", "config");
|
|
12670
|
+
resolvedEntry = {
|
|
12671
|
+
...entry,
|
|
12672
|
+
dest: entry.dest.replace(/^\.agents\/?/, "")
|
|
12673
|
+
};
|
|
12674
|
+
}
|
|
12675
|
+
const files = await resolveEntryFiles(resolvedEntry, presetsDir, resolvedBaseDir);
|
|
12551
12676
|
const isTemplate = entry.template === true;
|
|
12552
12677
|
for (const { src, dest } of files) {
|
|
12553
|
-
results.push(await applySingleFile(src, dest, strategy, force, dryRun, isTemplate, modelConfig));
|
|
12678
|
+
results.push(await applySingleFile(src, dest, strategy, force, dryRun, isTemplate, modelConfig, locale));
|
|
12554
12679
|
}
|
|
12555
12680
|
}
|
|
12556
12681
|
return results;
|
|
@@ -12629,7 +12754,7 @@ function getIndividualTargets(target) {
|
|
|
12629
12754
|
// src/commands/install.command.ts
|
|
12630
12755
|
async function installCommand(options) {
|
|
12631
12756
|
const { target, dryRun, force, global: isGlobal, output, projectRoot } = options;
|
|
12632
|
-
const baseDir = isGlobal ?
|
|
12757
|
+
const baseDir = isGlobal ? homedir3() : projectRoot;
|
|
12633
12758
|
try {
|
|
12634
12759
|
const runnerTarget = parseRunnerTarget(target);
|
|
12635
12760
|
const targets = getIndividualTargets(runnerTarget);
|
|
@@ -12724,17 +12849,24 @@ async function installCommand(options) {
|
|
|
12724
12849
|
}
|
|
12725
12850
|
async function installPresetCommand(options) {
|
|
12726
12851
|
const { target, dryRun, force, global: isGlobal, projectRoot } = options;
|
|
12727
|
-
const baseDir = isGlobal ?
|
|
12852
|
+
const baseDir = isGlobal ? homedir3() : projectRoot;
|
|
12728
12853
|
try {
|
|
12729
12854
|
const runnerTarget = parseRunnerTarget(target);
|
|
12730
12855
|
const targets = getIndividualTargets(runnerTarget);
|
|
12731
12856
|
const profile = await detectProject(projectRoot);
|
|
12732
12857
|
const presetResolution = targets.map((t) => resolvePreset(t, profile));
|
|
12858
|
+
let locale = options.locale ?? "en";
|
|
12859
|
+
if (!options.locale) {
|
|
12860
|
+
const configResult = await loadConfig(projectRoot);
|
|
12861
|
+
if (configResult.success) {
|
|
12862
|
+
locale = configResult.data.defaults.locale ?? "en";
|
|
12863
|
+
}
|
|
12864
|
+
}
|
|
12733
12865
|
const allFileResults = [];
|
|
12734
12866
|
for (const t of targets) {
|
|
12735
12867
|
const manifest = await loadManifest(t);
|
|
12736
12868
|
const modelConfig = await loadModelConfig(t);
|
|
12737
|
-
const results = await copyFromManifest(manifest, PRESETS_DIR, baseDir, isGlobal, dryRun, force, modelConfig);
|
|
12869
|
+
const results = await copyFromManifest(manifest, PRESETS_DIR, baseDir, isGlobal, dryRun, force, modelConfig, locale);
|
|
12738
12870
|
for (const r of results) {
|
|
12739
12871
|
allFileResults.push({ target: t, ...r });
|
|
12740
12872
|
}
|
|
@@ -12749,6 +12881,7 @@ async function installPresetCommand(options) {
|
|
|
12749
12881
|
targets,
|
|
12750
12882
|
global: isGlobal,
|
|
12751
12883
|
dryRun,
|
|
12884
|
+
locale,
|
|
12752
12885
|
presetResolution,
|
|
12753
12886
|
fileResults: allFileResults
|
|
12754
12887
|
}
|
|
@@ -12766,10 +12899,20 @@ async function installPresetCommand(options) {
|
|
|
12766
12899
|
}
|
|
12767
12900
|
|
|
12768
12901
|
// src/commands/install-lsp.command.ts
|
|
12769
|
-
import { homedir as
|
|
12902
|
+
import { homedir as homedir5 } from "node:os";
|
|
12770
12903
|
import { resolve as resolve8 } from "node:path";
|
|
12771
12904
|
|
|
12772
12905
|
// src/core/lsp/lsp-config-utils.ts
|
|
12906
|
+
function getLanguageServerConfig(lspIds) {
|
|
12907
|
+
const languageServers = {};
|
|
12908
|
+
for (const lspId of lspIds) {
|
|
12909
|
+
const config = getLspCommand(lspId);
|
|
12910
|
+
if (config) {
|
|
12911
|
+
languageServers[lspId] = { command: config.command, args: [...config.args] };
|
|
12912
|
+
}
|
|
12913
|
+
}
|
|
12914
|
+
return languageServers;
|
|
12915
|
+
}
|
|
12773
12916
|
function getLspCommand(lspId) {
|
|
12774
12917
|
switch (lspId) {
|
|
12775
12918
|
case "typescript":
|
|
@@ -12777,7 +12920,7 @@ function getLspCommand(lspId) {
|
|
|
12777
12920
|
case "php":
|
|
12778
12921
|
return { command: "intelephense", args: ["--stdio"] };
|
|
12779
12922
|
case "python":
|
|
12780
|
-
return { command: "
|
|
12923
|
+
return { command: "pyright-langserver", args: ["--stdio"] };
|
|
12781
12924
|
case "kotlin":
|
|
12782
12925
|
return { command: "kotlin-language-server", args: [] };
|
|
12783
12926
|
default:
|
|
@@ -12836,19 +12979,11 @@ class ClaudeLspGenerator {
|
|
|
12836
12979
|
if (successfulLsps.length === 0) {
|
|
12837
12980
|
return [];
|
|
12838
12981
|
}
|
|
12839
|
-
const
|
|
12840
|
-
|
|
12841
|
-
const config = getLspCommand(lsp.lspId);
|
|
12842
|
-
if (config) {
|
|
12843
|
-
mcpServers[lsp.lspId] = { command: config.command, args: [...config.args] };
|
|
12844
|
-
}
|
|
12845
|
-
}
|
|
12846
|
-
const content = JSON.stringify({
|
|
12847
|
-
mcpServers
|
|
12848
|
-
}, null, 2);
|
|
12982
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12983
|
+
const content = JSON.stringify(languageServers, null, 2);
|
|
12849
12984
|
return [
|
|
12850
12985
|
{
|
|
12851
|
-
path: ".claude/
|
|
12986
|
+
path: ".claude/plugins/codeconductor-lsp/.lsp.json",
|
|
12852
12987
|
content,
|
|
12853
12988
|
overwrite: false
|
|
12854
12989
|
}
|
|
@@ -12863,8 +12998,6 @@ function createClaudeLspGenerator() {
|
|
|
12863
12998
|
}
|
|
12864
12999
|
|
|
12865
13000
|
// src/adapters/codex/codex-lsp-generator.ts
|
|
12866
|
-
var MCP_STARTUP_TIMEOUT_SEC = 120;
|
|
12867
|
-
|
|
12868
13001
|
class CodexLspGenerator {
|
|
12869
13002
|
name = "codex-lsp";
|
|
12870
13003
|
target = "codex";
|
|
@@ -12877,12 +13010,11 @@ class CodexLspGenerator {
|
|
|
12877
13010
|
for (const lsp of successfulLsps) {
|
|
12878
13011
|
const config = getLspCommand(lsp.lspId);
|
|
12879
13012
|
if (config) {
|
|
12880
|
-
sections.push(`[
|
|
13013
|
+
sections.push(`[language_servers.${lsp.lspId}]`);
|
|
12881
13014
|
sections.push(`command = "${config.command}"`);
|
|
12882
13015
|
if (config.args.length > 0) {
|
|
12883
13016
|
sections.push(`args = [${config.args.map((a) => `"${a}"`).join(", ")}]`);
|
|
12884
13017
|
}
|
|
12885
|
-
sections.push(`startup_timeout_sec = ${MCP_STARTUP_TIMEOUT_SEC}`);
|
|
12886
13018
|
sections.push("");
|
|
12887
13019
|
}
|
|
12888
13020
|
}
|
|
@@ -12912,19 +13044,13 @@ class CursorLspGenerator {
|
|
|
12912
13044
|
if (successfulLsps.length === 0) {
|
|
12913
13045
|
return [];
|
|
12914
13046
|
}
|
|
12915
|
-
const
|
|
12916
|
-
for (const lsp of successfulLsps) {
|
|
12917
|
-
const config = getLspCommand(lsp.lspId);
|
|
12918
|
-
if (config) {
|
|
12919
|
-
mcpServers[lsp.lspId] = { command: config.command, args: [...config.args] };
|
|
12920
|
-
}
|
|
12921
|
-
}
|
|
13047
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12922
13048
|
const content = JSON.stringify({
|
|
12923
|
-
|
|
13049
|
+
languageServers
|
|
12924
13050
|
}, null, 2);
|
|
12925
13051
|
return [
|
|
12926
13052
|
{
|
|
12927
|
-
path: ".cursor/
|
|
13053
|
+
path: ".cursor/settings.json",
|
|
12928
13054
|
content,
|
|
12929
13055
|
overwrite: false
|
|
12930
13056
|
}
|
|
@@ -12947,15 +13073,9 @@ class GeminiLspGenerator {
|
|
|
12947
13073
|
if (successfulLsps.length === 0) {
|
|
12948
13074
|
return [];
|
|
12949
13075
|
}
|
|
12950
|
-
const
|
|
12951
|
-
for (const lsp of successfulLsps) {
|
|
12952
|
-
const config = getLspCommand(lsp.lspId);
|
|
12953
|
-
if (config) {
|
|
12954
|
-
mcpServers[lsp.lspId] = { command: config.command, args: [...config.args] };
|
|
12955
|
-
}
|
|
12956
|
-
}
|
|
13076
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12957
13077
|
const content = JSON.stringify({
|
|
12958
|
-
|
|
13078
|
+
languageServers
|
|
12959
13079
|
}, null, 2);
|
|
12960
13080
|
return [
|
|
12961
13081
|
{
|
|
@@ -12974,38 +13094,45 @@ function createGeminiLspGenerator() {
|
|
|
12974
13094
|
}
|
|
12975
13095
|
|
|
12976
13096
|
// src/adapters/opencode/opencode-lsp-generator.ts
|
|
13097
|
+
var OPENCODE_LSP_EXTENSIONS = {
|
|
13098
|
+
typescript: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
13099
|
+
php: [".php"],
|
|
13100
|
+
python: [".py", ".pyi"],
|
|
13101
|
+
kotlin: [".kt", ".kts"]
|
|
13102
|
+
};
|
|
13103
|
+
|
|
12977
13104
|
class OpenCodeLspGenerator {
|
|
12978
13105
|
name = "opencode-lsp";
|
|
12979
13106
|
target = "opencode";
|
|
12980
13107
|
generate(installedLsps) {
|
|
12981
|
-
const successfulLsps = installedLsps.filter((
|
|
13108
|
+
const successfulLsps = installedLsps.filter((lsp2) => lsp2.status !== "failed");
|
|
12982
13109
|
if (successfulLsps.length === 0) {
|
|
12983
13110
|
return [];
|
|
12984
13111
|
}
|
|
12985
|
-
const
|
|
12986
|
-
|
|
12987
|
-
const config = getLspCommand(lsp.lspId);
|
|
12988
|
-
if (config) {
|
|
12989
|
-
mcp[lsp.lspId] = {
|
|
12990
|
-
type: "local",
|
|
12991
|
-
command: [config.command, ...config.args],
|
|
12992
|
-
enabled: true,
|
|
12993
|
-
timeout: 120000
|
|
12994
|
-
};
|
|
12995
|
-
}
|
|
12996
|
-
}
|
|
13112
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp2) => lsp2.lspId));
|
|
13113
|
+
const lsp = this.toOpenCodeLspConfig(languageServers);
|
|
12997
13114
|
const content = JSON.stringify({
|
|
12998
13115
|
$schema: "https://opencode.ai/config.json",
|
|
12999
|
-
|
|
13116
|
+
lsp
|
|
13000
13117
|
}, null, 2);
|
|
13001
13118
|
return [
|
|
13002
13119
|
{
|
|
13003
13120
|
path: ".opencode/opencode.json",
|
|
13004
|
-
content
|
|
13121
|
+
content: `${content}
|
|
13122
|
+
`,
|
|
13005
13123
|
overwrite: false
|
|
13006
13124
|
}
|
|
13007
13125
|
];
|
|
13008
13126
|
}
|
|
13127
|
+
toOpenCodeLspConfig(languageServers) {
|
|
13128
|
+
return Object.fromEntries(Object.entries(languageServers).map(([name, config]) => [
|
|
13129
|
+
name,
|
|
13130
|
+
{
|
|
13131
|
+
command: [config.command, ...config.args],
|
|
13132
|
+
extensions: OPENCODE_LSP_EXTENSIONS[name] ?? []
|
|
13133
|
+
}
|
|
13134
|
+
]));
|
|
13135
|
+
}
|
|
13009
13136
|
async isAvailable() {
|
|
13010
13137
|
return true;
|
|
13011
13138
|
}
|
|
@@ -13017,7 +13144,7 @@ function createOpenCodeLspGenerator() {
|
|
|
13017
13144
|
// src/core/lsp/lsp-installer.ts
|
|
13018
13145
|
import { execFile } from "node:child_process";
|
|
13019
13146
|
import { access as access5, mkdir as mkdir5 } from "node:fs/promises";
|
|
13020
|
-
import { homedir as
|
|
13147
|
+
import { homedir as homedir4 } from "node:os";
|
|
13021
13148
|
import { join as join4 } from "node:path";
|
|
13022
13149
|
import { promisify } from "node:util";
|
|
13023
13150
|
var execFileAsync = promisify(execFile);
|
|
@@ -13025,7 +13152,7 @@ var execFileAsync = promisify(execFile);
|
|
|
13025
13152
|
class LspInstaller {
|
|
13026
13153
|
lspBinDir;
|
|
13027
13154
|
constructor() {
|
|
13028
|
-
this.lspBinDir = join4(
|
|
13155
|
+
this.lspBinDir = join4(homedir4(), ".codeconductor", "lsp", "bin");
|
|
13029
13156
|
}
|
|
13030
13157
|
async checkInstalled(def) {
|
|
13031
13158
|
try {
|
|
@@ -13201,13 +13328,13 @@ var LSP_DEFINITIONS = [
|
|
|
13201
13328
|
{
|
|
13202
13329
|
id: "python",
|
|
13203
13330
|
language: "python",
|
|
13204
|
-
serverName: "
|
|
13205
|
-
packageManager: "
|
|
13206
|
-
package: "
|
|
13207
|
-
binaryName: "
|
|
13208
|
-
installCmd: "
|
|
13331
|
+
serverName: "Pyright",
|
|
13332
|
+
packageManager: "npm",
|
|
13333
|
+
package: "pyright",
|
|
13334
|
+
binaryName: "pyright-langserver",
|
|
13335
|
+
installCmd: "npm install -g pyright",
|
|
13209
13336
|
versionFlag: "--version",
|
|
13210
|
-
|
|
13337
|
+
npmDetect: "pyright"
|
|
13211
13338
|
},
|
|
13212
13339
|
{
|
|
13213
13340
|
id: "kotlin",
|
|
@@ -13259,7 +13386,7 @@ function resolveLsps(languages) {
|
|
|
13259
13386
|
// src/commands/install-lsp.command.ts
|
|
13260
13387
|
async function installLspCommand(options) {
|
|
13261
13388
|
const { target, lang, dryRun, force, global: isGlobal, output, projectRoot } = options;
|
|
13262
|
-
const baseDir = isGlobal ?
|
|
13389
|
+
const baseDir = isGlobal ? homedir5() : projectRoot;
|
|
13263
13390
|
try {
|
|
13264
13391
|
const runnerTarget = parseRunnerTarget(target);
|
|
13265
13392
|
const targets = getIndividualTargets(runnerTarget);
|
|
@@ -13397,6 +13524,1505 @@ function getLspConfigGenerator(target) {
|
|
|
13397
13524
|
}
|
|
13398
13525
|
}
|
|
13399
13526
|
|
|
13527
|
+
// src/commands/seo-audit.command.ts
|
|
13528
|
+
import { writeFile as writeFile5, mkdir as mkdir6 } from "node:fs/promises";
|
|
13529
|
+
import { dirname as dirname4, resolve as resolve9 } from "node:path";
|
|
13530
|
+
|
|
13531
|
+
// src/infrastructure/http/safe-fetch.ts
|
|
13532
|
+
import { lookup } from "node:dns/promises";
|
|
13533
|
+
var PRIVATE_IP_RANGES = [
|
|
13534
|
+
/^10\./,
|
|
13535
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
13536
|
+
/^192\.168\./,
|
|
13537
|
+
/^127\./,
|
|
13538
|
+
/^0\./,
|
|
13539
|
+
/^169\.254\./,
|
|
13540
|
+
/^::1$/,
|
|
13541
|
+
/^fc00:/i,
|
|
13542
|
+
/^fe80:/i
|
|
13543
|
+
];
|
|
13544
|
+
function isPrivateIp(ip) {
|
|
13545
|
+
return PRIVATE_IP_RANGES.some((range) => range.test(ip));
|
|
13546
|
+
}
|
|
13547
|
+
async function validateUrl(urlString) {
|
|
13548
|
+
let parsed;
|
|
13549
|
+
try {
|
|
13550
|
+
parsed = new URL(urlString);
|
|
13551
|
+
} catch {
|
|
13552
|
+
throw new Error(`Invalid URL: ${urlString}`);
|
|
13553
|
+
}
|
|
13554
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
13555
|
+
throw new Error(`Blocked scheme: ${parsed.protocol}. Only http and https are allowed.`);
|
|
13556
|
+
}
|
|
13557
|
+
const hostname = parsed.hostname;
|
|
13558
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
13559
|
+
throw new Error(`Blocked hostname: ${hostname}`);
|
|
13560
|
+
}
|
|
13561
|
+
try {
|
|
13562
|
+
const addresses = await lookup(hostname, { all: true });
|
|
13563
|
+
const addrList = Array.isArray(addresses) ? addresses : [addresses];
|
|
13564
|
+
for (const addr of addrList) {
|
|
13565
|
+
if (isPrivateIp(addr.address)) {
|
|
13566
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${addr.address}`);
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13569
|
+
} catch (error) {
|
|
13570
|
+
if (error instanceof Error && error.message.startsWith("SSRF blocked")) {
|
|
13571
|
+
throw error;
|
|
13572
|
+
}
|
|
13573
|
+
throw new Error(`DNS resolution failed for ${hostname}: ${String(error)}`);
|
|
13574
|
+
}
|
|
13575
|
+
}
|
|
13576
|
+
async function safeFetch(urlString, options = {}) {
|
|
13577
|
+
const { timeout = 1e4, followRedirects = false } = options;
|
|
13578
|
+
await validateUrl(urlString);
|
|
13579
|
+
const controller = new AbortController;
|
|
13580
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
13581
|
+
const start = Date.now();
|
|
13582
|
+
try {
|
|
13583
|
+
const response = await fetch(urlString, {
|
|
13584
|
+
method: "GET",
|
|
13585
|
+
signal: controller.signal,
|
|
13586
|
+
redirect: followRedirects ? "follow" : "manual",
|
|
13587
|
+
headers: {
|
|
13588
|
+
"User-Agent": "CodeConductor-SEO/0.3.0",
|
|
13589
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
13590
|
+
}
|
|
13591
|
+
});
|
|
13592
|
+
const responseTime = Date.now() - start;
|
|
13593
|
+
const body = await response.text();
|
|
13594
|
+
const headers = {};
|
|
13595
|
+
response.headers.forEach((value, key) => {
|
|
13596
|
+
headers[key] = value;
|
|
13597
|
+
});
|
|
13598
|
+
return {
|
|
13599
|
+
status: response.status,
|
|
13600
|
+
headers,
|
|
13601
|
+
body,
|
|
13602
|
+
responseTime,
|
|
13603
|
+
url: urlString
|
|
13604
|
+
};
|
|
13605
|
+
} catch (error) {
|
|
13606
|
+
const responseTime = Date.now() - start;
|
|
13607
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
13608
|
+
throw new Error(`Request timed out after ${timeout}ms: ${urlString}`);
|
|
13609
|
+
}
|
|
13610
|
+
throw new Error(`Fetch failed (${responseTime}ms): ${urlString} — ${String(error)}`);
|
|
13611
|
+
} finally {
|
|
13612
|
+
clearTimeout(timer);
|
|
13613
|
+
}
|
|
13614
|
+
}
|
|
13615
|
+
async function delay(ms) {
|
|
13616
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
13617
|
+
}
|
|
13618
|
+
|
|
13619
|
+
// src/infrastructure/parsers/sitemap-parser.ts
|
|
13620
|
+
function extractTag(xml, tag) {
|
|
13621
|
+
const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
|
|
13622
|
+
return match ? match[1].trim() : undefined;
|
|
13623
|
+
}
|
|
13624
|
+
function extractAllBlocks(xml, tag) {
|
|
13625
|
+
const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "gi");
|
|
13626
|
+
const blocks = [];
|
|
13627
|
+
let match;
|
|
13628
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
13629
|
+
blocks.push(match[1]);
|
|
13630
|
+
}
|
|
13631
|
+
return blocks;
|
|
13632
|
+
}
|
|
13633
|
+
function parseUrlEntries(xml) {
|
|
13634
|
+
const urlBlocks = extractAllBlocks(xml, "url");
|
|
13635
|
+
return urlBlocks.map((block) => ({
|
|
13636
|
+
url: extractTag(block, "loc") ?? "",
|
|
13637
|
+
lastmod: extractTag(block, "lastmod"),
|
|
13638
|
+
changefreq: extractTag(block, "changefreq"),
|
|
13639
|
+
priority: extractTag(block, "priority")
|
|
13640
|
+
})).filter((entry) => entry.url.length > 0);
|
|
13641
|
+
}
|
|
13642
|
+
function parseSitemapLocs(xml) {
|
|
13643
|
+
const sitemapBlocks = extractAllBlocks(xml, "sitemap");
|
|
13644
|
+
return sitemapBlocks.map((block) => extractTag(block, "loc")).filter((loc) => loc !== undefined && loc.length > 0);
|
|
13645
|
+
}
|
|
13646
|
+
function getDomain(url) {
|
|
13647
|
+
try {
|
|
13648
|
+
return new URL(url).hostname;
|
|
13649
|
+
} catch {
|
|
13650
|
+
return "";
|
|
13651
|
+
}
|
|
13652
|
+
}
|
|
13653
|
+
function deduplicateEntries(entries) {
|
|
13654
|
+
const seen = new Set;
|
|
13655
|
+
return entries.filter((entry) => {
|
|
13656
|
+
if (seen.has(entry.url))
|
|
13657
|
+
return false;
|
|
13658
|
+
seen.add(entry.url);
|
|
13659
|
+
return true;
|
|
13660
|
+
});
|
|
13661
|
+
}
|
|
13662
|
+
function filterSameDomain(entries, domain) {
|
|
13663
|
+
return entries.filter((entry) => {
|
|
13664
|
+
try {
|
|
13665
|
+
return new URL(entry.url).hostname === domain;
|
|
13666
|
+
} catch {
|
|
13667
|
+
return false;
|
|
13668
|
+
}
|
|
13669
|
+
});
|
|
13670
|
+
}
|
|
13671
|
+
async function parseSitemap(sitemapUrl, options = {}) {
|
|
13672
|
+
const { maxDepth = 2, delay: requestDelay = 0 } = options;
|
|
13673
|
+
return parseSitemapRecursive(sitemapUrl, 0, maxDepth, requestDelay);
|
|
13674
|
+
}
|
|
13675
|
+
async function parseSitemapRecursive(sitemapUrl, depth, maxDepth, requestDelay) {
|
|
13676
|
+
const response = await safeFetch(sitemapUrl);
|
|
13677
|
+
const xml = response.body;
|
|
13678
|
+
const domain = getDomain(sitemapUrl);
|
|
13679
|
+
const isIndex = /<sitemapindex[\s>]/i.test(xml);
|
|
13680
|
+
if (isIndex) {
|
|
13681
|
+
const childUrls = parseSitemapLocs(xml);
|
|
13682
|
+
if (depth >= maxDepth) {
|
|
13683
|
+
return {
|
|
13684
|
+
entries: [],
|
|
13685
|
+
type: "sitemapindex",
|
|
13686
|
+
childSitemaps: childUrls
|
|
13687
|
+
};
|
|
13688
|
+
}
|
|
13689
|
+
const allEntries = [];
|
|
13690
|
+
const allChildSitemaps = [...childUrls];
|
|
13691
|
+
for (let i = 0;i < childUrls.length; i++) {
|
|
13692
|
+
if (i > 0 && requestDelay > 0) {
|
|
13693
|
+
await delay(requestDelay);
|
|
13694
|
+
}
|
|
13695
|
+
try {
|
|
13696
|
+
const childResult = await parseSitemapRecursive(childUrls[i], depth + 1, maxDepth, requestDelay);
|
|
13697
|
+
allEntries.push(...childResult.entries);
|
|
13698
|
+
allChildSitemaps.push(...childResult.childSitemaps);
|
|
13699
|
+
} catch {}
|
|
13700
|
+
}
|
|
13701
|
+
const deduped2 = deduplicateEntries(allEntries);
|
|
13702
|
+
const filtered2 = domain ? filterSameDomain(deduped2, domain) : deduped2;
|
|
13703
|
+
return {
|
|
13704
|
+
entries: filtered2,
|
|
13705
|
+
type: "sitemapindex",
|
|
13706
|
+
childSitemaps: allChildSitemaps
|
|
13707
|
+
};
|
|
13708
|
+
}
|
|
13709
|
+
const entries = parseUrlEntries(xml);
|
|
13710
|
+
const deduped = deduplicateEntries(entries);
|
|
13711
|
+
const filtered = domain ? filterSameDomain(deduped, domain) : deduped;
|
|
13712
|
+
return {
|
|
13713
|
+
entries: filtered,
|
|
13714
|
+
type: "urlset",
|
|
13715
|
+
childSitemaps: []
|
|
13716
|
+
};
|
|
13717
|
+
}
|
|
13718
|
+
|
|
13719
|
+
// src/domain/seo/meta-validator.ts
|
|
13720
|
+
function extractMetaContent(html, name) {
|
|
13721
|
+
const patterns = [
|
|
13722
|
+
new RegExp(`<meta[^>]*name=["']${name}["'][^>]*content=["']([^"']*)["']`, "i"),
|
|
13723
|
+
new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*name=["']${name}["']`, "i")
|
|
13724
|
+
];
|
|
13725
|
+
for (const pattern of patterns) {
|
|
13726
|
+
const match = html.match(pattern);
|
|
13727
|
+
if (match)
|
|
13728
|
+
return match[1].trim();
|
|
13729
|
+
}
|
|
13730
|
+
return;
|
|
13731
|
+
}
|
|
13732
|
+
function extractPropertyContent(html, property) {
|
|
13733
|
+
const patterns = [
|
|
13734
|
+
new RegExp(`<meta[^>]*property=["']${property}["'][^>]*content=["']([^"']*)["']`, "i"),
|
|
13735
|
+
new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*property=["']${property}["']`, "i")
|
|
13736
|
+
];
|
|
13737
|
+
for (const pattern of patterns) {
|
|
13738
|
+
const match = html.match(pattern);
|
|
13739
|
+
if (match)
|
|
13740
|
+
return match[1].trim();
|
|
13741
|
+
}
|
|
13742
|
+
return;
|
|
13743
|
+
}
|
|
13744
|
+
function extractTitle(html) {
|
|
13745
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
13746
|
+
return match ? match[1].trim() : undefined;
|
|
13747
|
+
}
|
|
13748
|
+
function extractCanonical(html) {
|
|
13749
|
+
const match = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']*)["']/i);
|
|
13750
|
+
return match ? match[1].trim() : undefined;
|
|
13751
|
+
}
|
|
13752
|
+
function extractH1Tags(html) {
|
|
13753
|
+
const regex = /<h1[^>]*>([\s\S]*?)<\/h1>/gi;
|
|
13754
|
+
const results = [];
|
|
13755
|
+
let match;
|
|
13756
|
+
while ((match = regex.exec(html)) !== null) {
|
|
13757
|
+
results.push(match[1].trim());
|
|
13758
|
+
}
|
|
13759
|
+
return results;
|
|
13760
|
+
}
|
|
13761
|
+
function extractHeadingHierarchy(html) {
|
|
13762
|
+
const regex = /<(h[1-6])[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
13763
|
+
const results = [];
|
|
13764
|
+
let match;
|
|
13765
|
+
while ((match = regex.exec(html)) !== null) {
|
|
13766
|
+
results.push({ tag: match[1].toLowerCase(), text: match[2].trim() });
|
|
13767
|
+
}
|
|
13768
|
+
return results;
|
|
13769
|
+
}
|
|
13770
|
+
function extractImgWithoutAlt(html) {
|
|
13771
|
+
const imgRegex = /<img[^>]*>/gi;
|
|
13772
|
+
let count = 0;
|
|
13773
|
+
let match;
|
|
13774
|
+
while ((match = imgRegex.exec(html)) !== null) {
|
|
13775
|
+
if (!/alt=["'][^"']+["']/i.test(match[0])) {
|
|
13776
|
+
count++;
|
|
13777
|
+
}
|
|
13778
|
+
}
|
|
13779
|
+
return count;
|
|
13780
|
+
}
|
|
13781
|
+
function extractHtmlLang(html) {
|
|
13782
|
+
const match = html.match(/<html[^>]*lang=["']([^"']*)["']/i);
|
|
13783
|
+
return match ? match[1].trim() : undefined;
|
|
13784
|
+
}
|
|
13785
|
+
function extractViewport(html) {
|
|
13786
|
+
return extractMetaContent(html, "viewport");
|
|
13787
|
+
}
|
|
13788
|
+
function extractInternalLinks(html, baseUrl) {
|
|
13789
|
+
let domain;
|
|
13790
|
+
try {
|
|
13791
|
+
domain = new URL(baseUrl).hostname;
|
|
13792
|
+
} catch {
|
|
13793
|
+
return 0;
|
|
13794
|
+
}
|
|
13795
|
+
const linkRegex = /href=["']([^"']*)["']/gi;
|
|
13796
|
+
let count = 0;
|
|
13797
|
+
let match;
|
|
13798
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
13799
|
+
const href = match[1];
|
|
13800
|
+
if (href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:"))
|
|
13801
|
+
continue;
|
|
13802
|
+
try {
|
|
13803
|
+
const resolved = new URL(href, baseUrl);
|
|
13804
|
+
if (resolved.hostname === domain)
|
|
13805
|
+
count++;
|
|
13806
|
+
} catch {
|
|
13807
|
+
if (href.startsWith("/"))
|
|
13808
|
+
count++;
|
|
13809
|
+
}
|
|
13810
|
+
}
|
|
13811
|
+
return count;
|
|
13812
|
+
}
|
|
13813
|
+
function validateMeta(html, url, responseTime) {
|
|
13814
|
+
const checks = [];
|
|
13815
|
+
const title = extractTitle(html);
|
|
13816
|
+
if (!title) {
|
|
13817
|
+
checks.push({
|
|
13818
|
+
name: "title-tag",
|
|
13819
|
+
category: "meta",
|
|
13820
|
+
severity: "error",
|
|
13821
|
+
message: "Missing <title> tag",
|
|
13822
|
+
remediation: "Add a <title> tag with 30-60 characters describing the page content."
|
|
13823
|
+
});
|
|
13824
|
+
} else if (title.length < 30) {
|
|
13825
|
+
checks.push({
|
|
13826
|
+
name: "title-tag",
|
|
13827
|
+
category: "meta",
|
|
13828
|
+
severity: "warning",
|
|
13829
|
+
message: `Title too short (${title.length} chars): "${title}"`,
|
|
13830
|
+
remediation: "Expand title to 30-60 characters."
|
|
13831
|
+
});
|
|
13832
|
+
} else if (title.length > 60) {
|
|
13833
|
+
checks.push({
|
|
13834
|
+
name: "title-tag",
|
|
13835
|
+
category: "meta",
|
|
13836
|
+
severity: "warning",
|
|
13837
|
+
message: `Title too long (${title.length} chars): "${title}"`,
|
|
13838
|
+
remediation: "Shorten title to 30-60 characters to avoid truncation in SERPs."
|
|
13839
|
+
});
|
|
13840
|
+
} else {
|
|
13841
|
+
checks.push({
|
|
13842
|
+
name: "title-tag",
|
|
13843
|
+
category: "meta",
|
|
13844
|
+
severity: "pass",
|
|
13845
|
+
message: `Title OK (${title.length} chars): "${title}"`
|
|
13846
|
+
});
|
|
13847
|
+
}
|
|
13848
|
+
const description = extractMetaContent(html, "description");
|
|
13849
|
+
if (!description) {
|
|
13850
|
+
checks.push({
|
|
13851
|
+
name: "meta-description",
|
|
13852
|
+
category: "meta",
|
|
13853
|
+
severity: "error",
|
|
13854
|
+
message: 'Missing <meta name="description">',
|
|
13855
|
+
remediation: "Add a meta description with 120-160 characters summarizing the page."
|
|
13856
|
+
});
|
|
13857
|
+
} else if (description.length < 120) {
|
|
13858
|
+
checks.push({
|
|
13859
|
+
name: "meta-description",
|
|
13860
|
+
category: "meta",
|
|
13861
|
+
severity: "warning",
|
|
13862
|
+
message: `Description too short (${description.length} chars)`,
|
|
13863
|
+
remediation: "Expand description to 120-160 characters."
|
|
13864
|
+
});
|
|
13865
|
+
} else if (description.length > 160) {
|
|
13866
|
+
checks.push({
|
|
13867
|
+
name: "meta-description",
|
|
13868
|
+
category: "meta",
|
|
13869
|
+
severity: "warning",
|
|
13870
|
+
message: `Description too long (${description.length} chars)`,
|
|
13871
|
+
remediation: "Shorten description to 120-160 characters."
|
|
13872
|
+
});
|
|
13873
|
+
} else {
|
|
13874
|
+
checks.push({
|
|
13875
|
+
name: "meta-description",
|
|
13876
|
+
category: "meta",
|
|
13877
|
+
severity: "pass",
|
|
13878
|
+
message: `Description OK (${description.length} chars)`
|
|
13879
|
+
});
|
|
13880
|
+
}
|
|
13881
|
+
const canonical = extractCanonical(html);
|
|
13882
|
+
if (!canonical) {
|
|
13883
|
+
checks.push({
|
|
13884
|
+
name: "canonical",
|
|
13885
|
+
category: "meta",
|
|
13886
|
+
severity: "warning",
|
|
13887
|
+
message: 'Missing <link rel="canonical">',
|
|
13888
|
+
remediation: "Add a canonical URL to prevent duplicate content issues."
|
|
13889
|
+
});
|
|
13890
|
+
} else if (canonical !== url) {
|
|
13891
|
+
checks.push({
|
|
13892
|
+
name: "canonical",
|
|
13893
|
+
category: "meta",
|
|
13894
|
+
severity: "info",
|
|
13895
|
+
message: `Canonical (${canonical}) differs from current URL (${url})`
|
|
13896
|
+
});
|
|
13897
|
+
} else {
|
|
13898
|
+
checks.push({
|
|
13899
|
+
name: "canonical",
|
|
13900
|
+
category: "meta",
|
|
13901
|
+
severity: "pass",
|
|
13902
|
+
message: `Canonical matches URL`
|
|
13903
|
+
});
|
|
13904
|
+
}
|
|
13905
|
+
const robots = extractMetaContent(html, "robots");
|
|
13906
|
+
if (robots) {
|
|
13907
|
+
if (/noindex/i.test(robots)) {
|
|
13908
|
+
checks.push({
|
|
13909
|
+
name: "robots-noindex",
|
|
13910
|
+
category: "crawl",
|
|
13911
|
+
severity: "warning",
|
|
13912
|
+
message: `Page has noindex directive: "${robots}"`,
|
|
13913
|
+
remediation: "Remove noindex if this page should appear in search results."
|
|
13914
|
+
});
|
|
13915
|
+
}
|
|
13916
|
+
if (/nofollow/i.test(robots)) {
|
|
13917
|
+
checks.push({
|
|
13918
|
+
name: "robots-nofollow",
|
|
13919
|
+
category: "crawl",
|
|
13920
|
+
severity: "warning",
|
|
13921
|
+
message: `Page has nofollow directive: "${robots}"`
|
|
13922
|
+
});
|
|
13923
|
+
}
|
|
13924
|
+
} else {
|
|
13925
|
+
checks.push({
|
|
13926
|
+
name: "robots-directive",
|
|
13927
|
+
category: "crawl",
|
|
13928
|
+
severity: "pass",
|
|
13929
|
+
message: "No restrictive robots directive"
|
|
13930
|
+
});
|
|
13931
|
+
}
|
|
13932
|
+
const hreflangRegex = /hreflang=["']([^"']*)["']/gi;
|
|
13933
|
+
const hreflangs = [];
|
|
13934
|
+
let hreflangMatch;
|
|
13935
|
+
while ((hreflangMatch = hreflangRegex.exec(html)) !== null) {
|
|
13936
|
+
hreflangs.push(hreflangMatch[1]);
|
|
13937
|
+
}
|
|
13938
|
+
if (hreflangs.length > 0) {
|
|
13939
|
+
checks.push({
|
|
13940
|
+
name: "hreflang",
|
|
13941
|
+
category: "meta",
|
|
13942
|
+
severity: "pass",
|
|
13943
|
+
message: `Found ${hreflangs.length} hreflang tags: ${hreflangs.join(", ")}`
|
|
13944
|
+
});
|
|
13945
|
+
}
|
|
13946
|
+
const ogTitle = extractPropertyContent(html, "og:title");
|
|
13947
|
+
const ogDescription = extractPropertyContent(html, "og:description");
|
|
13948
|
+
const ogImage = extractPropertyContent(html, "og:image");
|
|
13949
|
+
const ogUrl = extractPropertyContent(html, "og:url");
|
|
13950
|
+
if (!ogTitle) {
|
|
13951
|
+
checks.push({
|
|
13952
|
+
name: "og-title",
|
|
13953
|
+
category: "social",
|
|
13954
|
+
severity: "warning",
|
|
13955
|
+
message: "Missing og:title",
|
|
13956
|
+
remediation: 'Add <meta property="og:title" content="..."> for social sharing.'
|
|
13957
|
+
});
|
|
13958
|
+
} else {
|
|
13959
|
+
checks.push({ name: "og-title", category: "social", severity: "pass", message: "og:title present" });
|
|
13960
|
+
}
|
|
13961
|
+
if (!ogDescription) {
|
|
13962
|
+
checks.push({
|
|
13963
|
+
name: "og-description",
|
|
13964
|
+
category: "social",
|
|
13965
|
+
severity: "warning",
|
|
13966
|
+
message: "Missing og:description",
|
|
13967
|
+
remediation: 'Add <meta property="og:description" content="...">.'
|
|
13968
|
+
});
|
|
13969
|
+
} else {
|
|
13970
|
+
checks.push({ name: "og-description", category: "social", severity: "pass", message: "og:description present" });
|
|
13971
|
+
}
|
|
13972
|
+
if (!ogImage) {
|
|
13973
|
+
checks.push({
|
|
13974
|
+
name: "og-image",
|
|
13975
|
+
category: "social",
|
|
13976
|
+
severity: "warning",
|
|
13977
|
+
message: "Missing og:image",
|
|
13978
|
+
remediation: 'Add <meta property="og:image" content="..."> with a 1200x630px image.'
|
|
13979
|
+
});
|
|
13980
|
+
} else {
|
|
13981
|
+
checks.push({ name: "og-image", category: "social", severity: "pass", message: "og:image present" });
|
|
13982
|
+
}
|
|
13983
|
+
if (!ogUrl) {
|
|
13984
|
+
checks.push({
|
|
13985
|
+
name: "og-url",
|
|
13986
|
+
category: "social",
|
|
13987
|
+
severity: "info",
|
|
13988
|
+
message: "Missing og:url"
|
|
13989
|
+
});
|
|
13990
|
+
}
|
|
13991
|
+
const twitterCard = extractMetaContent(html, "twitter:card");
|
|
13992
|
+
if (!twitterCard) {
|
|
13993
|
+
checks.push({
|
|
13994
|
+
name: "twitter-card",
|
|
13995
|
+
category: "social",
|
|
13996
|
+
severity: "info",
|
|
13997
|
+
message: "Missing twitter:card",
|
|
13998
|
+
remediation: 'Add <meta name="twitter:card" content="summary_large_image">.'
|
|
13999
|
+
});
|
|
14000
|
+
} else {
|
|
14001
|
+
checks.push({ name: "twitter-card", category: "social", severity: "pass", message: `twitter:card: ${twitterCard}` });
|
|
14002
|
+
}
|
|
14003
|
+
const h1Tags = extractH1Tags(html);
|
|
14004
|
+
if (h1Tags.length === 0) {
|
|
14005
|
+
checks.push({
|
|
14006
|
+
name: "h1-tag",
|
|
14007
|
+
category: "content",
|
|
14008
|
+
severity: "error",
|
|
14009
|
+
message: "Missing <h1> tag",
|
|
14010
|
+
remediation: "Add exactly one <h1> tag with the main page heading."
|
|
14011
|
+
});
|
|
14012
|
+
} else if (h1Tags.length > 1) {
|
|
14013
|
+
checks.push({
|
|
14014
|
+
name: "h1-tag",
|
|
14015
|
+
category: "content",
|
|
14016
|
+
severity: "warning",
|
|
14017
|
+
message: `Multiple <h1> tags found (${h1Tags.length}): "${h1Tags.join('", "')}"`,
|
|
14018
|
+
remediation: "Use only one <h1> per page. Convert extras to <h2>."
|
|
14019
|
+
});
|
|
14020
|
+
} else {
|
|
14021
|
+
checks.push({
|
|
14022
|
+
name: "h1-tag",
|
|
14023
|
+
category: "content",
|
|
14024
|
+
severity: "pass",
|
|
14025
|
+
message: `H1 OK: "${h1Tags[0]}"`
|
|
14026
|
+
});
|
|
14027
|
+
}
|
|
14028
|
+
const headings = extractHeadingHierarchy(html);
|
|
14029
|
+
if (headings.length > 1) {
|
|
14030
|
+
let hasSkips = false;
|
|
14031
|
+
for (let i = 1;i < headings.length; i++) {
|
|
14032
|
+
const prev = parseInt(headings[i - 1].tag[1]);
|
|
14033
|
+
const curr = parseInt(headings[i].tag[1]);
|
|
14034
|
+
if (curr > prev + 1) {
|
|
14035
|
+
hasSkips = true;
|
|
14036
|
+
break;
|
|
14037
|
+
}
|
|
14038
|
+
}
|
|
14039
|
+
if (hasSkips) {
|
|
14040
|
+
checks.push({
|
|
14041
|
+
name: "heading-hierarchy",
|
|
14042
|
+
category: "content",
|
|
14043
|
+
severity: "warning",
|
|
14044
|
+
message: "Heading hierarchy has skipped levels (e.g., h2 → h4)",
|
|
14045
|
+
remediation: "Ensure headings follow sequential order: h1 → h2 → h3."
|
|
14046
|
+
});
|
|
14047
|
+
} else {
|
|
14048
|
+
checks.push({
|
|
14049
|
+
name: "heading-hierarchy",
|
|
14050
|
+
category: "content",
|
|
14051
|
+
severity: "pass",
|
|
14052
|
+
message: `Heading hierarchy OK (${headings.length} headings)`
|
|
14053
|
+
});
|
|
14054
|
+
}
|
|
14055
|
+
}
|
|
14056
|
+
const imgsWithoutAlt = extractImgWithoutAlt(html);
|
|
14057
|
+
if (imgsWithoutAlt > 0) {
|
|
14058
|
+
checks.push({
|
|
14059
|
+
name: "img-alt-text",
|
|
14060
|
+
category: "content",
|
|
14061
|
+
severity: "warning",
|
|
14062
|
+
message: `${imgsWithoutAlt} image(s) missing alt text`,
|
|
14063
|
+
remediation: "Add descriptive alt text to all images for accessibility and SEO."
|
|
14064
|
+
});
|
|
14065
|
+
} else {
|
|
14066
|
+
checks.push({
|
|
14067
|
+
name: "img-alt-text",
|
|
14068
|
+
category: "content",
|
|
14069
|
+
severity: "pass",
|
|
14070
|
+
message: "All images have alt text"
|
|
14071
|
+
});
|
|
14072
|
+
}
|
|
14073
|
+
const internalLinks = extractInternalLinks(html, url);
|
|
14074
|
+
checks.push({
|
|
14075
|
+
name: "internal-links",
|
|
14076
|
+
category: "content",
|
|
14077
|
+
severity: internalLinks > 0 ? "pass" : "warning",
|
|
14078
|
+
message: `${internalLinks} internal links found`,
|
|
14079
|
+
remediation: internalLinks === 0 ? "Add internal links to improve crawlability and page authority." : undefined
|
|
14080
|
+
});
|
|
14081
|
+
const lang = extractHtmlLang(html);
|
|
14082
|
+
if (!lang) {
|
|
14083
|
+
checks.push({
|
|
14084
|
+
name: "html-lang",
|
|
14085
|
+
category: "technical",
|
|
14086
|
+
severity: "warning",
|
|
14087
|
+
message: 'Missing <html lang="..."> attribute',
|
|
14088
|
+
remediation: 'Add lang attribute to <html> tag (e.g., lang="en").'
|
|
14089
|
+
});
|
|
14090
|
+
} else {
|
|
14091
|
+
checks.push({
|
|
14092
|
+
name: "html-lang",
|
|
14093
|
+
category: "technical",
|
|
14094
|
+
severity: "pass",
|
|
14095
|
+
message: `HTML lang: ${lang}`
|
|
14096
|
+
});
|
|
14097
|
+
}
|
|
14098
|
+
const viewport = extractViewport(html);
|
|
14099
|
+
if (!viewport) {
|
|
14100
|
+
checks.push({
|
|
14101
|
+
name: "viewport",
|
|
14102
|
+
category: "technical",
|
|
14103
|
+
severity: "error",
|
|
14104
|
+
message: 'Missing <meta name="viewport">',
|
|
14105
|
+
remediation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.'
|
|
14106
|
+
});
|
|
14107
|
+
} else {
|
|
14108
|
+
checks.push({
|
|
14109
|
+
name: "viewport",
|
|
14110
|
+
category: "technical",
|
|
14111
|
+
severity: "pass",
|
|
14112
|
+
message: "Viewport meta tag present"
|
|
14113
|
+
});
|
|
14114
|
+
}
|
|
14115
|
+
const isHttps = url.startsWith("https://");
|
|
14116
|
+
checks.push({
|
|
14117
|
+
name: "https",
|
|
14118
|
+
category: "technical",
|
|
14119
|
+
severity: isHttps ? "pass" : "error",
|
|
14120
|
+
message: isHttps ? "HTTPS enabled" : "Site is not using HTTPS",
|
|
14121
|
+
remediation: isHttps ? undefined : "Migrate to HTTPS for security and SEO ranking."
|
|
14122
|
+
});
|
|
14123
|
+
if (responseTime < 3000) {
|
|
14124
|
+
checks.push({
|
|
14125
|
+
name: "response-time",
|
|
14126
|
+
category: "technical",
|
|
14127
|
+
severity: "pass",
|
|
14128
|
+
message: `Response time: ${responseTime}ms`
|
|
14129
|
+
});
|
|
14130
|
+
} else if (responseTime < 5000) {
|
|
14131
|
+
checks.push({
|
|
14132
|
+
name: "response-time",
|
|
14133
|
+
category: "technical",
|
|
14134
|
+
severity: "warning",
|
|
14135
|
+
message: `Slow response time: ${responseTime}ms`,
|
|
14136
|
+
remediation: "Optimize server response time to under 3 seconds."
|
|
14137
|
+
});
|
|
14138
|
+
} else {
|
|
14139
|
+
checks.push({
|
|
14140
|
+
name: "response-time",
|
|
14141
|
+
category: "technical",
|
|
14142
|
+
severity: "error",
|
|
14143
|
+
message: `Very slow response time: ${responseTime}ms`,
|
|
14144
|
+
remediation: "Server response time exceeds 5 seconds. Investigate server performance."
|
|
14145
|
+
});
|
|
14146
|
+
}
|
|
14147
|
+
return checks;
|
|
14148
|
+
}
|
|
14149
|
+
|
|
14150
|
+
// src/domain/seo/schema-validator.ts
|
|
14151
|
+
var HOTEL_TYPES = [
|
|
14152
|
+
"Hotel",
|
|
14153
|
+
"LodgingBusiness",
|
|
14154
|
+
"HotelRoom",
|
|
14155
|
+
"LocalBusiness",
|
|
14156
|
+
"Resort",
|
|
14157
|
+
"Motel",
|
|
14158
|
+
"Hostel",
|
|
14159
|
+
"BedAndBreakfast",
|
|
14160
|
+
"Campground",
|
|
14161
|
+
"RV Park"
|
|
14162
|
+
];
|
|
14163
|
+
var SUPPORTING_TYPES = [
|
|
14164
|
+
"BreadcrumbList",
|
|
14165
|
+
"FAQPage",
|
|
14166
|
+
"Review",
|
|
14167
|
+
"AggregateRating",
|
|
14168
|
+
"Organization",
|
|
14169
|
+
"WebSite",
|
|
14170
|
+
"ImageObject",
|
|
14171
|
+
"Event",
|
|
14172
|
+
"TouristAttraction",
|
|
14173
|
+
"HowTo",
|
|
14174
|
+
"WebPage",
|
|
14175
|
+
"Person"
|
|
14176
|
+
];
|
|
14177
|
+
var REQUIRED_HOTEL_PROPERTIES = ["name", "address", "telephone", "image"];
|
|
14178
|
+
var RECOMMENDED_HOTEL_PROPERTIES = ["priceRange", "description", "url", "geo"];
|
|
14179
|
+
function extractJsonLdBlocks(html) {
|
|
14180
|
+
const regex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14181
|
+
const blocks = [];
|
|
14182
|
+
let match;
|
|
14183
|
+
while ((match = regex.exec(html)) !== null) {
|
|
14184
|
+
try {
|
|
14185
|
+
const parsed = JSON.parse(match[1].trim());
|
|
14186
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
14187
|
+
for (const item of items) {
|
|
14188
|
+
if (item && typeof item === "object" && "@type" in item) {
|
|
14189
|
+
blocks.push({
|
|
14190
|
+
type: String(item["@type"]),
|
|
14191
|
+
properties: item,
|
|
14192
|
+
raw: match[1].trim()
|
|
14193
|
+
});
|
|
14194
|
+
}
|
|
14195
|
+
}
|
|
14196
|
+
} catch {}
|
|
14197
|
+
}
|
|
14198
|
+
return blocks;
|
|
14199
|
+
}
|
|
14200
|
+
function validateHotelSchema(block) {
|
|
14201
|
+
const errors3 = [];
|
|
14202
|
+
const warnings = [];
|
|
14203
|
+
for (const prop of REQUIRED_HOTEL_PROPERTIES) {
|
|
14204
|
+
if (!(prop in block.properties)) {
|
|
14205
|
+
errors3.push(`Missing required property: ${prop}`);
|
|
14206
|
+
}
|
|
14207
|
+
}
|
|
14208
|
+
for (const prop of RECOMMENDED_HOTEL_PROPERTIES) {
|
|
14209
|
+
if (!(prop in block.properties)) {
|
|
14210
|
+
warnings.push(`Missing recommended property: ${prop}`);
|
|
14211
|
+
}
|
|
14212
|
+
}
|
|
14213
|
+
if ("address" in block.properties) {
|
|
14214
|
+
const address = block.properties.address;
|
|
14215
|
+
if (typeof address === "object" && address !== null) {
|
|
14216
|
+
const addrObj = address;
|
|
14217
|
+
if (!("streetAddress" in addrObj) && !("addressLocality" in addrObj)) {
|
|
14218
|
+
errors3.push("Address missing streetAddress or addressLocality");
|
|
14219
|
+
}
|
|
14220
|
+
}
|
|
14221
|
+
}
|
|
14222
|
+
if ("image" in block.properties) {
|
|
14223
|
+
const image = block.properties.image;
|
|
14224
|
+
if (typeof image === "string" && !image.startsWith("http")) {
|
|
14225
|
+
warnings.push("Image URL should be absolute");
|
|
14226
|
+
}
|
|
14227
|
+
}
|
|
14228
|
+
return {
|
|
14229
|
+
valid: errors3.length === 0,
|
|
14230
|
+
type: block.type,
|
|
14231
|
+
errors: errors3,
|
|
14232
|
+
warnings
|
|
14233
|
+
};
|
|
14234
|
+
}
|
|
14235
|
+
function validateSupportingSchema(block) {
|
|
14236
|
+
const errors3 = [];
|
|
14237
|
+
const warnings = [];
|
|
14238
|
+
if (block.type === "BreadcrumbList") {
|
|
14239
|
+
if (!("itemListElement" in block.properties)) {
|
|
14240
|
+
errors3.push("BreadcrumbList missing itemListElement");
|
|
14241
|
+
}
|
|
14242
|
+
}
|
|
14243
|
+
if (block.type === "FAQPage") {
|
|
14244
|
+
if (!("mainEntity" in block.properties)) {
|
|
14245
|
+
errors3.push("FAQPage missing mainEntity");
|
|
14246
|
+
}
|
|
14247
|
+
}
|
|
14248
|
+
if (block.type === "AggregateRating") {
|
|
14249
|
+
if (!("ratingValue" in block.properties)) {
|
|
14250
|
+
errors3.push("AggregateRating missing ratingValue");
|
|
14251
|
+
}
|
|
14252
|
+
if (!("reviewCount" in block.properties) && !("ratingCount" in block.properties)) {
|
|
14253
|
+
warnings.push("AggregateRating missing reviewCount or ratingCount");
|
|
14254
|
+
}
|
|
14255
|
+
}
|
|
14256
|
+
if (block.type === "Organization") {
|
|
14257
|
+
if (!("name" in block.properties)) {
|
|
14258
|
+
errors3.push("Organization missing name");
|
|
14259
|
+
}
|
|
14260
|
+
if (!("url" in block.properties)) {
|
|
14261
|
+
warnings.push("Organization missing url");
|
|
14262
|
+
}
|
|
14263
|
+
}
|
|
14264
|
+
return {
|
|
14265
|
+
valid: errors3.length === 0,
|
|
14266
|
+
type: block.type,
|
|
14267
|
+
errors: errors3,
|
|
14268
|
+
warnings
|
|
14269
|
+
};
|
|
14270
|
+
}
|
|
14271
|
+
function validateSchema(html) {
|
|
14272
|
+
const checks = [];
|
|
14273
|
+
const blocks = extractJsonLdBlocks(html);
|
|
14274
|
+
if (blocks.length === 0) {
|
|
14275
|
+
checks.push({
|
|
14276
|
+
name: "json-ld-presence",
|
|
14277
|
+
category: "schema",
|
|
14278
|
+
severity: "error",
|
|
14279
|
+
message: "No JSON-LD structured data found",
|
|
14280
|
+
remediation: 'Add <script type="application/ld+json"> with Hotel or LodgingBusiness schema.'
|
|
14281
|
+
});
|
|
14282
|
+
return checks;
|
|
14283
|
+
}
|
|
14284
|
+
checks.push({
|
|
14285
|
+
name: "json-ld-presence",
|
|
14286
|
+
category: "schema",
|
|
14287
|
+
severity: "pass",
|
|
14288
|
+
message: `Found ${blocks.length} JSON-LD block(s)`
|
|
14289
|
+
});
|
|
14290
|
+
const hotelBlocks = blocks.filter((b) => HOTEL_TYPES.includes(b.type));
|
|
14291
|
+
const supportingBlocks = blocks.filter((b) => SUPPORTING_TYPES.includes(b.type));
|
|
14292
|
+
const unknownBlocks = blocks.filter((b) => !HOTEL_TYPES.includes(b.type) && !SUPPORTING_TYPES.includes(b.type));
|
|
14293
|
+
if (hotelBlocks.length === 0) {
|
|
14294
|
+
checks.push({
|
|
14295
|
+
name: "hotel-schema",
|
|
14296
|
+
category: "schema",
|
|
14297
|
+
severity: "error",
|
|
14298
|
+
message: `No Hotel/Hospitality schema found. Types found: ${blocks.map((b) => b.type).join(", ")}`,
|
|
14299
|
+
remediation: "Add a Hotel, LodgingBusiness, or Resort schema with required properties."
|
|
14300
|
+
});
|
|
14301
|
+
}
|
|
14302
|
+
for (const block of hotelBlocks) {
|
|
14303
|
+
const result = validateHotelSchema(block);
|
|
14304
|
+
if (result.valid) {
|
|
14305
|
+
checks.push({
|
|
14306
|
+
name: `schema-${block.type}`,
|
|
14307
|
+
category: "schema",
|
|
14308
|
+
severity: "pass",
|
|
14309
|
+
message: `${block.type} schema valid`
|
|
14310
|
+
});
|
|
14311
|
+
} else {
|
|
14312
|
+
checks.push({
|
|
14313
|
+
name: `schema-${block.type}`,
|
|
14314
|
+
category: "schema",
|
|
14315
|
+
severity: "error",
|
|
14316
|
+
message: `${block.type} schema invalid: ${result.errors.join("; ")}`,
|
|
14317
|
+
remediation: `Fix the following properties: ${result.errors.join(", ")}`
|
|
14318
|
+
});
|
|
14319
|
+
}
|
|
14320
|
+
if (result.warnings.length > 0) {
|
|
14321
|
+
checks.push({
|
|
14322
|
+
name: `schema-${block.type}-warnings`,
|
|
14323
|
+
category: "schema",
|
|
14324
|
+
severity: "warning",
|
|
14325
|
+
message: `${block.type} recommendations: ${result.warnings.join("; ")}`
|
|
14326
|
+
});
|
|
14327
|
+
}
|
|
14328
|
+
}
|
|
14329
|
+
for (const block of supportingBlocks) {
|
|
14330
|
+
const result = validateSupportingSchema(block);
|
|
14331
|
+
checks.push({
|
|
14332
|
+
name: `schema-${block.type}`,
|
|
14333
|
+
category: "schema",
|
|
14334
|
+
severity: result.valid ? "pass" : "warning",
|
|
14335
|
+
message: result.valid ? `${block.type} schema valid` : `${block.type} schema issues: ${result.errors.join("; ")}`
|
|
14336
|
+
});
|
|
14337
|
+
}
|
|
14338
|
+
for (const block of unknownBlocks) {
|
|
14339
|
+
checks.push({
|
|
14340
|
+
name: `schema-${block.type}`,
|
|
14341
|
+
category: "schema",
|
|
14342
|
+
severity: "info",
|
|
14343
|
+
message: `Unknown schema type: ${block.type}`
|
|
14344
|
+
});
|
|
14345
|
+
}
|
|
14346
|
+
return checks;
|
|
14347
|
+
}
|
|
14348
|
+
|
|
14349
|
+
// src/domain/seo/geo-validator.ts
|
|
14350
|
+
function hasFactualStatements(html) {
|
|
14351
|
+
const bodyText = html.replace(/<[^>]*>/g, " ");
|
|
14352
|
+
const numberPattern = /\d{2,}/g;
|
|
14353
|
+
const datePattern = /\b(20\d{2}|19\d{2})\b/g;
|
|
14354
|
+
const properNounPattern = /\b[A-Z][a-z]{2,}\b/g;
|
|
14355
|
+
const numbers = bodyText.match(numberPattern) ?? [];
|
|
14356
|
+
const dates = bodyText.match(datePattern) ?? [];
|
|
14357
|
+
const properNouns = bodyText.match(properNounPattern) ?? [];
|
|
14358
|
+
return numbers.length >= 3 || dates.length >= 1 || properNouns.length >= 5;
|
|
14359
|
+
}
|
|
14360
|
+
function countStructuredLists(html) {
|
|
14361
|
+
const ulMatches = html.match(/<ul[^>]*>/gi) ?? [];
|
|
14362
|
+
const olMatches = html.match(/<ol[^>]*>/gi) ?? [];
|
|
14363
|
+
const dlMatches = html.match(/<dl[^>]*>/gi) ?? [];
|
|
14364
|
+
return ulMatches.length + olMatches.length + dlMatches.length;
|
|
14365
|
+
}
|
|
14366
|
+
function hasFaqSection(html) {
|
|
14367
|
+
const patterns = [
|
|
14368
|
+
/faqpage/i,
|
|
14369
|
+
/<details[^>]*>/i,
|
|
14370
|
+
/id=["'][^"']*faq[^"']*["']/i,
|
|
14371
|
+
/class=["'][^"']*faq[^"']*["']/i,
|
|
14372
|
+
/<h[2-4][^>]*>.*(?:faq|frequently asked)/i
|
|
14373
|
+
];
|
|
14374
|
+
return patterns.some((p) => p.test(html));
|
|
14375
|
+
}
|
|
14376
|
+
function hasContentDates(html) {
|
|
14377
|
+
const timeRegex2 = /<time[^>]*datetime=["']([^"']*)["']/gi;
|
|
14378
|
+
let match;
|
|
14379
|
+
while ((match = timeRegex2.exec(html)) !== null) {
|
|
14380
|
+
return true;
|
|
14381
|
+
}
|
|
14382
|
+
const datePattern = /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}\b/i;
|
|
14383
|
+
return datePattern.test(html);
|
|
14384
|
+
}
|
|
14385
|
+
function validateGeo(html, url) {
|
|
14386
|
+
const checks = [];
|
|
14387
|
+
if (hasFactualStatements(html)) {
|
|
14388
|
+
checks.push({
|
|
14389
|
+
name: "citable-content",
|
|
14390
|
+
category: "geo",
|
|
14391
|
+
severity: "pass",
|
|
14392
|
+
message: "Page contains factual statements suitable for AI citation"
|
|
14393
|
+
});
|
|
14394
|
+
} else {
|
|
14395
|
+
checks.push({
|
|
14396
|
+
name: "citable-content",
|
|
14397
|
+
category: "geo",
|
|
14398
|
+
severity: "warning",
|
|
14399
|
+
message: "Page lacks factual statements (numbers, dates, proper nouns) for AI citation",
|
|
14400
|
+
remediation: "Add specific facts, numbers, and named entities that AI assistants can cite."
|
|
14401
|
+
});
|
|
14402
|
+
}
|
|
14403
|
+
const listCount = countStructuredLists(html);
|
|
14404
|
+
if (listCount > 0) {
|
|
14405
|
+
checks.push({
|
|
14406
|
+
name: "structured-lists",
|
|
14407
|
+
category: "geo",
|
|
14408
|
+
severity: "pass",
|
|
14409
|
+
message: `Found ${listCount} structured list(s) (ul/ol/dl)`
|
|
14410
|
+
});
|
|
14411
|
+
} else {
|
|
14412
|
+
checks.push({
|
|
14413
|
+
name: "structured-lists",
|
|
14414
|
+
category: "geo",
|
|
14415
|
+
severity: "warning",
|
|
14416
|
+
message: "No structured lists found",
|
|
14417
|
+
remediation: "Add <ul> or <ol> lists for amenities, features, and services. AI tools extract structured lists for citations."
|
|
14418
|
+
});
|
|
14419
|
+
}
|
|
14420
|
+
if (hasFaqSection(html)) {
|
|
14421
|
+
checks.push({
|
|
14422
|
+
name: "faq-section",
|
|
14423
|
+
category: "geo",
|
|
14424
|
+
severity: "pass",
|
|
14425
|
+
message: "FAQ section detected"
|
|
14426
|
+
});
|
|
14427
|
+
} else {
|
|
14428
|
+
checks.push({
|
|
14429
|
+
name: "faq-section",
|
|
14430
|
+
category: "geo",
|
|
14431
|
+
severity: "warning",
|
|
14432
|
+
message: "No FAQ section found",
|
|
14433
|
+
remediation: "Add an FAQ section with FAQPage schema. AI search tools prioritize Q&A formatted content."
|
|
14434
|
+
});
|
|
14435
|
+
}
|
|
14436
|
+
if (hasContentDates(html)) {
|
|
14437
|
+
checks.push({
|
|
14438
|
+
name: "content-freshness",
|
|
14439
|
+
category: "geo",
|
|
14440
|
+
severity: "pass",
|
|
14441
|
+
message: "Content has date signals"
|
|
14442
|
+
});
|
|
14443
|
+
} else {
|
|
14444
|
+
checks.push({
|
|
14445
|
+
name: "content-freshness",
|
|
14446
|
+
category: "geo",
|
|
14447
|
+
severity: "info",
|
|
14448
|
+
message: "No date signals found in content",
|
|
14449
|
+
remediation: "Add <time> elements or visible dates to signal content freshness."
|
|
14450
|
+
});
|
|
14451
|
+
}
|
|
14452
|
+
return checks;
|
|
14453
|
+
}
|
|
14454
|
+
async function checkLlmsTxt(baseUrl) {
|
|
14455
|
+
const checks = [];
|
|
14456
|
+
let root;
|
|
14457
|
+
try {
|
|
14458
|
+
const parsed = new URL(baseUrl);
|
|
14459
|
+
root = `${parsed.protocol}//${parsed.host}`;
|
|
14460
|
+
} catch {
|
|
14461
|
+
return [{
|
|
14462
|
+
name: "llms-txt",
|
|
14463
|
+
category: "geo",
|
|
14464
|
+
severity: "error",
|
|
14465
|
+
message: `Invalid base URL: ${baseUrl}`
|
|
14466
|
+
}];
|
|
14467
|
+
}
|
|
14468
|
+
try {
|
|
14469
|
+
const response = await safeFetch(`${root}/llms.txt`);
|
|
14470
|
+
if (response.status === 200 && response.body.length > 0) {
|
|
14471
|
+
checks.push({
|
|
14472
|
+
name: "llms-txt",
|
|
14473
|
+
category: "geo",
|
|
14474
|
+
severity: "pass",
|
|
14475
|
+
message: `llms.txt found (${response.body.length} bytes)`
|
|
14476
|
+
});
|
|
14477
|
+
if (!response.body.startsWith("#")) {
|
|
14478
|
+
checks.push({
|
|
14479
|
+
name: "llms-txt-format",
|
|
14480
|
+
category: "geo",
|
|
14481
|
+
severity: "warning",
|
|
14482
|
+
message: "llms.txt should start with a # heading",
|
|
14483
|
+
remediation: "Format: # Site Name\\n> Description\\n\\n## Pages\\n- [Title](url): description"
|
|
14484
|
+
});
|
|
14485
|
+
} else {
|
|
14486
|
+
checks.push({
|
|
14487
|
+
name: "llms-txt-format",
|
|
14488
|
+
category: "geo",
|
|
14489
|
+
severity: "pass",
|
|
14490
|
+
message: "llms.txt format looks correct"
|
|
14491
|
+
});
|
|
14492
|
+
}
|
|
14493
|
+
} else {
|
|
14494
|
+
checks.push({
|
|
14495
|
+
name: "llms-txt",
|
|
14496
|
+
category: "geo",
|
|
14497
|
+
severity: "error",
|
|
14498
|
+
message: `llms.txt returned status ${response.status}`,
|
|
14499
|
+
remediation: "Create a llms.txt file at the site root following the llms.txt specification."
|
|
14500
|
+
});
|
|
14501
|
+
}
|
|
14502
|
+
} catch {
|
|
14503
|
+
checks.push({
|
|
14504
|
+
name: "llms-txt",
|
|
14505
|
+
category: "geo",
|
|
14506
|
+
severity: "error",
|
|
14507
|
+
message: "llms.txt not found or unreachable",
|
|
14508
|
+
remediation: "Create a llms.txt file at the site root. Use `codeconductor seo llms` to generate one."
|
|
14509
|
+
});
|
|
14510
|
+
}
|
|
14511
|
+
try {
|
|
14512
|
+
const response = await safeFetch(`${root}/llms-full.txt`);
|
|
14513
|
+
if (response.status === 200 && response.body.length > 0) {
|
|
14514
|
+
checks.push({
|
|
14515
|
+
name: "llms-full-txt",
|
|
14516
|
+
category: "geo",
|
|
14517
|
+
severity: "pass",
|
|
14518
|
+
message: `llms-full.txt found (${response.body.length} bytes)`
|
|
14519
|
+
});
|
|
14520
|
+
}
|
|
14521
|
+
} catch {
|
|
14522
|
+
checks.push({
|
|
14523
|
+
name: "llms-full-txt",
|
|
14524
|
+
category: "geo",
|
|
14525
|
+
severity: "info",
|
|
14526
|
+
message: "llms-full.txt not found (optional)",
|
|
14527
|
+
remediation: "Consider creating llms-full.txt with extended content for AI tools."
|
|
14528
|
+
});
|
|
14529
|
+
}
|
|
14530
|
+
return checks;
|
|
14531
|
+
}
|
|
14532
|
+
|
|
14533
|
+
// src/domain/seo/seo-auditor.ts
|
|
14534
|
+
function computeSummary(pages) {
|
|
14535
|
+
let passed = 0;
|
|
14536
|
+
let warnings = 0;
|
|
14537
|
+
let errors3 = 0;
|
|
14538
|
+
let total = 0;
|
|
14539
|
+
for (const page of pages) {
|
|
14540
|
+
for (const check of page.checks) {
|
|
14541
|
+
total++;
|
|
14542
|
+
if (check.severity === "pass")
|
|
14543
|
+
passed++;
|
|
14544
|
+
else if (check.severity === "warning" || check.severity === "info")
|
|
14545
|
+
warnings++;
|
|
14546
|
+
else if (check.severity === "error")
|
|
14547
|
+
errors3++;
|
|
14548
|
+
}
|
|
14549
|
+
}
|
|
14550
|
+
const score = total > 0 ? Math.round(passed / total * 100) : 0;
|
|
14551
|
+
return { total, passed, warnings, errors: errors3, score };
|
|
14552
|
+
}
|
|
14553
|
+
async function auditSingleUrl(url, options = {}) {
|
|
14554
|
+
const response = await safeFetch(url, {
|
|
14555
|
+
followRedirects: options.followRedirects ?? false
|
|
14556
|
+
});
|
|
14557
|
+
const html = response.body;
|
|
14558
|
+
const checks = [];
|
|
14559
|
+
checks.push(...validateMeta(html, url, response.responseTime));
|
|
14560
|
+
checks.push(...validateSchema(html));
|
|
14561
|
+
checks.push(...validateGeo(html, url));
|
|
14562
|
+
return {
|
|
14563
|
+
url,
|
|
14564
|
+
checks,
|
|
14565
|
+
responseTime: response.responseTime
|
|
14566
|
+
};
|
|
14567
|
+
}
|
|
14568
|
+
async function auditSitemap(sitemapUrl, options = {}) {
|
|
14569
|
+
const { delay: requestDelay = 500, followRedirects = false, maxUrls, onProgress } = options;
|
|
14570
|
+
const sitemapResult = await parseSitemap(sitemapUrl, { delay: requestDelay });
|
|
14571
|
+
let entries = sitemapResult.entries;
|
|
14572
|
+
if (maxUrls && entries.length > maxUrls) {
|
|
14573
|
+
entries = entries.slice(0, maxUrls);
|
|
14574
|
+
}
|
|
14575
|
+
const pages = [];
|
|
14576
|
+
for (let i = 0;i < entries.length; i++) {
|
|
14577
|
+
const entry = entries[i];
|
|
14578
|
+
if (onProgress) {
|
|
14579
|
+
onProgress(i + 1, entries.length, entry.url);
|
|
14580
|
+
}
|
|
14581
|
+
if (i > 0 && requestDelay > 0) {
|
|
14582
|
+
await delay(requestDelay);
|
|
14583
|
+
}
|
|
14584
|
+
try {
|
|
14585
|
+
const result = await auditSingleUrl(entry.url, { followRedirects });
|
|
14586
|
+
pages.push(result);
|
|
14587
|
+
} catch (error) {
|
|
14588
|
+
pages.push({
|
|
14589
|
+
url: entry.url,
|
|
14590
|
+
checks: [{
|
|
14591
|
+
name: "fetch-error",
|
|
14592
|
+
category: "technical",
|
|
14593
|
+
severity: "error",
|
|
14594
|
+
message: `Failed to fetch: ${String(error)}`
|
|
14595
|
+
}],
|
|
14596
|
+
responseTime: 0
|
|
14597
|
+
});
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
try {
|
|
14601
|
+
let siteRoot;
|
|
14602
|
+
try {
|
|
14603
|
+
const parsed = new URL(sitemapUrl);
|
|
14604
|
+
siteRoot = `${parsed.protocol}//${parsed.host}`;
|
|
14605
|
+
} catch {
|
|
14606
|
+
siteRoot = sitemapUrl;
|
|
14607
|
+
}
|
|
14608
|
+
const llmsChecks = await checkLlmsTxt(siteRoot);
|
|
14609
|
+
if (pages.length > 0) {
|
|
14610
|
+
pages[0] = {
|
|
14611
|
+
...pages[0],
|
|
14612
|
+
checks: [...pages[0].checks, ...llmsChecks]
|
|
14613
|
+
};
|
|
14614
|
+
}
|
|
14615
|
+
} catch {}
|
|
14616
|
+
const summary = computeSummary(pages);
|
|
14617
|
+
return {
|
|
14618
|
+
target: sitemapUrl,
|
|
14619
|
+
timestamp: new Date().toISOString(),
|
|
14620
|
+
pages,
|
|
14621
|
+
summary
|
|
14622
|
+
};
|
|
14623
|
+
}
|
|
14624
|
+
async function auditUrl(url, options = {}) {
|
|
14625
|
+
const page = await auditSingleUrl(url, options);
|
|
14626
|
+
try {
|
|
14627
|
+
const llmsChecks = await checkLlmsTxt(url);
|
|
14628
|
+
page.checks.push(...llmsChecks);
|
|
14629
|
+
} catch {}
|
|
14630
|
+
const summary = computeSummary([page]);
|
|
14631
|
+
return {
|
|
14632
|
+
target: url,
|
|
14633
|
+
timestamp: new Date().toISOString(),
|
|
14634
|
+
pages: [page],
|
|
14635
|
+
summary
|
|
14636
|
+
};
|
|
14637
|
+
}
|
|
14638
|
+
|
|
14639
|
+
// src/domain/seo/report-formatter.ts
|
|
14640
|
+
var SEVERITY_ICONS = {
|
|
14641
|
+
pass: "✓",
|
|
14642
|
+
warning: "⚠",
|
|
14643
|
+
error: "✗",
|
|
14644
|
+
info: "ℹ"
|
|
14645
|
+
};
|
|
14646
|
+
var SEVERITY_COLORS = {
|
|
14647
|
+
pass: "\x1B[32m",
|
|
14648
|
+
warning: "\x1B[33m",
|
|
14649
|
+
error: "\x1B[31m",
|
|
14650
|
+
info: "\x1B[36m"
|
|
14651
|
+
};
|
|
14652
|
+
var RESET = "\x1B[0m";
|
|
14653
|
+
var BOLD = "\x1B[1m";
|
|
14654
|
+
var DIM = "\x1B[2m";
|
|
14655
|
+
function formatCli(report) {
|
|
14656
|
+
const lines = [];
|
|
14657
|
+
lines.push("");
|
|
14658
|
+
lines.push(`${BOLD}SEO Audit Report${RESET}`);
|
|
14659
|
+
lines.push(`${DIM}Target: ${report.target}${RESET}`);
|
|
14660
|
+
lines.push(`${DIM}Time: ${report.timestamp}${RESET}`);
|
|
14661
|
+
lines.push(`${DIM}Pages: ${report.pages.length}${RESET}`);
|
|
14662
|
+
lines.push("");
|
|
14663
|
+
for (const page of report.pages) {
|
|
14664
|
+
lines.push(`${BOLD}── ${page.url} (${page.responseTime}ms) ──${RESET}`);
|
|
14665
|
+
const grouped = groupByCategory(page.checks);
|
|
14666
|
+
for (const [category, checks] of grouped) {
|
|
14667
|
+
lines.push(` ${DIM}${category}${RESET}`);
|
|
14668
|
+
for (const check of checks) {
|
|
14669
|
+
const icon = SEVERITY_ICONS[check.severity];
|
|
14670
|
+
const color = SEVERITY_COLORS[check.severity];
|
|
14671
|
+
lines.push(` ${color}${icon}${RESET} ${check.name}: ${check.message}`);
|
|
14672
|
+
if (check.remediation && check.severity !== "pass") {
|
|
14673
|
+
lines.push(` ${DIM}→ ${check.remediation}${RESET}`);
|
|
14674
|
+
}
|
|
14675
|
+
}
|
|
14676
|
+
}
|
|
14677
|
+
lines.push("");
|
|
14678
|
+
}
|
|
14679
|
+
const { summary } = report;
|
|
14680
|
+
lines.push(`${BOLD}Summary${RESET}`);
|
|
14681
|
+
lines.push(` Score: ${summary.score}%`);
|
|
14682
|
+
lines.push(` ${SEVERITY_COLORS.pass}✓ ${summary.passed} passed${RESET}`);
|
|
14683
|
+
lines.push(` ${SEVERITY_COLORS.warning}⚠ ${summary.warnings} warnings${RESET}`);
|
|
14684
|
+
lines.push(` ${SEVERITY_COLORS.error}✗ ${summary.errors} errors${RESET}`);
|
|
14685
|
+
lines.push(` Total checks: ${summary.total}`);
|
|
14686
|
+
lines.push("");
|
|
14687
|
+
return lines.join(`
|
|
14688
|
+
`);
|
|
14689
|
+
}
|
|
14690
|
+
function formatJson(report) {
|
|
14691
|
+
return JSON.stringify(report, null, 2);
|
|
14692
|
+
}
|
|
14693
|
+
function formatMarkdown(report) {
|
|
14694
|
+
const lines = [];
|
|
14695
|
+
lines.push("# SEO Audit Report");
|
|
14696
|
+
lines.push("");
|
|
14697
|
+
lines.push(`- **Target:** ${report.target}`);
|
|
14698
|
+
lines.push(`- **Date:** ${report.timestamp}`);
|
|
14699
|
+
lines.push(`- **Pages audited:** ${report.pages.length}`);
|
|
14700
|
+
lines.push("");
|
|
14701
|
+
lines.push("## Summary");
|
|
14702
|
+
lines.push("");
|
|
14703
|
+
lines.push(`| Metric | Value |`);
|
|
14704
|
+
lines.push(`|--------|-------|`);
|
|
14705
|
+
lines.push(`| Score | ${report.summary.score}% |`);
|
|
14706
|
+
lines.push(`| Passed | ${report.summary.passed} |`);
|
|
14707
|
+
lines.push(`| Warnings | ${report.summary.warnings} |`);
|
|
14708
|
+
lines.push(`| Errors | ${report.summary.errors} |`);
|
|
14709
|
+
lines.push(`| Total checks | ${report.summary.total} |`);
|
|
14710
|
+
lines.push("");
|
|
14711
|
+
for (const page of report.pages) {
|
|
14712
|
+
lines.push(`## ${page.url}`);
|
|
14713
|
+
lines.push("");
|
|
14714
|
+
lines.push(`Response time: ${page.responseTime}ms`);
|
|
14715
|
+
lines.push("");
|
|
14716
|
+
const errors3 = page.checks.filter((c) => c.severity === "error");
|
|
14717
|
+
const warnings = page.checks.filter((c) => c.severity === "warning");
|
|
14718
|
+
const passed = page.checks.filter((c) => c.severity === "pass");
|
|
14719
|
+
const info = page.checks.filter((c) => c.severity === "info");
|
|
14720
|
+
if (errors3.length > 0) {
|
|
14721
|
+
lines.push("### Errors");
|
|
14722
|
+
lines.push("");
|
|
14723
|
+
for (const check of errors3) {
|
|
14724
|
+
lines.push(`- **${check.name}** (${check.category}): ${check.message}`);
|
|
14725
|
+
if (check.remediation) {
|
|
14726
|
+
lines.push(` - Fix: ${check.remediation}`);
|
|
14727
|
+
}
|
|
14728
|
+
}
|
|
14729
|
+
lines.push("");
|
|
14730
|
+
}
|
|
14731
|
+
if (warnings.length > 0) {
|
|
14732
|
+
lines.push("### Warnings");
|
|
14733
|
+
lines.push("");
|
|
14734
|
+
for (const check of warnings) {
|
|
14735
|
+
lines.push(`- **${check.name}** (${check.category}): ${check.message}`);
|
|
14736
|
+
if (check.remediation) {
|
|
14737
|
+
lines.push(` - Fix: ${check.remediation}`);
|
|
14738
|
+
}
|
|
14739
|
+
}
|
|
14740
|
+
lines.push("");
|
|
14741
|
+
}
|
|
14742
|
+
if (passed.length > 0) {
|
|
14743
|
+
lines.push("### Passed");
|
|
14744
|
+
lines.push("");
|
|
14745
|
+
for (const check of passed) {
|
|
14746
|
+
lines.push(`- ${check.name}: ${check.message}`);
|
|
14747
|
+
}
|
|
14748
|
+
lines.push("");
|
|
14749
|
+
}
|
|
14750
|
+
if (info.length > 0) {
|
|
14751
|
+
lines.push("### Info");
|
|
14752
|
+
lines.push("");
|
|
14753
|
+
for (const check of info) {
|
|
14754
|
+
lines.push(`- ${check.name}: ${check.message}`);
|
|
14755
|
+
}
|
|
14756
|
+
lines.push("");
|
|
14757
|
+
}
|
|
14758
|
+
}
|
|
14759
|
+
lines.push("---");
|
|
14760
|
+
lines.push(`*Generated by CodeConductor SEO Audit on ${report.timestamp.split("T")[0]}*`);
|
|
14761
|
+
return lines.join(`
|
|
14762
|
+
`);
|
|
14763
|
+
}
|
|
14764
|
+
function groupByCategory(checks) {
|
|
14765
|
+
const groups = new Map;
|
|
14766
|
+
for (const check of checks) {
|
|
14767
|
+
if (!groups.has(check.category)) {
|
|
14768
|
+
groups.set(check.category, []);
|
|
14769
|
+
}
|
|
14770
|
+
groups.get(check.category).push(check);
|
|
14771
|
+
}
|
|
14772
|
+
return groups;
|
|
14773
|
+
}
|
|
14774
|
+
function computeExitCode(report, failOn) {
|
|
14775
|
+
if (report.summary.errors > 0)
|
|
14776
|
+
return 1;
|
|
14777
|
+
if (failOn === "warning" && report.summary.warnings > 0)
|
|
14778
|
+
return 2;
|
|
14779
|
+
if (report.summary.warnings > 0)
|
|
14780
|
+
return 0;
|
|
14781
|
+
return 0;
|
|
14782
|
+
}
|
|
14783
|
+
|
|
14784
|
+
// src/commands/seo-audit.command.ts
|
|
14785
|
+
async function seoAuditCommand(options) {
|
|
14786
|
+
const { url, sitemap, format, failOn, delay: delay2, output, followRedirects } = options;
|
|
14787
|
+
if (!url && !sitemap) {
|
|
14788
|
+
return {
|
|
14789
|
+
code: 1,
|
|
14790
|
+
data: {
|
|
14791
|
+
success: false,
|
|
14792
|
+
command: "seo audit",
|
|
14793
|
+
errors: ["Either --url or --sitemap is required"]
|
|
14794
|
+
}
|
|
14795
|
+
};
|
|
14796
|
+
}
|
|
14797
|
+
try {
|
|
14798
|
+
const report = sitemap ? await auditSitemap(sitemap, {
|
|
14799
|
+
delay: delay2,
|
|
14800
|
+
followRedirects,
|
|
14801
|
+
onProgress: format === "cli" ? (current, total, pageUrl) => {
|
|
14802
|
+
process.stderr.write(`\r Auditing ${current}/${total}: ${pageUrl}`);
|
|
14803
|
+
} : undefined
|
|
14804
|
+
}) : await auditUrl(url, { followRedirects });
|
|
14805
|
+
if (format === "cli") {
|
|
14806
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
14807
|
+
}
|
|
14808
|
+
let formattedOutput;
|
|
14809
|
+
switch (format) {
|
|
14810
|
+
case "json":
|
|
14811
|
+
formattedOutput = formatJson(report);
|
|
14812
|
+
break;
|
|
14813
|
+
case "markdown":
|
|
14814
|
+
formattedOutput = formatMarkdown(report);
|
|
14815
|
+
break;
|
|
14816
|
+
default:
|
|
14817
|
+
formattedOutput = formatCli(report);
|
|
14818
|
+
}
|
|
14819
|
+
if (output) {
|
|
14820
|
+
const outputPath = resolve9(options.projectRoot, output);
|
|
14821
|
+
await mkdir6(dirname4(outputPath), { recursive: true });
|
|
14822
|
+
await writeFile5(outputPath, formattedOutput, "utf-8");
|
|
14823
|
+
} else if (format === "markdown") {
|
|
14824
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
14825
|
+
const defaultPath = resolve9(options.projectRoot, "seo-reports", `audit-report-${timestamp}.md`);
|
|
14826
|
+
await mkdir6(dirname4(defaultPath), { recursive: true });
|
|
14827
|
+
await writeFile5(defaultPath, formattedOutput, "utf-8");
|
|
14828
|
+
process.stderr.write(`Report saved to: ${defaultPath}
|
|
14829
|
+
`);
|
|
14830
|
+
}
|
|
14831
|
+
const exitCode = computeExitCode(report, failOn);
|
|
14832
|
+
return {
|
|
14833
|
+
code: exitCode,
|
|
14834
|
+
data: {
|
|
14835
|
+
success: true,
|
|
14836
|
+
command: "seo audit",
|
|
14837
|
+
report,
|
|
14838
|
+
output: formattedOutput,
|
|
14839
|
+
outputFile: output ? resolve9(options.projectRoot, output) : format === "markdown" ? resolve9(options.projectRoot, "seo-reports") : undefined
|
|
14840
|
+
}
|
|
14841
|
+
};
|
|
14842
|
+
} catch (error) {
|
|
14843
|
+
return {
|
|
14844
|
+
code: 3,
|
|
14845
|
+
data: {
|
|
14846
|
+
success: false,
|
|
14847
|
+
command: "seo audit",
|
|
14848
|
+
errors: [String(error)]
|
|
14849
|
+
}
|
|
14850
|
+
};
|
|
14851
|
+
}
|
|
14852
|
+
}
|
|
14853
|
+
|
|
14854
|
+
// src/commands/seo-llms.command.ts
|
|
14855
|
+
import { writeFile as writeFile6, mkdir as mkdir7 } from "node:fs/promises";
|
|
14856
|
+
import { dirname as dirname5, resolve as resolve10 } from "node:path";
|
|
14857
|
+
|
|
14858
|
+
// src/domain/seo/llms-generator.ts
|
|
14859
|
+
function extractTitle2(html) {
|
|
14860
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
14861
|
+
return match ? match[1].trim() : "";
|
|
14862
|
+
}
|
|
14863
|
+
function extractDescription(html) {
|
|
14864
|
+
const patterns = [
|
|
14865
|
+
/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i,
|
|
14866
|
+
/<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["']/i,
|
|
14867
|
+
/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']*)["']/i
|
|
14868
|
+
];
|
|
14869
|
+
for (const pattern of patterns) {
|
|
14870
|
+
const match = html.match(pattern);
|
|
14871
|
+
if (match)
|
|
14872
|
+
return match[1].trim();
|
|
14873
|
+
}
|
|
14874
|
+
return "";
|
|
14875
|
+
}
|
|
14876
|
+
function extractFirstParagraph(html) {
|
|
14877
|
+
const bodyHtml = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "");
|
|
14878
|
+
const match = bodyHtml.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
14879
|
+
if (!match)
|
|
14880
|
+
return "";
|
|
14881
|
+
return match[1].replace(/<[^>]*>/g, "").trim().slice(0, 200);
|
|
14882
|
+
}
|
|
14883
|
+
function groupByPathSegment(entries) {
|
|
14884
|
+
const groups = new Map;
|
|
14885
|
+
for (const entry of entries) {
|
|
14886
|
+
try {
|
|
14887
|
+
const parsed = new URL(entry.url);
|
|
14888
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
14889
|
+
const group = segments.length > 0 ? `/${segments[0]}/` : "/";
|
|
14890
|
+
if (!groups.has(group)) {
|
|
14891
|
+
groups.set(group, []);
|
|
14892
|
+
}
|
|
14893
|
+
groups.get(group).push(entry);
|
|
14894
|
+
} catch {
|
|
14895
|
+
if (!groups.has("/")) {
|
|
14896
|
+
groups.set("/", []);
|
|
14897
|
+
}
|
|
14898
|
+
groups.get("/").push(entry);
|
|
14899
|
+
}
|
|
14900
|
+
}
|
|
14901
|
+
return groups;
|
|
14902
|
+
}
|
|
14903
|
+
function formatLlmsTxt(siteName, siteDescription, entries) {
|
|
14904
|
+
const lines = [];
|
|
14905
|
+
lines.push(`# ${siteName}`);
|
|
14906
|
+
if (siteDescription) {
|
|
14907
|
+
lines.push(`> ${siteDescription}`);
|
|
14908
|
+
}
|
|
14909
|
+
lines.push("");
|
|
14910
|
+
const groups = groupByPathSegment(entries);
|
|
14911
|
+
for (const [group, groupEntries] of groups) {
|
|
14912
|
+
const groupName = group === "/" ? "Main Pages" : group.replace(/\//g, "").replace(/-/g, " ");
|
|
14913
|
+
lines.push(`## ${groupName.charAt(0).toUpperCase() + groupName.slice(1)}`);
|
|
14914
|
+
lines.push("");
|
|
14915
|
+
for (const entry of groupEntries) {
|
|
14916
|
+
const desc = entry.description ? `: ${entry.description}` : "";
|
|
14917
|
+
lines.push(`- [${entry.title}](${entry.url})${desc}`);
|
|
14918
|
+
}
|
|
14919
|
+
lines.push("");
|
|
14920
|
+
}
|
|
14921
|
+
lines.push("---");
|
|
14922
|
+
lines.push(`Generated by CodeConductor SEO on ${new Date().toISOString().split("T")[0]}`);
|
|
14923
|
+
return lines.join(`
|
|
14924
|
+
`);
|
|
14925
|
+
}
|
|
14926
|
+
async function generateLlmsTxtFromSitemap(sitemapUrl, options = {}) {
|
|
14927
|
+
const { delay: requestDelay = 500, maxUrls, onProgress } = options;
|
|
14928
|
+
const sitemapResult = await parseSitemap(sitemapUrl, { delay: requestDelay });
|
|
14929
|
+
let urls = sitemapResult.entries.map((e) => e.url);
|
|
14930
|
+
if (maxUrls && urls.length > maxUrls) {
|
|
14931
|
+
urls = urls.slice(0, maxUrls);
|
|
14932
|
+
}
|
|
14933
|
+
const entries = [];
|
|
14934
|
+
let siteName = "";
|
|
14935
|
+
let siteDescription = "";
|
|
14936
|
+
for (let i = 0;i < urls.length; i++) {
|
|
14937
|
+
const url = urls[i];
|
|
14938
|
+
if (onProgress) {
|
|
14939
|
+
onProgress(i + 1, urls.length, url);
|
|
14940
|
+
}
|
|
14941
|
+
if (i > 0 && requestDelay > 0) {
|
|
14942
|
+
await delay(requestDelay);
|
|
14943
|
+
}
|
|
14944
|
+
try {
|
|
14945
|
+
const response = await safeFetch(url);
|
|
14946
|
+
const html = response.body;
|
|
14947
|
+
const title = extractTitle2(html) || url;
|
|
14948
|
+
const description = extractDescription(html) || extractFirstParagraph(html);
|
|
14949
|
+
if (i === 0) {
|
|
14950
|
+
siteName = title;
|
|
14951
|
+
siteDescription = extractDescription(html);
|
|
14952
|
+
}
|
|
14953
|
+
entries.push({ title, url, description });
|
|
14954
|
+
} catch {
|
|
14955
|
+
entries.push({ title: url, url, description: "" });
|
|
14956
|
+
}
|
|
14957
|
+
}
|
|
14958
|
+
if (!siteName) {
|
|
14959
|
+
try {
|
|
14960
|
+
siteName = new URL(sitemapUrl).hostname;
|
|
14961
|
+
} catch {
|
|
14962
|
+
siteName = "Site";
|
|
14963
|
+
}
|
|
14964
|
+
}
|
|
14965
|
+
const content = formatLlmsTxt(siteName, siteDescription, entries);
|
|
14966
|
+
return { content, entries };
|
|
14967
|
+
}
|
|
14968
|
+
async function generateLlmsTxtFromUrl(url) {
|
|
14969
|
+
const response = await safeFetch(url);
|
|
14970
|
+
const html = response.body;
|
|
14971
|
+
const title = extractTitle2(html) || url;
|
|
14972
|
+
const description = extractDescription(html) || extractFirstParagraph(html);
|
|
14973
|
+
const entries = [{ title, url, description }];
|
|
14974
|
+
const content = formatLlmsTxt(title, description, entries);
|
|
14975
|
+
return { content, entries };
|
|
14976
|
+
}
|
|
14977
|
+
|
|
14978
|
+
// src/commands/seo-llms.command.ts
|
|
14979
|
+
async function seoLlmsCommand(options) {
|
|
14980
|
+
const { url, sitemap, output, delay: delay2 } = options;
|
|
14981
|
+
if (!url && !sitemap) {
|
|
14982
|
+
return {
|
|
14983
|
+
code: 1,
|
|
14984
|
+
data: {
|
|
14985
|
+
success: false,
|
|
14986
|
+
command: "seo llms",
|
|
14987
|
+
errors: ["Either --url or --sitemap is required"]
|
|
14988
|
+
}
|
|
14989
|
+
};
|
|
14990
|
+
}
|
|
14991
|
+
try {
|
|
14992
|
+
const result = sitemap ? await generateLlmsTxtFromSitemap(sitemap, {
|
|
14993
|
+
delay: delay2,
|
|
14994
|
+
onProgress: (current, total, pageUrl) => {
|
|
14995
|
+
process.stderr.write(`\r Processing ${current}/${total}: ${pageUrl}`);
|
|
14996
|
+
}
|
|
14997
|
+
}) : await generateLlmsTxtFromUrl(url);
|
|
14998
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
14999
|
+
const outputPath = output ? resolve10(options.projectRoot, output) : resolve10(options.projectRoot, "llms.txt");
|
|
15000
|
+
await mkdir7(dirname5(outputPath), { recursive: true });
|
|
15001
|
+
await writeFile6(outputPath, result.content, "utf-8");
|
|
15002
|
+
process.stderr.write(`Generated: ${outputPath} (${result.entries.length} entries)
|
|
15003
|
+
`);
|
|
15004
|
+
return {
|
|
15005
|
+
code: 0,
|
|
15006
|
+
data: {
|
|
15007
|
+
success: true,
|
|
15008
|
+
command: "seo llms",
|
|
15009
|
+
outputFile: outputPath,
|
|
15010
|
+
entries: result.entries.length,
|
|
15011
|
+
content: result.content
|
|
15012
|
+
}
|
|
15013
|
+
};
|
|
15014
|
+
} catch (error) {
|
|
15015
|
+
return {
|
|
15016
|
+
code: 3,
|
|
15017
|
+
data: {
|
|
15018
|
+
success: false,
|
|
15019
|
+
command: "seo llms",
|
|
15020
|
+
errors: [String(error)]
|
|
15021
|
+
}
|
|
15022
|
+
};
|
|
15023
|
+
}
|
|
15024
|
+
}
|
|
15025
|
+
|
|
13400
15026
|
// src/commands/update.command.ts
|
|
13401
15027
|
async function updateCommand(options) {
|
|
13402
15028
|
const { dryRun, force, output, projectRoot } = options;
|
|
@@ -13599,6 +15225,8 @@ Commands:
|
|
|
13599
15225
|
install council Install generated council spec files to runner targets
|
|
13600
15226
|
install preset Install full preset (agents, prompts, skills, commands)
|
|
13601
15227
|
install lsp Install and configure LSP servers for AI coding tools
|
|
15228
|
+
seo audit Run SEO audit on a URL or sitemap
|
|
15229
|
+
seo llms Generate llms.txt from a URL or sitemap
|
|
13602
15230
|
doctor Validate configuration and generated files
|
|
13603
15231
|
update Update installed presets
|
|
13604
15232
|
|
|
@@ -13610,16 +15238,19 @@ Options:
|
|
|
13610
15238
|
--global Install to home directory (~/.claude, ~/.opencode, etc.)
|
|
13611
15239
|
--output, -o Output mode: human or json
|
|
13612
15240
|
--lang Comma-separated list of languages (e.g., typescript,php,python)
|
|
15241
|
+
--locale Instruction language for agent files: en (default) | es
|
|
13613
15242
|
|
|
13614
15243
|
Examples:
|
|
13615
15244
|
npx cc-codeconductor init
|
|
13616
15245
|
npx cc-codeconductor init --global
|
|
15246
|
+
npx cc-codeconductor init --locale=es
|
|
13617
15247
|
npx cc-codeconductor detect
|
|
13618
15248
|
npx cc-codeconductor install preset --target opencode
|
|
13619
15249
|
npx cc-codeconductor install preset --target claude
|
|
13620
15250
|
npx cc-codeconductor install preset --target codex
|
|
13621
15251
|
npx cc-codeconductor install preset --target all
|
|
13622
15252
|
npx cc-codeconductor install preset --target claude --global
|
|
15253
|
+
npx cc-codeconductor install preset --target claude --locale=es
|
|
13623
15254
|
npx cc-codeconductor install council --target opencode
|
|
13624
15255
|
npx cc-codeconductor install council --target claude
|
|
13625
15256
|
npx cc-codeconductor install council --target codex
|
|
@@ -13629,6 +15260,11 @@ Examples:
|
|
|
13629
15260
|
npx cc-codeconductor install lsp --target claude --dry-run
|
|
13630
15261
|
npx cc-codeconductor doctor
|
|
13631
15262
|
npx cc-codeconductor update --dry-run
|
|
15263
|
+
npx cc-codeconductor seo audit --url https://example.com
|
|
15264
|
+
npx cc-codeconductor seo audit --sitemap https://example.com/sitemap.xml
|
|
15265
|
+
npx cc-codeconductor seo audit --sitemap https://example.com/sitemap.xml --format markdown
|
|
15266
|
+
npx cc-codeconductor seo llms --sitemap https://example.com/sitemap.xml
|
|
15267
|
+
npx cc-codeconductor seo llms --url https://example.com --output llms.txt
|
|
13632
15268
|
`;
|
|
13633
15269
|
}
|
|
13634
15270
|
async function routeCommand(args, projectRoot) {
|
|
@@ -13642,7 +15278,8 @@ async function routeCommand(args, projectRoot) {
|
|
|
13642
15278
|
dryRun: flags.dryRun,
|
|
13643
15279
|
force: flags.force,
|
|
13644
15280
|
global: options.global === true || options.global === "true",
|
|
13645
|
-
output: flags.output
|
|
15281
|
+
output: flags.output,
|
|
15282
|
+
locale: options.locale === "es" ? "es" : "en"
|
|
13646
15283
|
});
|
|
13647
15284
|
case "detect":
|
|
13648
15285
|
return detectCommand({
|
|
@@ -13677,7 +15314,8 @@ async function routeCommand(args, projectRoot) {
|
|
|
13677
15314
|
dryRun: flags.dryRun,
|
|
13678
15315
|
force: flags.force,
|
|
13679
15316
|
global: isGlobal,
|
|
13680
|
-
output: flags.output
|
|
15317
|
+
output: flags.output,
|
|
15318
|
+
locale: options.locale === "es" ? "es" : options.locale === "en" ? "en" : undefined
|
|
13681
15319
|
});
|
|
13682
15320
|
}
|
|
13683
15321
|
return installCommand({
|
|
@@ -13701,6 +15339,37 @@ async function routeCommand(args, projectRoot) {
|
|
|
13701
15339
|
force: flags.force,
|
|
13702
15340
|
output: flags.output
|
|
13703
15341
|
});
|
|
15342
|
+
case "seo": {
|
|
15343
|
+
if (subcommand === "audit") {
|
|
15344
|
+
return seoAuditCommand({
|
|
15345
|
+
url: options.url,
|
|
15346
|
+
sitemap: options.sitemap,
|
|
15347
|
+
format: options.format ?? (flags.output === "json" ? "json" : "cli"),
|
|
15348
|
+
failOn: options["fail-on"] ?? "error",
|
|
15349
|
+
delay: options.delay ? parseInt(String(options.delay), 10) : 500,
|
|
15350
|
+
output: options.output,
|
|
15351
|
+
followRedirects: options["follow-redirects"] === true,
|
|
15352
|
+
projectRoot
|
|
15353
|
+
});
|
|
15354
|
+
}
|
|
15355
|
+
if (subcommand === "llms") {
|
|
15356
|
+
return seoLlmsCommand({
|
|
15357
|
+
url: options.url,
|
|
15358
|
+
sitemap: options.sitemap,
|
|
15359
|
+
output: options.output,
|
|
15360
|
+
delay: options.delay ? parseInt(String(options.delay), 10) : 500,
|
|
15361
|
+
projectRoot
|
|
15362
|
+
});
|
|
15363
|
+
}
|
|
15364
|
+
return {
|
|
15365
|
+
code: 1,
|
|
15366
|
+
data: {
|
|
15367
|
+
success: false,
|
|
15368
|
+
command: "seo",
|
|
15369
|
+
errors: ["Usage: seo audit|llms. Run `codeconductor seo audit --help` for details."]
|
|
15370
|
+
}
|
|
15371
|
+
};
|
|
15372
|
+
}
|
|
13704
15373
|
default:
|
|
13705
15374
|
return {
|
|
13706
15375
|
code: 1,
|
|
@@ -13792,6 +15461,8 @@ ${count} files processed${errCount > 0 ? `, ${errCount} errors` : ""}${note}`);
|
|
|
13792
15461
|
console.log(` - ${key}: ${value}`);
|
|
13793
15462
|
}
|
|
13794
15463
|
});
|
|
15464
|
+
} else if ("output" in data && typeof data.output === "string") {
|
|
15465
|
+
console.log(data.output);
|
|
13795
15466
|
}
|
|
13796
15467
|
}
|
|
13797
15468
|
}
|