cc-codeconductor 0.2.7 → 0.2.9
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 +25 -1
- package/dist/index.js +2486 -196
- package/package.json +1 -1
- package/presets/opencode/README.md +7 -7
- package/presets/opencode/agents/architect.md +0 -1
- package/presets/opencode/agents/docs.md +0 -1
- package/presets/opencode/agents/implementer.md +0 -1
- package/presets/opencode/agents/orchestrator.md +0 -1
- package/presets/opencode/agents/repo-explorer.md +0 -1
- package/presets/opencode/agents/reviewer.md +0 -1
- package/presets/opencode/agents/task-coach.md +0 -1
- package/presets/opencode/agents/tester.md +0 -1
- package/presets/opencode/opencode.jsonc +2 -11
- package/presets/opencode/prompts/v0.2.0/docs.md +1 -1
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +1 -1
- package/presets/opencode/prompts/v0.2.0/reviewer.md +1 -1
- package/presets/opencode/prompts/v0.2.0/task-coach.md +1 -1
- package/presets/seo-hotel/commands/cc-seo-audit.md +87 -0
- package/presets/seo-hotel/commands/cc-seo-llms.md +64 -0
- package/presets/seo-hotel/seo-hotel.yml +55 -0
- package/presets/seo-hotel/skills/astro-seo/SKILL.md +245 -0
- package/presets/seo-hotel/skills/geo-readiness/SKILL.md +221 -0
- package/presets/seo-hotel/skills/off-page/SKILL.md +201 -0
- package/presets/seo-hotel/skills/schema-validator/SKILL.md +325 -0
- package/presets/seo-hotel/skills/seo-audit/SKILL.md +191 -0
- package/src/presets/manifests/agy.yml +9 -0
- package/src/presets/manifests/opencode.yml +2 -1
- package/src/presets/models/agy.yml +59 -0
- package/src/presets/models/claude.yml +3 -3
- package/src/presets/models/codex.yml +3 -3
- package/src/presets/models/cursor.yml +3 -3
- package/src/presets/models/gemini.yml +3 -3
- package/src/presets/models/opencode.yml +11 -46
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.9",
|
|
7048
7048
|
description: "A multi-agent orchestration framework for AI-assisted software engineering workflows.",
|
|
7049
7049
|
keywords: [
|
|
7050
7050
|
"ai",
|
|
@@ -7132,6 +7132,13 @@ async function detectProject(rootDir) {
|
|
|
7132
7132
|
runtimes.push("python");
|
|
7133
7133
|
frameworks.push("django");
|
|
7134
7134
|
}
|
|
7135
|
+
const phpSignals = await detectPhp(rootDir);
|
|
7136
|
+
if (phpSignals.length > 0) {
|
|
7137
|
+
signals.push(...phpSignals);
|
|
7138
|
+
languages.push("php");
|
|
7139
|
+
runtimes.push("php");
|
|
7140
|
+
packageManagers.push("composer");
|
|
7141
|
+
}
|
|
7135
7142
|
const astroSignals = await detectAstro(rootDir);
|
|
7136
7143
|
if (astroSignals.length > 0) {
|
|
7137
7144
|
signals.push(...astroSignals);
|
|
@@ -7218,6 +7225,22 @@ async function detectAstro(rootDir) {
|
|
|
7218
7225
|
}
|
|
7219
7226
|
return signals;
|
|
7220
7227
|
}
|
|
7228
|
+
async function detectPhp(rootDir) {
|
|
7229
|
+
const { fileExists: fileExists2 } = await Promise.resolve().then(() => (init_safety(), exports_safety));
|
|
7230
|
+
const signals = [];
|
|
7231
|
+
if (await fileExists2(rootDir, "composer.json")) {
|
|
7232
|
+
signals.push("composer.json");
|
|
7233
|
+
}
|
|
7234
|
+
try {
|
|
7235
|
+
const { readdir } = await import("node:fs/promises");
|
|
7236
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
7237
|
+
const hasPhpFiles = entries.some((entry) => entry.isFile() && entry.name.endsWith(".php"));
|
|
7238
|
+
if (hasPhpFiles) {
|
|
7239
|
+
signals.push("*.php");
|
|
7240
|
+
}
|
|
7241
|
+
} catch {}
|
|
7242
|
+
return signals;
|
|
7243
|
+
}
|
|
7221
7244
|
|
|
7222
7245
|
// src/commands/detect.command.ts
|
|
7223
7246
|
async function detectCommand(options) {
|
|
@@ -11347,7 +11370,7 @@ var CodeConductorConfigSchema = exports_external.object({
|
|
|
11347
11370
|
profile: exports_external.string().optional()
|
|
11348
11371
|
}),
|
|
11349
11372
|
defaults: exports_external.object({
|
|
11350
|
-
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor"]),
|
|
11373
|
+
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor", "agy"]),
|
|
11351
11374
|
overwrite: exports_external.boolean()
|
|
11352
11375
|
}),
|
|
11353
11376
|
presets: exports_external.object({
|
|
@@ -11367,6 +11390,7 @@ var RunnerTargetSchema = exports_external.enum([
|
|
|
11367
11390
|
"codex",
|
|
11368
11391
|
"gemini",
|
|
11369
11392
|
"cursor",
|
|
11393
|
+
"agy",
|
|
11370
11394
|
"all"
|
|
11371
11395
|
]);
|
|
11372
11396
|
var InstallStrategySchema = exports_external.enum([
|
|
@@ -11384,20 +11408,23 @@ var ManifestEntrySchema = exports_external.object({
|
|
|
11384
11408
|
template: exports_external.boolean().optional()
|
|
11385
11409
|
});
|
|
11386
11410
|
var InstallManifestSchema = exports_external.object({
|
|
11387
|
-
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor"]),
|
|
11411
|
+
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor", "agy"]),
|
|
11388
11412
|
entries: exports_external.array(ManifestEntrySchema)
|
|
11389
11413
|
});
|
|
11390
11414
|
var ToolProviderNamesSchema = exports_external.record(exports_external.string(), exports_external.string());
|
|
11415
|
+
var PermissionProviderNamesSchema = exports_external.record(exports_external.string(), exports_external.string());
|
|
11391
11416
|
var ModelConfigSchema = exports_external.object({
|
|
11392
|
-
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor"]),
|
|
11417
|
+
target: exports_external.enum(["opencode", "claude", "codex", "gemini", "cursor", "agy"]),
|
|
11393
11418
|
agents: exports_external.record(exports_external.string(), exports_external.object({
|
|
11394
11419
|
claude: exports_external.string().optional(),
|
|
11395
11420
|
opencode: exports_external.string().optional(),
|
|
11396
11421
|
codex: exports_external.string().optional(),
|
|
11397
11422
|
gemini: exports_external.string().optional(),
|
|
11398
|
-
cursor: exports_external.string().optional()
|
|
11423
|
+
cursor: exports_external.string().optional(),
|
|
11424
|
+
agy: exports_external.string().optional()
|
|
11399
11425
|
})),
|
|
11400
|
-
tools: exports_external.record(exports_external.string(), ToolProviderNamesSchema).optional()
|
|
11426
|
+
tools: exports_external.record(exports_external.string(), ToolProviderNamesSchema).optional(),
|
|
11427
|
+
permissions: PermissionProviderNamesSchema.optional()
|
|
11401
11428
|
});
|
|
11402
11429
|
function validateCouncilSpec(data) {
|
|
11403
11430
|
return CouncilSpecSchema.parse(data);
|
|
@@ -11721,6 +11748,8 @@ function resolveAssets(target) {
|
|
|
11721
11748
|
return ["agents", "prompts/v0.2.0"];
|
|
11722
11749
|
case "cursor":
|
|
11723
11750
|
return ["agents", "prompts/v0.2.0"];
|
|
11751
|
+
case "agy":
|
|
11752
|
+
return ["agents", "prompts/v0.2.0"];
|
|
11724
11753
|
}
|
|
11725
11754
|
}
|
|
11726
11755
|
|
|
@@ -11904,7 +11933,27 @@ function getResponsibilities(agentId) {
|
|
|
11904
11933
|
devil: `- Challenge assumptions
|
|
11905
11934
|
- Find edge cases
|
|
11906
11935
|
- Identify failure modes
|
|
11907
|
-
- Stress test solutions
|
|
11936
|
+
- Stress test solutions`,
|
|
11937
|
+
"seo-auditor": `- Audit technical SEO
|
|
11938
|
+
- Check meta tags and headings
|
|
11939
|
+
- Validate crawl directives
|
|
11940
|
+
- Assess page speed signals`,
|
|
11941
|
+
"schema-validator": `- Validate Schema.org markup
|
|
11942
|
+
- Check JSON-LD syntax
|
|
11943
|
+
- Verify required properties
|
|
11944
|
+
- Suggest structured data fixes`,
|
|
11945
|
+
"geo-specialist": `- Assess AI-search readiness
|
|
11946
|
+
- Validate llms.txt
|
|
11947
|
+
- Check citable content
|
|
11948
|
+
- Review GEO optimization`,
|
|
11949
|
+
"content-strategist": `- Plan content marketing
|
|
11950
|
+
- Guide off-page SEO
|
|
11951
|
+
- Suggest backlink strategy
|
|
11952
|
+
- Review hotel copywriting`,
|
|
11953
|
+
"astro-specialist": `- Validate Astro SEO patterns
|
|
11954
|
+
- Check static generation
|
|
11955
|
+
- Review Islands Architecture
|
|
11956
|
+
- Optimize image handling`
|
|
11908
11957
|
};
|
|
11909
11958
|
return responsibilities[agentId] || "- Provide critical review";
|
|
11910
11959
|
}
|
|
@@ -12082,7 +12131,7 @@ function createCodexInstaller(spec) {
|
|
|
12082
12131
|
function generateOpenCodeFiles(spec) {
|
|
12083
12132
|
const files = [];
|
|
12084
12133
|
files.push({
|
|
12085
|
-
path: ".opencode/commands/council.md",
|
|
12134
|
+
path: ".opencode/commands/cc-council.md",
|
|
12086
12135
|
content: generateCouncilCommand(spec),
|
|
12087
12136
|
overwrite: false
|
|
12088
12137
|
});
|
|
@@ -12094,17 +12143,20 @@ function generateOpenCodeFiles(spec) {
|
|
|
12094
12143
|
for (const agent of spec.agents) {
|
|
12095
12144
|
files.push({
|
|
12096
12145
|
path: `.opencode/agents/council-${agent.id}.md`,
|
|
12097
|
-
content:
|
|
12146
|
+
content: generateOpenCodeAgentContent(agent),
|
|
12098
12147
|
overwrite: false
|
|
12099
12148
|
});
|
|
12100
12149
|
}
|
|
12101
12150
|
return files;
|
|
12102
12151
|
}
|
|
12103
12152
|
function generateCouncilCommand(spec) {
|
|
12104
|
-
return
|
|
12153
|
+
return `---
|
|
12154
|
+
description: ${yamlString(spec.description)}
|
|
12155
|
+
agent: council-lead
|
|
12156
|
+
subtask: true
|
|
12157
|
+
---
|
|
12105
12158
|
|
|
12106
|
-
|
|
12107
|
-
${spec.description}
|
|
12159
|
+
Run the CodeConductor council for multi-perspective analysis.
|
|
12108
12160
|
|
|
12109
12161
|
## Version
|
|
12110
12162
|
${spec.version}
|
|
@@ -12113,12 +12165,25 @@ ${spec.version}
|
|
|
12113
12165
|
${spec.agents.map((a) => `- ${a.role} (${a.id})`).join(`
|
|
12114
12166
|
`)}
|
|
12115
12167
|
|
|
12116
|
-
##
|
|
12117
|
-
|
|
12168
|
+
## Instructions
|
|
12169
|
+
Coordinate with the council agents and synthesize their perspectives into the configured output contract.
|
|
12118
12170
|
`;
|
|
12119
12171
|
}
|
|
12120
12172
|
function generateCouncilLead(spec) {
|
|
12121
|
-
return
|
|
12173
|
+
return `---
|
|
12174
|
+
description: ${yamlString(`${spec.description} Council lead. Coordinates council members and synthesizes recommendations.`)}
|
|
12175
|
+
mode: subagent
|
|
12176
|
+
permission:
|
|
12177
|
+
read: allow
|
|
12178
|
+
edit: deny
|
|
12179
|
+
bash: deny
|
|
12180
|
+
glob: allow
|
|
12181
|
+
grep: allow
|
|
12182
|
+
webfetch: deny
|
|
12183
|
+
websearch: deny
|
|
12184
|
+
---
|
|
12185
|
+
|
|
12186
|
+
# Council Lead Agent
|
|
12122
12187
|
|
|
12123
12188
|
## Role
|
|
12124
12189
|
Coordinates the council and synthesizes perspectives
|
|
@@ -12137,6 +12202,37 @@ ${spec.agents.map((a) => `- ${a.role}: ${a.focus.join(", ")}`).join(`
|
|
|
12137
12202
|
- Provide final recommendation
|
|
12138
12203
|
`;
|
|
12139
12204
|
}
|
|
12205
|
+
function generateOpenCodeAgentContent(agent) {
|
|
12206
|
+
return `---
|
|
12207
|
+
description: ${yamlString(`${agent.role} council agent. Focus: ${agent.focus.join(", ")}. Context: ${agent.context}. Model hint: ${agent.modelHint}.`)}
|
|
12208
|
+
mode: subagent
|
|
12209
|
+
permission:
|
|
12210
|
+
${generatePermissionBlock(agent.context)}
|
|
12211
|
+
---
|
|
12212
|
+
|
|
12213
|
+
${generateAgentContent(agent)}`;
|
|
12214
|
+
}
|
|
12215
|
+
function generatePermissionBlock(context) {
|
|
12216
|
+
if (context === "repo-readonly") {
|
|
12217
|
+
return ` read: allow
|
|
12218
|
+
edit: deny
|
|
12219
|
+
bash: deny
|
|
12220
|
+
glob: allow
|
|
12221
|
+
grep: allow
|
|
12222
|
+
webfetch: deny
|
|
12223
|
+
websearch: deny`;
|
|
12224
|
+
}
|
|
12225
|
+
return ` read: deny
|
|
12226
|
+
edit: deny
|
|
12227
|
+
bash: deny
|
|
12228
|
+
glob: deny
|
|
12229
|
+
grep: deny
|
|
12230
|
+
webfetch: deny
|
|
12231
|
+
websearch: deny`;
|
|
12232
|
+
}
|
|
12233
|
+
function yamlString(value) {
|
|
12234
|
+
return JSON.stringify(value);
|
|
12235
|
+
}
|
|
12140
12236
|
|
|
12141
12237
|
// src/adapters/opencode/opencode-installer.ts
|
|
12142
12238
|
class OpenCodeInstaller {
|
|
@@ -12320,7 +12416,7 @@ function renderTemplate(content, modelConfig, filePath) {
|
|
|
12320
12416
|
const agentModels = modelConfig.agents[agentRole];
|
|
12321
12417
|
const targetModel = agentModels[modelConfig.target];
|
|
12322
12418
|
let result2 = content.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 ?? "");
|
|
12323
|
-
if (modelConfig.tools) {
|
|
12419
|
+
if (modelConfig.tools || modelConfig.permissions) {
|
|
12324
12420
|
result2 = substituteToolNames(result2, modelConfig);
|
|
12325
12421
|
}
|
|
12326
12422
|
return result2;
|
|
@@ -12349,19 +12445,27 @@ function renderTemplate(content, modelConfig, filePath) {
|
|
|
12349
12445
|
}
|
|
12350
12446
|
}
|
|
12351
12447
|
}
|
|
12352
|
-
if (modelConfig.tools) {
|
|
12448
|
+
if (modelConfig.tools || modelConfig.permissions) {
|
|
12353
12449
|
result = substituteToolNames(result, modelConfig);
|
|
12354
12450
|
}
|
|
12355
12451
|
return result;
|
|
12356
12452
|
}
|
|
12357
12453
|
function substituteToolNames(content, modelConfig) {
|
|
12358
|
-
if (!modelConfig.tools)
|
|
12454
|
+
if (!modelConfig.tools && !modelConfig.permissions)
|
|
12359
12455
|
return content;
|
|
12360
12456
|
const target = modelConfig.target;
|
|
12361
12457
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
12362
12458
|
if (!fmMatch)
|
|
12363
12459
|
return content;
|
|
12364
12460
|
const frontmatter = fmMatch[1];
|
|
12461
|
+
if (target === "opencode" && modelConfig.permissions) {
|
|
12462
|
+
const updatedFrontmatter2 = frontmatter.replace(/^tools:\s*(.+)\n?/m, "");
|
|
12463
|
+
return content.replace(fmMatch[0], `---
|
|
12464
|
+
${updatedFrontmatter2}
|
|
12465
|
+
---`);
|
|
12466
|
+
}
|
|
12467
|
+
if (!modelConfig.tools)
|
|
12468
|
+
return content;
|
|
12365
12469
|
const updatedFrontmatter = frontmatter.replace(/^tools:\s*(.+)$/m, (_match, toolsLine) => {
|
|
12366
12470
|
const baseNames = toolsLine.split(",").map((t) => t.trim());
|
|
12367
12471
|
const mappedNames = baseNames.map((baseName) => {
|
|
@@ -12390,7 +12494,7 @@ async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTem
|
|
|
12390
12494
|
const incomingContent = isTemplate && modelConfig ? renderTemplate(content, modelConfig, srcPath) : content;
|
|
12391
12495
|
let finalContent = incomingContent;
|
|
12392
12496
|
let action = "written";
|
|
12393
|
-
if (strategy === "append"
|
|
12497
|
+
if (strategy === "append") {
|
|
12394
12498
|
let existing = "";
|
|
12395
12499
|
try {
|
|
12396
12500
|
existing = await readFile4(destPath, "utf-8");
|
|
@@ -12403,7 +12507,7 @@ async function applySingleFile(srcPath, destPath, strategy, force, dryRun, isTem
|
|
|
12403
12507
|
` + incomingContent;
|
|
12404
12508
|
}
|
|
12405
12509
|
action = "appended";
|
|
12406
|
-
} else if (strategy === "merge-json"
|
|
12510
|
+
} else if (strategy === "merge-json") {
|
|
12407
12511
|
let existing = {};
|
|
12408
12512
|
try {
|
|
12409
12513
|
existing = JSON.parse(await readFile4(destPath, "utf-8"));
|
|
@@ -12504,8 +12608,8 @@ async function loadCouncilPreset(projectRoot) {
|
|
|
12504
12608
|
}
|
|
12505
12609
|
|
|
12506
12610
|
// src/core/runner/runner-target.ts
|
|
12507
|
-
var RUNNER_TARGETS = ["opencode", "claude", "codex", "gemini", "cursor", "all"];
|
|
12508
|
-
var INDIVIDUAL_TARGETS = ["opencode", "claude", "codex", "gemini", "cursor"];
|
|
12611
|
+
var RUNNER_TARGETS = ["opencode", "claude", "codex", "gemini", "cursor", "agy", "all"];
|
|
12612
|
+
var INDIVIDUAL_TARGETS = ["opencode", "claude", "codex", "gemini", "cursor", "agy"];
|
|
12509
12613
|
function isRunnerTarget(value) {
|
|
12510
12614
|
return RUNNER_TARGETS.includes(value);
|
|
12511
12615
|
}
|
|
@@ -12661,202 +12765,2335 @@ async function installPresetCommand(options) {
|
|
|
12661
12765
|
}
|
|
12662
12766
|
}
|
|
12663
12767
|
|
|
12664
|
-
// src/commands/
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
errors: ["No config found. Run `codeconductor init` first."]
|
|
12676
|
-
}
|
|
12677
|
-
};
|
|
12678
|
-
}
|
|
12679
|
-
const config = configResult.data;
|
|
12680
|
-
if (!config.presets.council.enabled) {
|
|
12681
|
-
return {
|
|
12682
|
-
code: 4,
|
|
12683
|
-
data: {
|
|
12684
|
-
success: false,
|
|
12685
|
-
command: "update",
|
|
12686
|
-
errors: ["Council preset is not enabled"]
|
|
12687
|
-
}
|
|
12688
|
-
};
|
|
12768
|
+
// src/commands/install-lsp.command.ts
|
|
12769
|
+
import { homedir as homedir4 } from "node:os";
|
|
12770
|
+
import { resolve as resolve8 } from "node:path";
|
|
12771
|
+
|
|
12772
|
+
// src/core/lsp/lsp-config-utils.ts
|
|
12773
|
+
function getLanguageServerConfig(lspIds) {
|
|
12774
|
+
const languageServers = {};
|
|
12775
|
+
for (const lspId of lspIds) {
|
|
12776
|
+
const config = getLspCommand(lspId);
|
|
12777
|
+
if (config) {
|
|
12778
|
+
languageServers[lspId] = { command: config.command, args: [...config.args] };
|
|
12689
12779
|
}
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12780
|
+
}
|
|
12781
|
+
return languageServers;
|
|
12782
|
+
}
|
|
12783
|
+
function getLspCommand(lspId) {
|
|
12784
|
+
switch (lspId) {
|
|
12785
|
+
case "typescript":
|
|
12786
|
+
return { command: "typescript-language-server", args: ["--stdio"] };
|
|
12787
|
+
case "php":
|
|
12788
|
+
return { command: "intelephense", args: ["--stdio"] };
|
|
12789
|
+
case "python":
|
|
12790
|
+
return { command: "pyright-langserver", args: ["--stdio"] };
|
|
12791
|
+
case "kotlin":
|
|
12792
|
+
return { command: "kotlin-language-server", args: [] };
|
|
12793
|
+
default:
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12796
|
+
}
|
|
12797
|
+
|
|
12798
|
+
// src/adapters/agy/agy-lsp-generator.ts
|
|
12799
|
+
class AgyLspGenerator {
|
|
12800
|
+
name = "agy-lsp";
|
|
12801
|
+
target = "agy";
|
|
12802
|
+
generate(installedLsps) {
|
|
12803
|
+
const successfulLsps = installedLsps.filter((lsp) => lsp.status !== "failed");
|
|
12804
|
+
if (successfulLsps.length === 0) {
|
|
12805
|
+
return [];
|
|
12700
12806
|
}
|
|
12701
|
-
const
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12807
|
+
const sections = [
|
|
12808
|
+
"# Agy LSP Configuration (experimental)",
|
|
12809
|
+
"# NOTE: Agy config format may change as the tool evolves",
|
|
12810
|
+
""
|
|
12811
|
+
];
|
|
12812
|
+
for (const lsp of successfulLsps) {
|
|
12813
|
+
const config = getLspCommand(lsp.lspId);
|
|
12814
|
+
if (config) {
|
|
12815
|
+
sections.push(`${lsp.lspId}:`);
|
|
12816
|
+
sections.push(` command: ${config.command}`);
|
|
12817
|
+
if (config.args.length > 0) {
|
|
12818
|
+
sections.push(` args: [${config.args.join(", ")}]`);
|
|
12713
12819
|
}
|
|
12714
|
-
|
|
12820
|
+
sections.push("");
|
|
12821
|
+
}
|
|
12715
12822
|
}
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12823
|
+
return [
|
|
12824
|
+
{
|
|
12825
|
+
path: ".agy/tools.yaml",
|
|
12826
|
+
content: sections.join(`
|
|
12827
|
+
`),
|
|
12828
|
+
overwrite: false
|
|
12829
|
+
}
|
|
12830
|
+
];
|
|
12831
|
+
}
|
|
12832
|
+
async isAvailable() {
|
|
12833
|
+
return true;
|
|
12834
|
+
}
|
|
12835
|
+
}
|
|
12836
|
+
function createAgyLspGenerator() {
|
|
12837
|
+
return new AgyLspGenerator;
|
|
12838
|
+
}
|
|
12839
|
+
|
|
12840
|
+
// src/adapters/claude/claude-lsp-generator.ts
|
|
12841
|
+
class ClaudeLspGenerator {
|
|
12842
|
+
name = "claude-lsp";
|
|
12843
|
+
target = "claude";
|
|
12844
|
+
generate(installedLsps) {
|
|
12845
|
+
const successfulLsps = installedLsps.filter((lsp) => lsp.status !== "failed");
|
|
12846
|
+
if (successfulLsps.length === 0) {
|
|
12847
|
+
return [];
|
|
12728
12848
|
}
|
|
12729
|
-
const
|
|
12730
|
-
const
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12849
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12850
|
+
const content = JSON.stringify(languageServers, null, 2);
|
|
12851
|
+
return [
|
|
12852
|
+
{
|
|
12853
|
+
path: ".claude/plugins/codeconductor-lsp/.lsp.json",
|
|
12854
|
+
content,
|
|
12855
|
+
overwrite: false
|
|
12856
|
+
}
|
|
12857
|
+
];
|
|
12858
|
+
}
|
|
12859
|
+
async isAvailable() {
|
|
12860
|
+
return true;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
function createClaudeLspGenerator() {
|
|
12864
|
+
return new ClaudeLspGenerator;
|
|
12865
|
+
}
|
|
12866
|
+
|
|
12867
|
+
// src/adapters/codex/codex-lsp-generator.ts
|
|
12868
|
+
class CodexLspGenerator {
|
|
12869
|
+
name = "codex-lsp";
|
|
12870
|
+
target = "codex";
|
|
12871
|
+
generate(installedLsps) {
|
|
12872
|
+
const successfulLsps = installedLsps.filter((lsp) => lsp.status !== "failed");
|
|
12873
|
+
if (successfulLsps.length === 0) {
|
|
12874
|
+
return [];
|
|
12751
12875
|
}
|
|
12752
|
-
const
|
|
12753
|
-
const
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
success: false,
|
|
12761
|
-
command: "update",
|
|
12762
|
-
errors: errors3
|
|
12876
|
+
const sections = ["# Codex LSP Configuration", ""];
|
|
12877
|
+
for (const lsp of successfulLsps) {
|
|
12878
|
+
const config = getLspCommand(lsp.lspId);
|
|
12879
|
+
if (config) {
|
|
12880
|
+
sections.push(`[language_servers.${lsp.lspId}]`);
|
|
12881
|
+
sections.push(`command = "${config.command}"`);
|
|
12882
|
+
if (config.args.length > 0) {
|
|
12883
|
+
sections.push(`args = [${config.args.map((a) => `"${a}"`).join(", ")}]`);
|
|
12763
12884
|
}
|
|
12764
|
-
|
|
12765
|
-
}
|
|
12766
|
-
return {
|
|
12767
|
-
code: 0,
|
|
12768
|
-
data: {
|
|
12769
|
-
success: true,
|
|
12770
|
-
command: "update",
|
|
12771
|
-
message: "Updated successfully",
|
|
12772
|
-
currentVersion,
|
|
12773
|
-
newVersion,
|
|
12774
|
-
updated
|
|
12885
|
+
sections.push("");
|
|
12775
12886
|
}
|
|
12776
|
-
}
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
errors: [String(error)]
|
|
12887
|
+
}
|
|
12888
|
+
return [
|
|
12889
|
+
{
|
|
12890
|
+
path: ".codex/config.toml",
|
|
12891
|
+
content: sections.join(`
|
|
12892
|
+
`),
|
|
12893
|
+
overwrite: false
|
|
12784
12894
|
}
|
|
12785
|
-
|
|
12895
|
+
];
|
|
12896
|
+
}
|
|
12897
|
+
async isAvailable() {
|
|
12898
|
+
return true;
|
|
12786
12899
|
}
|
|
12787
12900
|
}
|
|
12901
|
+
function createCodexLspGenerator() {
|
|
12902
|
+
return new CodexLspGenerator;
|
|
12903
|
+
}
|
|
12788
12904
|
|
|
12789
|
-
// src/
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
};
|
|
12798
|
-
const options = {};
|
|
12799
|
-
const remaining = [];
|
|
12800
|
-
for (const arg of args) {
|
|
12801
|
-
if (arg === "--help" || arg === "-h") {
|
|
12802
|
-
flags.help = true;
|
|
12803
|
-
} else if (arg === "--version" || arg === "-v") {
|
|
12804
|
-
flags.version = true;
|
|
12805
|
-
} else if (arg === "--dry-run") {
|
|
12806
|
-
flags.dryRun = true;
|
|
12807
|
-
} else if (arg === "--force") {
|
|
12808
|
-
flags.force = true;
|
|
12809
|
-
} else if (arg === "--output" || arg === "-o") {
|
|
12810
|
-
remaining.push(arg);
|
|
12811
|
-
} else if (arg.startsWith("--output=") || arg.startsWith("-o=")) {
|
|
12812
|
-
const value = arg.split("=")[1];
|
|
12813
|
-
if (value === "json" || value === "human") {
|
|
12814
|
-
flags.output = value;
|
|
12815
|
-
}
|
|
12816
|
-
} else {
|
|
12817
|
-
remaining.push(arg);
|
|
12905
|
+
// src/adapters/cursor/cursor-lsp-generator.ts
|
|
12906
|
+
class CursorLspGenerator {
|
|
12907
|
+
name = "cursor-lsp";
|
|
12908
|
+
target = "cursor";
|
|
12909
|
+
generate(installedLsps) {
|
|
12910
|
+
const successfulLsps = installedLsps.filter((lsp) => lsp.status !== "failed");
|
|
12911
|
+
if (successfulLsps.length === 0) {
|
|
12912
|
+
return [];
|
|
12818
12913
|
}
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12914
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12915
|
+
const content = JSON.stringify({
|
|
12916
|
+
languageServers
|
|
12917
|
+
}, null, 2);
|
|
12918
|
+
return [
|
|
12919
|
+
{
|
|
12920
|
+
path: ".cursor/settings.json",
|
|
12921
|
+
content,
|
|
12922
|
+
overwrite: false
|
|
12827
12923
|
}
|
|
12828
|
-
|
|
12924
|
+
];
|
|
12829
12925
|
}
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12926
|
+
async isAvailable() {
|
|
12927
|
+
return true;
|
|
12928
|
+
}
|
|
12929
|
+
}
|
|
12930
|
+
function createCursorLspGenerator() {
|
|
12931
|
+
return new CursorLspGenerator;
|
|
12932
|
+
}
|
|
12933
|
+
|
|
12934
|
+
// src/adapters/gemini/gemini-lsp-generator.ts
|
|
12935
|
+
class GeminiLspGenerator {
|
|
12936
|
+
name = "gemini-lsp";
|
|
12937
|
+
target = "gemini";
|
|
12938
|
+
generate(installedLsps) {
|
|
12939
|
+
const successfulLsps = installedLsps.filter((lsp) => lsp.status !== "failed");
|
|
12940
|
+
if (successfulLsps.length === 0) {
|
|
12941
|
+
return [];
|
|
12843
12942
|
}
|
|
12943
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp) => lsp.lspId));
|
|
12944
|
+
const content = JSON.stringify({
|
|
12945
|
+
languageServers
|
|
12946
|
+
}, null, 2);
|
|
12947
|
+
return [
|
|
12948
|
+
{
|
|
12949
|
+
path: ".gemini/settings.json",
|
|
12950
|
+
content,
|
|
12951
|
+
overwrite: false
|
|
12952
|
+
}
|
|
12953
|
+
];
|
|
12954
|
+
}
|
|
12955
|
+
async isAvailable() {
|
|
12956
|
+
return true;
|
|
12844
12957
|
}
|
|
12845
|
-
return { command, subcommand, options, flags };
|
|
12846
12958
|
}
|
|
12847
|
-
function
|
|
12848
|
-
return
|
|
12959
|
+
function createGeminiLspGenerator() {
|
|
12960
|
+
return new GeminiLspGenerator;
|
|
12849
12961
|
}
|
|
12850
|
-
function getHelp() {
|
|
12851
|
-
return `CodeConductor CLI v${package_default.version}
|
|
12852
12962
|
|
|
12853
|
-
|
|
12963
|
+
// src/adapters/opencode/opencode-lsp-generator.ts
|
|
12964
|
+
var OPENCODE_LSP_EXTENSIONS = {
|
|
12965
|
+
typescript: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
12966
|
+
php: [".php"],
|
|
12967
|
+
python: [".py", ".pyi"],
|
|
12968
|
+
kotlin: [".kt", ".kts"]
|
|
12969
|
+
};
|
|
12854
12970
|
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12971
|
+
class OpenCodeLspGenerator {
|
|
12972
|
+
name = "opencode-lsp";
|
|
12973
|
+
target = "opencode";
|
|
12974
|
+
generate(installedLsps) {
|
|
12975
|
+
const successfulLsps = installedLsps.filter((lsp2) => lsp2.status !== "failed");
|
|
12976
|
+
if (successfulLsps.length === 0) {
|
|
12977
|
+
return [];
|
|
12978
|
+
}
|
|
12979
|
+
const languageServers = getLanguageServerConfig(successfulLsps.map((lsp2) => lsp2.lspId));
|
|
12980
|
+
const lsp = this.toOpenCodeLspConfig(languageServers);
|
|
12981
|
+
const content = JSON.stringify({
|
|
12982
|
+
$schema: "https://opencode.ai/config.json",
|
|
12983
|
+
lsp
|
|
12984
|
+
}, null, 2);
|
|
12985
|
+
return [
|
|
12986
|
+
{
|
|
12987
|
+
path: ".opencode/opencode.json",
|
|
12988
|
+
content: `${content}
|
|
12989
|
+
`,
|
|
12990
|
+
overwrite: false
|
|
12991
|
+
}
|
|
12992
|
+
];
|
|
12993
|
+
}
|
|
12994
|
+
toOpenCodeLspConfig(languageServers) {
|
|
12995
|
+
return Object.fromEntries(Object.entries(languageServers).map(([name, config]) => [
|
|
12996
|
+
name,
|
|
12997
|
+
{
|
|
12998
|
+
command: [config.command, ...config.args],
|
|
12999
|
+
extensions: OPENCODE_LSP_EXTENSIONS[name] ?? []
|
|
13000
|
+
}
|
|
13001
|
+
]));
|
|
13002
|
+
}
|
|
13003
|
+
async isAvailable() {
|
|
13004
|
+
return true;
|
|
13005
|
+
}
|
|
13006
|
+
}
|
|
13007
|
+
function createOpenCodeLspGenerator() {
|
|
13008
|
+
return new OpenCodeLspGenerator;
|
|
13009
|
+
}
|
|
13010
|
+
|
|
13011
|
+
// src/core/lsp/lsp-installer.ts
|
|
13012
|
+
import { execFile } from "node:child_process";
|
|
13013
|
+
import { access as access5, mkdir as mkdir5 } from "node:fs/promises";
|
|
13014
|
+
import { homedir as homedir3 } from "node:os";
|
|
13015
|
+
import { join as join4 } from "node:path";
|
|
13016
|
+
import { promisify } from "node:util";
|
|
13017
|
+
var execFileAsync = promisify(execFile);
|
|
13018
|
+
|
|
13019
|
+
class LspInstaller {
|
|
13020
|
+
lspBinDir;
|
|
13021
|
+
constructor() {
|
|
13022
|
+
this.lspBinDir = join4(homedir3(), ".codeconductor", "lsp", "bin");
|
|
13023
|
+
}
|
|
13024
|
+
async checkInstalled(def) {
|
|
13025
|
+
try {
|
|
13026
|
+
const { stdout } = await execFileAsync("which", [def.binaryName], { timeout: 5000 });
|
|
13027
|
+
const path = stdout.trim();
|
|
13028
|
+
if (path) {
|
|
13029
|
+
const version = await this.getVersion(def);
|
|
13030
|
+
return { installed: true, version, path };
|
|
13031
|
+
}
|
|
13032
|
+
} catch {}
|
|
13033
|
+
if (def.packageManager === "npm" && def.npmDetect) {
|
|
13034
|
+
try {
|
|
13035
|
+
const { stdout } = await execFileAsync("npm", ["list", "-g", def.npmDetect, "--depth=0"], { timeout: 1e4 });
|
|
13036
|
+
if (stdout.includes(def.npmDetect)) {
|
|
13037
|
+
const version = this.parseVersionFromNpmList(stdout, def.npmDetect);
|
|
13038
|
+
return { installed: true, version };
|
|
13039
|
+
}
|
|
13040
|
+
} catch {}
|
|
13041
|
+
}
|
|
13042
|
+
if (def.packageManager === "pip" && def.pipDetect) {
|
|
13043
|
+
try {
|
|
13044
|
+
const { stdout } = await execFileAsync("pip", ["show", def.pipDetect], { timeout: 1e4 });
|
|
13045
|
+
if (stdout.includes("Version:")) {
|
|
13046
|
+
const version = this.parseVersionFromPipShow(stdout);
|
|
13047
|
+
return { installed: true, version };
|
|
13048
|
+
}
|
|
13049
|
+
} catch {}
|
|
13050
|
+
}
|
|
13051
|
+
return { installed: false };
|
|
13052
|
+
}
|
|
13053
|
+
async installLsp(def) {
|
|
13054
|
+
const status = await this.checkInstalled(def);
|
|
13055
|
+
if (status.installed) {
|
|
13056
|
+
return {
|
|
13057
|
+
lspId: def.id,
|
|
13058
|
+
status: "already-installed",
|
|
13059
|
+
version: status.version
|
|
13060
|
+
};
|
|
13061
|
+
}
|
|
13062
|
+
try {
|
|
13063
|
+
switch (def.packageManager) {
|
|
13064
|
+
case "npm":
|
|
13065
|
+
await this.installNpm(def);
|
|
13066
|
+
break;
|
|
13067
|
+
case "pip":
|
|
13068
|
+
await this.installPip(def);
|
|
13069
|
+
break;
|
|
13070
|
+
case "binary":
|
|
13071
|
+
await this.installBinary(def);
|
|
13072
|
+
break;
|
|
13073
|
+
}
|
|
13074
|
+
const newStatus = await this.checkInstalled(def);
|
|
13075
|
+
return {
|
|
13076
|
+
lspId: def.id,
|
|
13077
|
+
status: "installed",
|
|
13078
|
+
version: newStatus.version
|
|
13079
|
+
};
|
|
13080
|
+
} catch (error) {
|
|
13081
|
+
return {
|
|
13082
|
+
lspId: def.id,
|
|
13083
|
+
status: "failed",
|
|
13084
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13085
|
+
};
|
|
13086
|
+
}
|
|
13087
|
+
}
|
|
13088
|
+
async installAll(lsps, options) {
|
|
13089
|
+
const results = [];
|
|
13090
|
+
for (const lsp of lsps) {
|
|
13091
|
+
if (options.dryRun) {
|
|
13092
|
+
const status = await this.checkInstalled(lsp);
|
|
13093
|
+
results.push({
|
|
13094
|
+
lspId: lsp.id,
|
|
13095
|
+
status: status.installed ? "already-installed" : "installed",
|
|
13096
|
+
version: status.version
|
|
13097
|
+
});
|
|
13098
|
+
} else {
|
|
13099
|
+
const result = await this.installLsp(lsp);
|
|
13100
|
+
results.push(result);
|
|
13101
|
+
}
|
|
13102
|
+
}
|
|
13103
|
+
return {
|
|
13104
|
+
results,
|
|
13105
|
+
allSucceeded: results.every((r) => r.status !== "failed")
|
|
13106
|
+
};
|
|
13107
|
+
}
|
|
13108
|
+
async getVersion(def) {
|
|
13109
|
+
try {
|
|
13110
|
+
const { stdout } = await execFileAsync(def.binaryName, [def.versionFlag], { timeout: 5000 });
|
|
13111
|
+
return stdout.trim().split(`
|
|
13112
|
+
`)[0];
|
|
13113
|
+
} catch {
|
|
13114
|
+
return;
|
|
13115
|
+
}
|
|
13116
|
+
}
|
|
13117
|
+
parseVersionFromNpmList(output, packageName) {
|
|
13118
|
+
const match = output.match(new RegExp(`${packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@([\\d.]+)`));
|
|
13119
|
+
return match?.[1];
|
|
13120
|
+
}
|
|
13121
|
+
parseVersionFromPipShow(output) {
|
|
13122
|
+
const match = output.match(/Version:\s*([\d.]+)/);
|
|
13123
|
+
return match?.[1];
|
|
13124
|
+
}
|
|
13125
|
+
async installNpm(def) {
|
|
13126
|
+
try {
|
|
13127
|
+
await execFileAsync("npm", ["install", "-g", def.package], { timeout: 120000 });
|
|
13128
|
+
} catch (error) {
|
|
13129
|
+
throw new Error(`Failed to install ${def.serverName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
13130
|
+
}
|
|
13131
|
+
}
|
|
13132
|
+
async installPip(def) {
|
|
13133
|
+
try {
|
|
13134
|
+
await execFileAsync("pip", ["install", "--user", def.package], { timeout: 120000 });
|
|
13135
|
+
} catch (error) {
|
|
13136
|
+
throw new Error(`Failed to install ${def.serverName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
13137
|
+
}
|
|
13138
|
+
}
|
|
13139
|
+
async installBinary(def) {
|
|
13140
|
+
if (!def.binaryPlatforms) {
|
|
13141
|
+
throw new Error(`No binary platforms defined for ${def.serverName}`);
|
|
13142
|
+
}
|
|
13143
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
13144
|
+
const binary = def.binaryPlatforms[platformKey];
|
|
13145
|
+
if (!binary) {
|
|
13146
|
+
throw new Error(`No binary available for platform: ${platformKey}`);
|
|
13147
|
+
}
|
|
13148
|
+
await mkdir5(this.lspBinDir, { recursive: true });
|
|
13149
|
+
const destPath = join4(this.lspBinDir, def.binaryName);
|
|
13150
|
+
try {
|
|
13151
|
+
await access5(destPath);
|
|
13152
|
+
return;
|
|
13153
|
+
} catch {}
|
|
13154
|
+
const { execSync } = await import("node:child_process");
|
|
13155
|
+
const isWindows = process.platform === "win32";
|
|
13156
|
+
const extractCmd = binary.url.endsWith(".zip") ? `curl -L "${binary.url}" | tar -xz -C "${this.lspBinDir}"` : `curl -L "${binary.url}" | tar -xz -C "${this.lspBinDir}"`;
|
|
13157
|
+
try {
|
|
13158
|
+
execSync(extractCmd, { timeout: 120000, stdio: "pipe" });
|
|
13159
|
+
if (!isWindows) {
|
|
13160
|
+
execSync(`chmod +x "${destPath}"`, { stdio: "pipe" });
|
|
13161
|
+
}
|
|
13162
|
+
} catch (error) {
|
|
13163
|
+
throw new Error(`Failed to download ${def.serverName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
13164
|
+
}
|
|
13165
|
+
}
|
|
13166
|
+
}
|
|
13167
|
+
function createLspInstaller() {
|
|
13168
|
+
return new LspInstaller;
|
|
13169
|
+
}
|
|
13170
|
+
|
|
13171
|
+
// src/core/lsp/lsp-registry.ts
|
|
13172
|
+
var LSP_DEFINITIONS = [
|
|
13173
|
+
{
|
|
13174
|
+
id: "typescript",
|
|
13175
|
+
language: "typescript",
|
|
13176
|
+
serverName: "TypeScript Language Server",
|
|
13177
|
+
packageManager: "npm",
|
|
13178
|
+
package: "typescript-language-server",
|
|
13179
|
+
binaryName: "typescript-language-server",
|
|
13180
|
+
installCmd: "npm install -g typescript-language-server",
|
|
13181
|
+
versionFlag: "--version",
|
|
13182
|
+
npmDetect: "typescript-language-server"
|
|
13183
|
+
},
|
|
13184
|
+
{
|
|
13185
|
+
id: "php",
|
|
13186
|
+
language: "php",
|
|
13187
|
+
serverName: "Intelephense",
|
|
13188
|
+
packageManager: "npm",
|
|
13189
|
+
package: "@bmewburn/vscode-intelephense-client",
|
|
13190
|
+
binaryName: "intelephense",
|
|
13191
|
+
installCmd: "npm install -g @bmewburn/vscode-intelephense-client",
|
|
13192
|
+
versionFlag: "--version",
|
|
13193
|
+
npmDetect: "@bmewburn/vscode-intelephense-client"
|
|
13194
|
+
},
|
|
13195
|
+
{
|
|
13196
|
+
id: "python",
|
|
13197
|
+
language: "python",
|
|
13198
|
+
serverName: "Pyright",
|
|
13199
|
+
packageManager: "npm",
|
|
13200
|
+
package: "pyright",
|
|
13201
|
+
binaryName: "pyright-langserver",
|
|
13202
|
+
installCmd: "npm install -g pyright",
|
|
13203
|
+
versionFlag: "--version",
|
|
13204
|
+
npmDetect: "pyright"
|
|
13205
|
+
},
|
|
13206
|
+
{
|
|
13207
|
+
id: "kotlin",
|
|
13208
|
+
language: "kotlin",
|
|
13209
|
+
serverName: "Kotlin Language Server",
|
|
13210
|
+
packageManager: "binary",
|
|
13211
|
+
package: "kotlin-language-server",
|
|
13212
|
+
binaryName: "kotlin-language-server",
|
|
13213
|
+
installCmd: "Download from GitHub releases",
|
|
13214
|
+
versionFlag: "--version",
|
|
13215
|
+
binaryPlatforms: {
|
|
13216
|
+
"linux-x64": {
|
|
13217
|
+
url: "https://github.com/fwcd/kotlin-language-server/releases/latest/download/server-linux-x64.tar.gz"
|
|
13218
|
+
},
|
|
13219
|
+
"linux-arm64": {
|
|
13220
|
+
url: "https://github.com/fwcd/kotlin-language-server/releases/latest/download/server-linux-arm64.tar.gz"
|
|
13221
|
+
},
|
|
13222
|
+
"darwin-x64": {
|
|
13223
|
+
url: "https://github.com/fwcd/kotlin-language-server/releases/latest/download/server-macos-x64.tar.gz"
|
|
13224
|
+
},
|
|
13225
|
+
"darwin-arm64": {
|
|
13226
|
+
url: "https://github.com/fwcd/kotlin-language-server/releases/latest/download/server-macos-arm64.tar.gz"
|
|
13227
|
+
},
|
|
13228
|
+
"win32-x64": {
|
|
13229
|
+
url: "https://github.com/fwcd/kotlin-language-server/releases/latest/download/server-windows-x64.zip"
|
|
13230
|
+
}
|
|
13231
|
+
}
|
|
13232
|
+
}
|
|
13233
|
+
];
|
|
13234
|
+
function resolveLsps(languages) {
|
|
13235
|
+
const languageToLspId = {
|
|
13236
|
+
typescript: "typescript",
|
|
13237
|
+
javascript: "typescript",
|
|
13238
|
+
php: "php",
|
|
13239
|
+
python: "python",
|
|
13240
|
+
java: "kotlin",
|
|
13241
|
+
kotlin: "kotlin"
|
|
13242
|
+
};
|
|
13243
|
+
const resolvedIds = new Set;
|
|
13244
|
+
for (const lang of languages) {
|
|
13245
|
+
const lspId = languageToLspId[lang.toLowerCase()];
|
|
13246
|
+
if (lspId) {
|
|
13247
|
+
resolvedIds.add(lspId);
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13250
|
+
return LSP_DEFINITIONS.filter((def) => resolvedIds.has(def.id));
|
|
13251
|
+
}
|
|
13252
|
+
|
|
13253
|
+
// src/commands/install-lsp.command.ts
|
|
13254
|
+
async function installLspCommand(options) {
|
|
13255
|
+
const { target, lang, dryRun, force, global: isGlobal, output, projectRoot } = options;
|
|
13256
|
+
const baseDir = isGlobal ? homedir4() : projectRoot;
|
|
13257
|
+
try {
|
|
13258
|
+
const runnerTarget = parseRunnerTarget(target);
|
|
13259
|
+
const targets = getIndividualTargets(runnerTarget);
|
|
13260
|
+
let languages;
|
|
13261
|
+
if (lang && lang.length > 0) {
|
|
13262
|
+
languages = lang;
|
|
13263
|
+
} else {
|
|
13264
|
+
const profile = await detectProject(projectRoot);
|
|
13265
|
+
languages = profile.languages;
|
|
13266
|
+
}
|
|
13267
|
+
if (languages.length === 0) {
|
|
13268
|
+
return {
|
|
13269
|
+
code: 1,
|
|
13270
|
+
data: {
|
|
13271
|
+
success: false,
|
|
13272
|
+
command: "install",
|
|
13273
|
+
subcommand: "lsp",
|
|
13274
|
+
errors: ["No languages detected. Use --lang to specify languages manually."]
|
|
13275
|
+
}
|
|
13276
|
+
};
|
|
13277
|
+
}
|
|
13278
|
+
const lsps = resolveLsps(languages);
|
|
13279
|
+
if (lsps.length === 0) {
|
|
13280
|
+
return {
|
|
13281
|
+
code: 1,
|
|
13282
|
+
data: {
|
|
13283
|
+
success: false,
|
|
13284
|
+
command: "install",
|
|
13285
|
+
subcommand: "lsp",
|
|
13286
|
+
errors: [`No LSP servers available for languages: ${languages.join(", ")}`]
|
|
13287
|
+
}
|
|
13288
|
+
};
|
|
13289
|
+
}
|
|
13290
|
+
const installer = createLspInstaller();
|
|
13291
|
+
const installReport = await installer.installAll(lsps, { dryRun });
|
|
13292
|
+
const writeOptions = { dryRun, force };
|
|
13293
|
+
const allConfigResults = [];
|
|
13294
|
+
for (const t of targets) {
|
|
13295
|
+
const generator = getLspConfigGenerator(t);
|
|
13296
|
+
if (!generator) {
|
|
13297
|
+
continue;
|
|
13298
|
+
}
|
|
13299
|
+
const generatedFiles = generator.generate(installReport.results);
|
|
13300
|
+
const resolvedFiles = generatedFiles.map((f) => ({
|
|
13301
|
+
...f,
|
|
13302
|
+
path: resolve8(baseDir, f.path)
|
|
13303
|
+
}));
|
|
13304
|
+
const results = await writeGeneratedFiles(resolvedFiles, writeOptions);
|
|
13305
|
+
for (const result of results) {
|
|
13306
|
+
allConfigResults.push({
|
|
13307
|
+
target: t,
|
|
13308
|
+
path: result.path,
|
|
13309
|
+
success: result.success,
|
|
13310
|
+
error: result.error
|
|
13311
|
+
});
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
const errors3 = allConfigResults.filter((r) => !r.success).map((r) => `${r.path}: ${r.error}`);
|
|
13315
|
+
if (output === "json") {
|
|
13316
|
+
return {
|
|
13317
|
+
code: errors3.length > 0 ? 2 : 0,
|
|
13318
|
+
data: {
|
|
13319
|
+
success: errors3.length === 0,
|
|
13320
|
+
command: "install",
|
|
13321
|
+
subcommand: "lsp",
|
|
13322
|
+
targets,
|
|
13323
|
+
languages: [...languages],
|
|
13324
|
+
global: isGlobal,
|
|
13325
|
+
dryRun,
|
|
13326
|
+
lspResults: installReport.results,
|
|
13327
|
+
configResults: allConfigResults
|
|
13328
|
+
}
|
|
13329
|
+
};
|
|
13330
|
+
}
|
|
13331
|
+
console.log(`
|
|
13332
|
+
LSP Installation:`);
|
|
13333
|
+
for (const result of installReport.results) {
|
|
13334
|
+
const icon = result.status === "already-installed" ? "✓" : result.status === "installed" ? "+" : "✗";
|
|
13335
|
+
const version = result.version ? ` (${result.version})` : "";
|
|
13336
|
+
const error = result.error ? `: ${result.error}` : "";
|
|
13337
|
+
console.log(` ${icon} ${result.lspId}${version}${error}`);
|
|
13338
|
+
}
|
|
13339
|
+
console.log(`
|
|
13340
|
+
Config Files:`);
|
|
13341
|
+
for (const result of allConfigResults) {
|
|
13342
|
+
const icon = result.success ? "✓" : "✗";
|
|
13343
|
+
const error = result.error ? `: ${result.error}` : "";
|
|
13344
|
+
console.log(` ${icon} ${result.target}: ${result.path}${error}`);
|
|
13345
|
+
}
|
|
13346
|
+
const note = dryRun ? " (dry-run)" : "";
|
|
13347
|
+
console.log(`
|
|
13348
|
+
${installReport.results.length} LSPs processed, ${allConfigResults.length} config files${note}`);
|
|
13349
|
+
return {
|
|
13350
|
+
code: errors3.length > 0 ? 2 : 0,
|
|
13351
|
+
data: {
|
|
13352
|
+
success: errors3.length === 0,
|
|
13353
|
+
command: "install",
|
|
13354
|
+
subcommand: "lsp",
|
|
13355
|
+
targets,
|
|
13356
|
+
languages: [...languages],
|
|
13357
|
+
global: isGlobal,
|
|
13358
|
+
dryRun,
|
|
13359
|
+
lspResults: installReport.results,
|
|
13360
|
+
configResults: allConfigResults
|
|
13361
|
+
}
|
|
13362
|
+
};
|
|
13363
|
+
} catch (error) {
|
|
13364
|
+
return {
|
|
13365
|
+
code: 1,
|
|
13366
|
+
data: {
|
|
13367
|
+
success: false,
|
|
13368
|
+
command: "install",
|
|
13369
|
+
subcommand: "lsp",
|
|
13370
|
+
errors: [String(error)]
|
|
13371
|
+
}
|
|
13372
|
+
};
|
|
13373
|
+
}
|
|
13374
|
+
}
|
|
13375
|
+
function getLspConfigGenerator(target) {
|
|
13376
|
+
switch (target) {
|
|
13377
|
+
case "opencode":
|
|
13378
|
+
return createOpenCodeLspGenerator();
|
|
13379
|
+
case "claude":
|
|
13380
|
+
return createClaudeLspGenerator();
|
|
13381
|
+
case "codex":
|
|
13382
|
+
return createCodexLspGenerator();
|
|
13383
|
+
case "gemini":
|
|
13384
|
+
return createGeminiLspGenerator();
|
|
13385
|
+
case "cursor":
|
|
13386
|
+
return createCursorLspGenerator();
|
|
13387
|
+
case "agy":
|
|
13388
|
+
return createAgyLspGenerator();
|
|
13389
|
+
default:
|
|
13390
|
+
return;
|
|
13391
|
+
}
|
|
13392
|
+
}
|
|
13393
|
+
|
|
13394
|
+
// src/commands/seo-audit.command.ts
|
|
13395
|
+
import { writeFile as writeFile5, mkdir as mkdir6 } from "node:fs/promises";
|
|
13396
|
+
import { dirname as dirname4, resolve as resolve9 } from "node:path";
|
|
13397
|
+
|
|
13398
|
+
// src/infrastructure/http/safe-fetch.ts
|
|
13399
|
+
import { lookup } from "node:dns/promises";
|
|
13400
|
+
var PRIVATE_IP_RANGES = [
|
|
13401
|
+
/^10\./,
|
|
13402
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
13403
|
+
/^192\.168\./,
|
|
13404
|
+
/^127\./,
|
|
13405
|
+
/^0\./,
|
|
13406
|
+
/^169\.254\./,
|
|
13407
|
+
/^::1$/,
|
|
13408
|
+
/^fc00:/i,
|
|
13409
|
+
/^fe80:/i
|
|
13410
|
+
];
|
|
13411
|
+
function isPrivateIp(ip) {
|
|
13412
|
+
return PRIVATE_IP_RANGES.some((range) => range.test(ip));
|
|
13413
|
+
}
|
|
13414
|
+
async function validateUrl(urlString) {
|
|
13415
|
+
let parsed;
|
|
13416
|
+
try {
|
|
13417
|
+
parsed = new URL(urlString);
|
|
13418
|
+
} catch {
|
|
13419
|
+
throw new Error(`Invalid URL: ${urlString}`);
|
|
13420
|
+
}
|
|
13421
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
13422
|
+
throw new Error(`Blocked scheme: ${parsed.protocol}. Only http and https are allowed.`);
|
|
13423
|
+
}
|
|
13424
|
+
const hostname = parsed.hostname;
|
|
13425
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
13426
|
+
throw new Error(`Blocked hostname: ${hostname}`);
|
|
13427
|
+
}
|
|
13428
|
+
try {
|
|
13429
|
+
const addresses = await lookup(hostname, { all: true });
|
|
13430
|
+
const addrList = Array.isArray(addresses) ? addresses : [addresses];
|
|
13431
|
+
for (const addr of addrList) {
|
|
13432
|
+
if (isPrivateIp(addr.address)) {
|
|
13433
|
+
throw new Error(`SSRF blocked: ${hostname} resolves to private IP ${addr.address}`);
|
|
13434
|
+
}
|
|
13435
|
+
}
|
|
13436
|
+
} catch (error) {
|
|
13437
|
+
if (error instanceof Error && error.message.startsWith("SSRF blocked")) {
|
|
13438
|
+
throw error;
|
|
13439
|
+
}
|
|
13440
|
+
throw new Error(`DNS resolution failed for ${hostname}: ${String(error)}`);
|
|
13441
|
+
}
|
|
13442
|
+
}
|
|
13443
|
+
async function safeFetch(urlString, options = {}) {
|
|
13444
|
+
const { timeout = 1e4, followRedirects = false } = options;
|
|
13445
|
+
await validateUrl(urlString);
|
|
13446
|
+
const controller = new AbortController;
|
|
13447
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
13448
|
+
const start = Date.now();
|
|
13449
|
+
try {
|
|
13450
|
+
const response = await fetch(urlString, {
|
|
13451
|
+
method: "GET",
|
|
13452
|
+
signal: controller.signal,
|
|
13453
|
+
redirect: followRedirects ? "follow" : "manual",
|
|
13454
|
+
headers: {
|
|
13455
|
+
"User-Agent": "CodeConductor-SEO/0.3.0",
|
|
13456
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
13457
|
+
}
|
|
13458
|
+
});
|
|
13459
|
+
const responseTime = Date.now() - start;
|
|
13460
|
+
const body = await response.text();
|
|
13461
|
+
const headers = {};
|
|
13462
|
+
response.headers.forEach((value, key) => {
|
|
13463
|
+
headers[key] = value;
|
|
13464
|
+
});
|
|
13465
|
+
return {
|
|
13466
|
+
status: response.status,
|
|
13467
|
+
headers,
|
|
13468
|
+
body,
|
|
13469
|
+
responseTime,
|
|
13470
|
+
url: urlString
|
|
13471
|
+
};
|
|
13472
|
+
} catch (error) {
|
|
13473
|
+
const responseTime = Date.now() - start;
|
|
13474
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
13475
|
+
throw new Error(`Request timed out after ${timeout}ms: ${urlString}`);
|
|
13476
|
+
}
|
|
13477
|
+
throw new Error(`Fetch failed (${responseTime}ms): ${urlString} — ${String(error)}`);
|
|
13478
|
+
} finally {
|
|
13479
|
+
clearTimeout(timer);
|
|
13480
|
+
}
|
|
13481
|
+
}
|
|
13482
|
+
async function delay(ms) {
|
|
13483
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
13484
|
+
}
|
|
13485
|
+
|
|
13486
|
+
// src/infrastructure/parsers/sitemap-parser.ts
|
|
13487
|
+
function extractTag(xml, tag) {
|
|
13488
|
+
const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
|
|
13489
|
+
return match ? match[1].trim() : undefined;
|
|
13490
|
+
}
|
|
13491
|
+
function extractAllBlocks(xml, tag) {
|
|
13492
|
+
const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "gi");
|
|
13493
|
+
const blocks = [];
|
|
13494
|
+
let match;
|
|
13495
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
13496
|
+
blocks.push(match[1]);
|
|
13497
|
+
}
|
|
13498
|
+
return blocks;
|
|
13499
|
+
}
|
|
13500
|
+
function parseUrlEntries(xml) {
|
|
13501
|
+
const urlBlocks = extractAllBlocks(xml, "url");
|
|
13502
|
+
return urlBlocks.map((block) => ({
|
|
13503
|
+
url: extractTag(block, "loc") ?? "",
|
|
13504
|
+
lastmod: extractTag(block, "lastmod"),
|
|
13505
|
+
changefreq: extractTag(block, "changefreq"),
|
|
13506
|
+
priority: extractTag(block, "priority")
|
|
13507
|
+
})).filter((entry) => entry.url.length > 0);
|
|
13508
|
+
}
|
|
13509
|
+
function parseSitemapLocs(xml) {
|
|
13510
|
+
const sitemapBlocks = extractAllBlocks(xml, "sitemap");
|
|
13511
|
+
return sitemapBlocks.map((block) => extractTag(block, "loc")).filter((loc) => loc !== undefined && loc.length > 0);
|
|
13512
|
+
}
|
|
13513
|
+
function getDomain(url) {
|
|
13514
|
+
try {
|
|
13515
|
+
return new URL(url).hostname;
|
|
13516
|
+
} catch {
|
|
13517
|
+
return "";
|
|
13518
|
+
}
|
|
13519
|
+
}
|
|
13520
|
+
function deduplicateEntries(entries) {
|
|
13521
|
+
const seen = new Set;
|
|
13522
|
+
return entries.filter((entry) => {
|
|
13523
|
+
if (seen.has(entry.url))
|
|
13524
|
+
return false;
|
|
13525
|
+
seen.add(entry.url);
|
|
13526
|
+
return true;
|
|
13527
|
+
});
|
|
13528
|
+
}
|
|
13529
|
+
function filterSameDomain(entries, domain) {
|
|
13530
|
+
return entries.filter((entry) => {
|
|
13531
|
+
try {
|
|
13532
|
+
return new URL(entry.url).hostname === domain;
|
|
13533
|
+
} catch {
|
|
13534
|
+
return false;
|
|
13535
|
+
}
|
|
13536
|
+
});
|
|
13537
|
+
}
|
|
13538
|
+
async function parseSitemap(sitemapUrl, options = {}) {
|
|
13539
|
+
const { maxDepth = 2, delay: requestDelay = 0 } = options;
|
|
13540
|
+
return parseSitemapRecursive(sitemapUrl, 0, maxDepth, requestDelay);
|
|
13541
|
+
}
|
|
13542
|
+
async function parseSitemapRecursive(sitemapUrl, depth, maxDepth, requestDelay) {
|
|
13543
|
+
const response = await safeFetch(sitemapUrl);
|
|
13544
|
+
const xml = response.body;
|
|
13545
|
+
const domain = getDomain(sitemapUrl);
|
|
13546
|
+
const isIndex = /<sitemapindex[\s>]/i.test(xml);
|
|
13547
|
+
if (isIndex) {
|
|
13548
|
+
const childUrls = parseSitemapLocs(xml);
|
|
13549
|
+
if (depth >= maxDepth) {
|
|
13550
|
+
return {
|
|
13551
|
+
entries: [],
|
|
13552
|
+
type: "sitemapindex",
|
|
13553
|
+
childSitemaps: childUrls
|
|
13554
|
+
};
|
|
13555
|
+
}
|
|
13556
|
+
const allEntries = [];
|
|
13557
|
+
const allChildSitemaps = [...childUrls];
|
|
13558
|
+
for (let i = 0;i < childUrls.length; i++) {
|
|
13559
|
+
if (i > 0 && requestDelay > 0) {
|
|
13560
|
+
await delay(requestDelay);
|
|
13561
|
+
}
|
|
13562
|
+
try {
|
|
13563
|
+
const childResult = await parseSitemapRecursive(childUrls[i], depth + 1, maxDepth, requestDelay);
|
|
13564
|
+
allEntries.push(...childResult.entries);
|
|
13565
|
+
allChildSitemaps.push(...childResult.childSitemaps);
|
|
13566
|
+
} catch {}
|
|
13567
|
+
}
|
|
13568
|
+
const deduped2 = deduplicateEntries(allEntries);
|
|
13569
|
+
const filtered2 = domain ? filterSameDomain(deduped2, domain) : deduped2;
|
|
13570
|
+
return {
|
|
13571
|
+
entries: filtered2,
|
|
13572
|
+
type: "sitemapindex",
|
|
13573
|
+
childSitemaps: allChildSitemaps
|
|
13574
|
+
};
|
|
13575
|
+
}
|
|
13576
|
+
const entries = parseUrlEntries(xml);
|
|
13577
|
+
const deduped = deduplicateEntries(entries);
|
|
13578
|
+
const filtered = domain ? filterSameDomain(deduped, domain) : deduped;
|
|
13579
|
+
return {
|
|
13580
|
+
entries: filtered,
|
|
13581
|
+
type: "urlset",
|
|
13582
|
+
childSitemaps: []
|
|
13583
|
+
};
|
|
13584
|
+
}
|
|
13585
|
+
|
|
13586
|
+
// src/domain/seo/meta-validator.ts
|
|
13587
|
+
function extractMetaContent(html, name) {
|
|
13588
|
+
const patterns = [
|
|
13589
|
+
new RegExp(`<meta[^>]*name=["']${name}["'][^>]*content=["']([^"']*)["']`, "i"),
|
|
13590
|
+
new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*name=["']${name}["']`, "i")
|
|
13591
|
+
];
|
|
13592
|
+
for (const pattern of patterns) {
|
|
13593
|
+
const match = html.match(pattern);
|
|
13594
|
+
if (match)
|
|
13595
|
+
return match[1].trim();
|
|
13596
|
+
}
|
|
13597
|
+
return;
|
|
13598
|
+
}
|
|
13599
|
+
function extractPropertyContent(html, property) {
|
|
13600
|
+
const patterns = [
|
|
13601
|
+
new RegExp(`<meta[^>]*property=["']${property}["'][^>]*content=["']([^"']*)["']`, "i"),
|
|
13602
|
+
new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*property=["']${property}["']`, "i")
|
|
13603
|
+
];
|
|
13604
|
+
for (const pattern of patterns) {
|
|
13605
|
+
const match = html.match(pattern);
|
|
13606
|
+
if (match)
|
|
13607
|
+
return match[1].trim();
|
|
13608
|
+
}
|
|
13609
|
+
return;
|
|
13610
|
+
}
|
|
13611
|
+
function extractTitle(html) {
|
|
13612
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
13613
|
+
return match ? match[1].trim() : undefined;
|
|
13614
|
+
}
|
|
13615
|
+
function extractCanonical(html) {
|
|
13616
|
+
const match = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']*)["']/i);
|
|
13617
|
+
return match ? match[1].trim() : undefined;
|
|
13618
|
+
}
|
|
13619
|
+
function extractH1Tags(html) {
|
|
13620
|
+
const regex = /<h1[^>]*>([\s\S]*?)<\/h1>/gi;
|
|
13621
|
+
const results = [];
|
|
13622
|
+
let match;
|
|
13623
|
+
while ((match = regex.exec(html)) !== null) {
|
|
13624
|
+
results.push(match[1].trim());
|
|
13625
|
+
}
|
|
13626
|
+
return results;
|
|
13627
|
+
}
|
|
13628
|
+
function extractHeadingHierarchy(html) {
|
|
13629
|
+
const regex = /<(h[1-6])[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
13630
|
+
const results = [];
|
|
13631
|
+
let match;
|
|
13632
|
+
while ((match = regex.exec(html)) !== null) {
|
|
13633
|
+
results.push({ tag: match[1].toLowerCase(), text: match[2].trim() });
|
|
13634
|
+
}
|
|
13635
|
+
return results;
|
|
13636
|
+
}
|
|
13637
|
+
function extractImgWithoutAlt(html) {
|
|
13638
|
+
const imgRegex = /<img[^>]*>/gi;
|
|
13639
|
+
let count = 0;
|
|
13640
|
+
let match;
|
|
13641
|
+
while ((match = imgRegex.exec(html)) !== null) {
|
|
13642
|
+
if (!/alt=["'][^"']+["']/i.test(match[0])) {
|
|
13643
|
+
count++;
|
|
13644
|
+
}
|
|
13645
|
+
}
|
|
13646
|
+
return count;
|
|
13647
|
+
}
|
|
13648
|
+
function extractHtmlLang(html) {
|
|
13649
|
+
const match = html.match(/<html[^>]*lang=["']([^"']*)["']/i);
|
|
13650
|
+
return match ? match[1].trim() : undefined;
|
|
13651
|
+
}
|
|
13652
|
+
function extractViewport(html) {
|
|
13653
|
+
return extractMetaContent(html, "viewport");
|
|
13654
|
+
}
|
|
13655
|
+
function extractInternalLinks(html, baseUrl) {
|
|
13656
|
+
let domain;
|
|
13657
|
+
try {
|
|
13658
|
+
domain = new URL(baseUrl).hostname;
|
|
13659
|
+
} catch {
|
|
13660
|
+
return 0;
|
|
13661
|
+
}
|
|
13662
|
+
const linkRegex = /href=["']([^"']*)["']/gi;
|
|
13663
|
+
let count = 0;
|
|
13664
|
+
let match;
|
|
13665
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
13666
|
+
const href = match[1];
|
|
13667
|
+
if (href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:"))
|
|
13668
|
+
continue;
|
|
13669
|
+
try {
|
|
13670
|
+
const resolved = new URL(href, baseUrl);
|
|
13671
|
+
if (resolved.hostname === domain)
|
|
13672
|
+
count++;
|
|
13673
|
+
} catch {
|
|
13674
|
+
if (href.startsWith("/"))
|
|
13675
|
+
count++;
|
|
13676
|
+
}
|
|
13677
|
+
}
|
|
13678
|
+
return count;
|
|
13679
|
+
}
|
|
13680
|
+
function validateMeta(html, url, responseTime) {
|
|
13681
|
+
const checks = [];
|
|
13682
|
+
const title = extractTitle(html);
|
|
13683
|
+
if (!title) {
|
|
13684
|
+
checks.push({
|
|
13685
|
+
name: "title-tag",
|
|
13686
|
+
category: "meta",
|
|
13687
|
+
severity: "error",
|
|
13688
|
+
message: "Missing <title> tag",
|
|
13689
|
+
remediation: "Add a <title> tag with 30-60 characters describing the page content."
|
|
13690
|
+
});
|
|
13691
|
+
} else if (title.length < 30) {
|
|
13692
|
+
checks.push({
|
|
13693
|
+
name: "title-tag",
|
|
13694
|
+
category: "meta",
|
|
13695
|
+
severity: "warning",
|
|
13696
|
+
message: `Title too short (${title.length} chars): "${title}"`,
|
|
13697
|
+
remediation: "Expand title to 30-60 characters."
|
|
13698
|
+
});
|
|
13699
|
+
} else if (title.length > 60) {
|
|
13700
|
+
checks.push({
|
|
13701
|
+
name: "title-tag",
|
|
13702
|
+
category: "meta",
|
|
13703
|
+
severity: "warning",
|
|
13704
|
+
message: `Title too long (${title.length} chars): "${title}"`,
|
|
13705
|
+
remediation: "Shorten title to 30-60 characters to avoid truncation in SERPs."
|
|
13706
|
+
});
|
|
13707
|
+
} else {
|
|
13708
|
+
checks.push({
|
|
13709
|
+
name: "title-tag",
|
|
13710
|
+
category: "meta",
|
|
13711
|
+
severity: "pass",
|
|
13712
|
+
message: `Title OK (${title.length} chars): "${title}"`
|
|
13713
|
+
});
|
|
13714
|
+
}
|
|
13715
|
+
const description = extractMetaContent(html, "description");
|
|
13716
|
+
if (!description) {
|
|
13717
|
+
checks.push({
|
|
13718
|
+
name: "meta-description",
|
|
13719
|
+
category: "meta",
|
|
13720
|
+
severity: "error",
|
|
13721
|
+
message: 'Missing <meta name="description">',
|
|
13722
|
+
remediation: "Add a meta description with 120-160 characters summarizing the page."
|
|
13723
|
+
});
|
|
13724
|
+
} else if (description.length < 120) {
|
|
13725
|
+
checks.push({
|
|
13726
|
+
name: "meta-description",
|
|
13727
|
+
category: "meta",
|
|
13728
|
+
severity: "warning",
|
|
13729
|
+
message: `Description too short (${description.length} chars)`,
|
|
13730
|
+
remediation: "Expand description to 120-160 characters."
|
|
13731
|
+
});
|
|
13732
|
+
} else if (description.length > 160) {
|
|
13733
|
+
checks.push({
|
|
13734
|
+
name: "meta-description",
|
|
13735
|
+
category: "meta",
|
|
13736
|
+
severity: "warning",
|
|
13737
|
+
message: `Description too long (${description.length} chars)`,
|
|
13738
|
+
remediation: "Shorten description to 120-160 characters."
|
|
13739
|
+
});
|
|
13740
|
+
} else {
|
|
13741
|
+
checks.push({
|
|
13742
|
+
name: "meta-description",
|
|
13743
|
+
category: "meta",
|
|
13744
|
+
severity: "pass",
|
|
13745
|
+
message: `Description OK (${description.length} chars)`
|
|
13746
|
+
});
|
|
13747
|
+
}
|
|
13748
|
+
const canonical = extractCanonical(html);
|
|
13749
|
+
if (!canonical) {
|
|
13750
|
+
checks.push({
|
|
13751
|
+
name: "canonical",
|
|
13752
|
+
category: "meta",
|
|
13753
|
+
severity: "warning",
|
|
13754
|
+
message: 'Missing <link rel="canonical">',
|
|
13755
|
+
remediation: "Add a canonical URL to prevent duplicate content issues."
|
|
13756
|
+
});
|
|
13757
|
+
} else if (canonical !== url) {
|
|
13758
|
+
checks.push({
|
|
13759
|
+
name: "canonical",
|
|
13760
|
+
category: "meta",
|
|
13761
|
+
severity: "info",
|
|
13762
|
+
message: `Canonical (${canonical}) differs from current URL (${url})`
|
|
13763
|
+
});
|
|
13764
|
+
} else {
|
|
13765
|
+
checks.push({
|
|
13766
|
+
name: "canonical",
|
|
13767
|
+
category: "meta",
|
|
13768
|
+
severity: "pass",
|
|
13769
|
+
message: `Canonical matches URL`
|
|
13770
|
+
});
|
|
13771
|
+
}
|
|
13772
|
+
const robots = extractMetaContent(html, "robots");
|
|
13773
|
+
if (robots) {
|
|
13774
|
+
if (/noindex/i.test(robots)) {
|
|
13775
|
+
checks.push({
|
|
13776
|
+
name: "robots-noindex",
|
|
13777
|
+
category: "crawl",
|
|
13778
|
+
severity: "warning",
|
|
13779
|
+
message: `Page has noindex directive: "${robots}"`,
|
|
13780
|
+
remediation: "Remove noindex if this page should appear in search results."
|
|
13781
|
+
});
|
|
13782
|
+
}
|
|
13783
|
+
if (/nofollow/i.test(robots)) {
|
|
13784
|
+
checks.push({
|
|
13785
|
+
name: "robots-nofollow",
|
|
13786
|
+
category: "crawl",
|
|
13787
|
+
severity: "warning",
|
|
13788
|
+
message: `Page has nofollow directive: "${robots}"`
|
|
13789
|
+
});
|
|
13790
|
+
}
|
|
13791
|
+
} else {
|
|
13792
|
+
checks.push({
|
|
13793
|
+
name: "robots-directive",
|
|
13794
|
+
category: "crawl",
|
|
13795
|
+
severity: "pass",
|
|
13796
|
+
message: "No restrictive robots directive"
|
|
13797
|
+
});
|
|
13798
|
+
}
|
|
13799
|
+
const hreflangRegex = /hreflang=["']([^"']*)["']/gi;
|
|
13800
|
+
const hreflangs = [];
|
|
13801
|
+
let hreflangMatch;
|
|
13802
|
+
while ((hreflangMatch = hreflangRegex.exec(html)) !== null) {
|
|
13803
|
+
hreflangs.push(hreflangMatch[1]);
|
|
13804
|
+
}
|
|
13805
|
+
if (hreflangs.length > 0) {
|
|
13806
|
+
checks.push({
|
|
13807
|
+
name: "hreflang",
|
|
13808
|
+
category: "meta",
|
|
13809
|
+
severity: "pass",
|
|
13810
|
+
message: `Found ${hreflangs.length} hreflang tags: ${hreflangs.join(", ")}`
|
|
13811
|
+
});
|
|
13812
|
+
}
|
|
13813
|
+
const ogTitle = extractPropertyContent(html, "og:title");
|
|
13814
|
+
const ogDescription = extractPropertyContent(html, "og:description");
|
|
13815
|
+
const ogImage = extractPropertyContent(html, "og:image");
|
|
13816
|
+
const ogUrl = extractPropertyContent(html, "og:url");
|
|
13817
|
+
if (!ogTitle) {
|
|
13818
|
+
checks.push({
|
|
13819
|
+
name: "og-title",
|
|
13820
|
+
category: "social",
|
|
13821
|
+
severity: "warning",
|
|
13822
|
+
message: "Missing og:title",
|
|
13823
|
+
remediation: 'Add <meta property="og:title" content="..."> for social sharing.'
|
|
13824
|
+
});
|
|
13825
|
+
} else {
|
|
13826
|
+
checks.push({ name: "og-title", category: "social", severity: "pass", message: "og:title present" });
|
|
13827
|
+
}
|
|
13828
|
+
if (!ogDescription) {
|
|
13829
|
+
checks.push({
|
|
13830
|
+
name: "og-description",
|
|
13831
|
+
category: "social",
|
|
13832
|
+
severity: "warning",
|
|
13833
|
+
message: "Missing og:description",
|
|
13834
|
+
remediation: 'Add <meta property="og:description" content="...">.'
|
|
13835
|
+
});
|
|
13836
|
+
} else {
|
|
13837
|
+
checks.push({ name: "og-description", category: "social", severity: "pass", message: "og:description present" });
|
|
13838
|
+
}
|
|
13839
|
+
if (!ogImage) {
|
|
13840
|
+
checks.push({
|
|
13841
|
+
name: "og-image",
|
|
13842
|
+
category: "social",
|
|
13843
|
+
severity: "warning",
|
|
13844
|
+
message: "Missing og:image",
|
|
13845
|
+
remediation: 'Add <meta property="og:image" content="..."> with a 1200x630px image.'
|
|
13846
|
+
});
|
|
13847
|
+
} else {
|
|
13848
|
+
checks.push({ name: "og-image", category: "social", severity: "pass", message: "og:image present" });
|
|
13849
|
+
}
|
|
13850
|
+
if (!ogUrl) {
|
|
13851
|
+
checks.push({
|
|
13852
|
+
name: "og-url",
|
|
13853
|
+
category: "social",
|
|
13854
|
+
severity: "info",
|
|
13855
|
+
message: "Missing og:url"
|
|
13856
|
+
});
|
|
13857
|
+
}
|
|
13858
|
+
const twitterCard = extractMetaContent(html, "twitter:card");
|
|
13859
|
+
if (!twitterCard) {
|
|
13860
|
+
checks.push({
|
|
13861
|
+
name: "twitter-card",
|
|
13862
|
+
category: "social",
|
|
13863
|
+
severity: "info",
|
|
13864
|
+
message: "Missing twitter:card",
|
|
13865
|
+
remediation: 'Add <meta name="twitter:card" content="summary_large_image">.'
|
|
13866
|
+
});
|
|
13867
|
+
} else {
|
|
13868
|
+
checks.push({ name: "twitter-card", category: "social", severity: "pass", message: `twitter:card: ${twitterCard}` });
|
|
13869
|
+
}
|
|
13870
|
+
const h1Tags = extractH1Tags(html);
|
|
13871
|
+
if (h1Tags.length === 0) {
|
|
13872
|
+
checks.push({
|
|
13873
|
+
name: "h1-tag",
|
|
13874
|
+
category: "content",
|
|
13875
|
+
severity: "error",
|
|
13876
|
+
message: "Missing <h1> tag",
|
|
13877
|
+
remediation: "Add exactly one <h1> tag with the main page heading."
|
|
13878
|
+
});
|
|
13879
|
+
} else if (h1Tags.length > 1) {
|
|
13880
|
+
checks.push({
|
|
13881
|
+
name: "h1-tag",
|
|
13882
|
+
category: "content",
|
|
13883
|
+
severity: "warning",
|
|
13884
|
+
message: `Multiple <h1> tags found (${h1Tags.length}): "${h1Tags.join('", "')}"`,
|
|
13885
|
+
remediation: "Use only one <h1> per page. Convert extras to <h2>."
|
|
13886
|
+
});
|
|
13887
|
+
} else {
|
|
13888
|
+
checks.push({
|
|
13889
|
+
name: "h1-tag",
|
|
13890
|
+
category: "content",
|
|
13891
|
+
severity: "pass",
|
|
13892
|
+
message: `H1 OK: "${h1Tags[0]}"`
|
|
13893
|
+
});
|
|
13894
|
+
}
|
|
13895
|
+
const headings = extractHeadingHierarchy(html);
|
|
13896
|
+
if (headings.length > 1) {
|
|
13897
|
+
let hasSkips = false;
|
|
13898
|
+
for (let i = 1;i < headings.length; i++) {
|
|
13899
|
+
const prev = parseInt(headings[i - 1].tag[1]);
|
|
13900
|
+
const curr = parseInt(headings[i].tag[1]);
|
|
13901
|
+
if (curr > prev + 1) {
|
|
13902
|
+
hasSkips = true;
|
|
13903
|
+
break;
|
|
13904
|
+
}
|
|
13905
|
+
}
|
|
13906
|
+
if (hasSkips) {
|
|
13907
|
+
checks.push({
|
|
13908
|
+
name: "heading-hierarchy",
|
|
13909
|
+
category: "content",
|
|
13910
|
+
severity: "warning",
|
|
13911
|
+
message: "Heading hierarchy has skipped levels (e.g., h2 → h4)",
|
|
13912
|
+
remediation: "Ensure headings follow sequential order: h1 → h2 → h3."
|
|
13913
|
+
});
|
|
13914
|
+
} else {
|
|
13915
|
+
checks.push({
|
|
13916
|
+
name: "heading-hierarchy",
|
|
13917
|
+
category: "content",
|
|
13918
|
+
severity: "pass",
|
|
13919
|
+
message: `Heading hierarchy OK (${headings.length} headings)`
|
|
13920
|
+
});
|
|
13921
|
+
}
|
|
13922
|
+
}
|
|
13923
|
+
const imgsWithoutAlt = extractImgWithoutAlt(html);
|
|
13924
|
+
if (imgsWithoutAlt > 0) {
|
|
13925
|
+
checks.push({
|
|
13926
|
+
name: "img-alt-text",
|
|
13927
|
+
category: "content",
|
|
13928
|
+
severity: "warning",
|
|
13929
|
+
message: `${imgsWithoutAlt} image(s) missing alt text`,
|
|
13930
|
+
remediation: "Add descriptive alt text to all images for accessibility and SEO."
|
|
13931
|
+
});
|
|
13932
|
+
} else {
|
|
13933
|
+
checks.push({
|
|
13934
|
+
name: "img-alt-text",
|
|
13935
|
+
category: "content",
|
|
13936
|
+
severity: "pass",
|
|
13937
|
+
message: "All images have alt text"
|
|
13938
|
+
});
|
|
13939
|
+
}
|
|
13940
|
+
const internalLinks = extractInternalLinks(html, url);
|
|
13941
|
+
checks.push({
|
|
13942
|
+
name: "internal-links",
|
|
13943
|
+
category: "content",
|
|
13944
|
+
severity: internalLinks > 0 ? "pass" : "warning",
|
|
13945
|
+
message: `${internalLinks} internal links found`,
|
|
13946
|
+
remediation: internalLinks === 0 ? "Add internal links to improve crawlability and page authority." : undefined
|
|
13947
|
+
});
|
|
13948
|
+
const lang = extractHtmlLang(html);
|
|
13949
|
+
if (!lang) {
|
|
13950
|
+
checks.push({
|
|
13951
|
+
name: "html-lang",
|
|
13952
|
+
category: "technical",
|
|
13953
|
+
severity: "warning",
|
|
13954
|
+
message: 'Missing <html lang="..."> attribute',
|
|
13955
|
+
remediation: 'Add lang attribute to <html> tag (e.g., lang="en").'
|
|
13956
|
+
});
|
|
13957
|
+
} else {
|
|
13958
|
+
checks.push({
|
|
13959
|
+
name: "html-lang",
|
|
13960
|
+
category: "technical",
|
|
13961
|
+
severity: "pass",
|
|
13962
|
+
message: `HTML lang: ${lang}`
|
|
13963
|
+
});
|
|
13964
|
+
}
|
|
13965
|
+
const viewport = extractViewport(html);
|
|
13966
|
+
if (!viewport) {
|
|
13967
|
+
checks.push({
|
|
13968
|
+
name: "viewport",
|
|
13969
|
+
category: "technical",
|
|
13970
|
+
severity: "error",
|
|
13971
|
+
message: 'Missing <meta name="viewport">',
|
|
13972
|
+
remediation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.'
|
|
13973
|
+
});
|
|
13974
|
+
} else {
|
|
13975
|
+
checks.push({
|
|
13976
|
+
name: "viewport",
|
|
13977
|
+
category: "technical",
|
|
13978
|
+
severity: "pass",
|
|
13979
|
+
message: "Viewport meta tag present"
|
|
13980
|
+
});
|
|
13981
|
+
}
|
|
13982
|
+
const isHttps = url.startsWith("https://");
|
|
13983
|
+
checks.push({
|
|
13984
|
+
name: "https",
|
|
13985
|
+
category: "technical",
|
|
13986
|
+
severity: isHttps ? "pass" : "error",
|
|
13987
|
+
message: isHttps ? "HTTPS enabled" : "Site is not using HTTPS",
|
|
13988
|
+
remediation: isHttps ? undefined : "Migrate to HTTPS for security and SEO ranking."
|
|
13989
|
+
});
|
|
13990
|
+
if (responseTime < 3000) {
|
|
13991
|
+
checks.push({
|
|
13992
|
+
name: "response-time",
|
|
13993
|
+
category: "technical",
|
|
13994
|
+
severity: "pass",
|
|
13995
|
+
message: `Response time: ${responseTime}ms`
|
|
13996
|
+
});
|
|
13997
|
+
} else if (responseTime < 5000) {
|
|
13998
|
+
checks.push({
|
|
13999
|
+
name: "response-time",
|
|
14000
|
+
category: "technical",
|
|
14001
|
+
severity: "warning",
|
|
14002
|
+
message: `Slow response time: ${responseTime}ms`,
|
|
14003
|
+
remediation: "Optimize server response time to under 3 seconds."
|
|
14004
|
+
});
|
|
14005
|
+
} else {
|
|
14006
|
+
checks.push({
|
|
14007
|
+
name: "response-time",
|
|
14008
|
+
category: "technical",
|
|
14009
|
+
severity: "error",
|
|
14010
|
+
message: `Very slow response time: ${responseTime}ms`,
|
|
14011
|
+
remediation: "Server response time exceeds 5 seconds. Investigate server performance."
|
|
14012
|
+
});
|
|
14013
|
+
}
|
|
14014
|
+
return checks;
|
|
14015
|
+
}
|
|
14016
|
+
|
|
14017
|
+
// src/domain/seo/schema-validator.ts
|
|
14018
|
+
var HOTEL_TYPES = [
|
|
14019
|
+
"Hotel",
|
|
14020
|
+
"LodgingBusiness",
|
|
14021
|
+
"HotelRoom",
|
|
14022
|
+
"LocalBusiness",
|
|
14023
|
+
"Resort",
|
|
14024
|
+
"Motel",
|
|
14025
|
+
"Hostel",
|
|
14026
|
+
"BedAndBreakfast",
|
|
14027
|
+
"Campground",
|
|
14028
|
+
"RV Park"
|
|
14029
|
+
];
|
|
14030
|
+
var SUPPORTING_TYPES = [
|
|
14031
|
+
"BreadcrumbList",
|
|
14032
|
+
"FAQPage",
|
|
14033
|
+
"Review",
|
|
14034
|
+
"AggregateRating",
|
|
14035
|
+
"Organization",
|
|
14036
|
+
"WebSite",
|
|
14037
|
+
"ImageObject",
|
|
14038
|
+
"Event",
|
|
14039
|
+
"TouristAttraction",
|
|
14040
|
+
"HowTo",
|
|
14041
|
+
"WebPage",
|
|
14042
|
+
"Person"
|
|
14043
|
+
];
|
|
14044
|
+
var REQUIRED_HOTEL_PROPERTIES = ["name", "address", "telephone", "image"];
|
|
14045
|
+
var RECOMMENDED_HOTEL_PROPERTIES = ["priceRange", "description", "url", "geo"];
|
|
14046
|
+
function extractJsonLdBlocks(html) {
|
|
14047
|
+
const regex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14048
|
+
const blocks = [];
|
|
14049
|
+
let match;
|
|
14050
|
+
while ((match = regex.exec(html)) !== null) {
|
|
14051
|
+
try {
|
|
14052
|
+
const parsed = JSON.parse(match[1].trim());
|
|
14053
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
14054
|
+
for (const item of items) {
|
|
14055
|
+
if (item && typeof item === "object" && "@type" in item) {
|
|
14056
|
+
blocks.push({
|
|
14057
|
+
type: String(item["@type"]),
|
|
14058
|
+
properties: item,
|
|
14059
|
+
raw: match[1].trim()
|
|
14060
|
+
});
|
|
14061
|
+
}
|
|
14062
|
+
}
|
|
14063
|
+
} catch {}
|
|
14064
|
+
}
|
|
14065
|
+
return blocks;
|
|
14066
|
+
}
|
|
14067
|
+
function validateHotelSchema(block) {
|
|
14068
|
+
const errors3 = [];
|
|
14069
|
+
const warnings = [];
|
|
14070
|
+
for (const prop of REQUIRED_HOTEL_PROPERTIES) {
|
|
14071
|
+
if (!(prop in block.properties)) {
|
|
14072
|
+
errors3.push(`Missing required property: ${prop}`);
|
|
14073
|
+
}
|
|
14074
|
+
}
|
|
14075
|
+
for (const prop of RECOMMENDED_HOTEL_PROPERTIES) {
|
|
14076
|
+
if (!(prop in block.properties)) {
|
|
14077
|
+
warnings.push(`Missing recommended property: ${prop}`);
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
if ("address" in block.properties) {
|
|
14081
|
+
const address = block.properties.address;
|
|
14082
|
+
if (typeof address === "object" && address !== null) {
|
|
14083
|
+
const addrObj = address;
|
|
14084
|
+
if (!("streetAddress" in addrObj) && !("addressLocality" in addrObj)) {
|
|
14085
|
+
errors3.push("Address missing streetAddress or addressLocality");
|
|
14086
|
+
}
|
|
14087
|
+
}
|
|
14088
|
+
}
|
|
14089
|
+
if ("image" in block.properties) {
|
|
14090
|
+
const image = block.properties.image;
|
|
14091
|
+
if (typeof image === "string" && !image.startsWith("http")) {
|
|
14092
|
+
warnings.push("Image URL should be absolute");
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
return {
|
|
14096
|
+
valid: errors3.length === 0,
|
|
14097
|
+
type: block.type,
|
|
14098
|
+
errors: errors3,
|
|
14099
|
+
warnings
|
|
14100
|
+
};
|
|
14101
|
+
}
|
|
14102
|
+
function validateSupportingSchema(block) {
|
|
14103
|
+
const errors3 = [];
|
|
14104
|
+
const warnings = [];
|
|
14105
|
+
if (block.type === "BreadcrumbList") {
|
|
14106
|
+
if (!("itemListElement" in block.properties)) {
|
|
14107
|
+
errors3.push("BreadcrumbList missing itemListElement");
|
|
14108
|
+
}
|
|
14109
|
+
}
|
|
14110
|
+
if (block.type === "FAQPage") {
|
|
14111
|
+
if (!("mainEntity" in block.properties)) {
|
|
14112
|
+
errors3.push("FAQPage missing mainEntity");
|
|
14113
|
+
}
|
|
14114
|
+
}
|
|
14115
|
+
if (block.type === "AggregateRating") {
|
|
14116
|
+
if (!("ratingValue" in block.properties)) {
|
|
14117
|
+
errors3.push("AggregateRating missing ratingValue");
|
|
14118
|
+
}
|
|
14119
|
+
if (!("reviewCount" in block.properties) && !("ratingCount" in block.properties)) {
|
|
14120
|
+
warnings.push("AggregateRating missing reviewCount or ratingCount");
|
|
14121
|
+
}
|
|
14122
|
+
}
|
|
14123
|
+
if (block.type === "Organization") {
|
|
14124
|
+
if (!("name" in block.properties)) {
|
|
14125
|
+
errors3.push("Organization missing name");
|
|
14126
|
+
}
|
|
14127
|
+
if (!("url" in block.properties)) {
|
|
14128
|
+
warnings.push("Organization missing url");
|
|
14129
|
+
}
|
|
14130
|
+
}
|
|
14131
|
+
return {
|
|
14132
|
+
valid: errors3.length === 0,
|
|
14133
|
+
type: block.type,
|
|
14134
|
+
errors: errors3,
|
|
14135
|
+
warnings
|
|
14136
|
+
};
|
|
14137
|
+
}
|
|
14138
|
+
function validateSchema(html) {
|
|
14139
|
+
const checks = [];
|
|
14140
|
+
const blocks = extractJsonLdBlocks(html);
|
|
14141
|
+
if (blocks.length === 0) {
|
|
14142
|
+
checks.push({
|
|
14143
|
+
name: "json-ld-presence",
|
|
14144
|
+
category: "schema",
|
|
14145
|
+
severity: "error",
|
|
14146
|
+
message: "No JSON-LD structured data found",
|
|
14147
|
+
remediation: 'Add <script type="application/ld+json"> with Hotel or LodgingBusiness schema.'
|
|
14148
|
+
});
|
|
14149
|
+
return checks;
|
|
14150
|
+
}
|
|
14151
|
+
checks.push({
|
|
14152
|
+
name: "json-ld-presence",
|
|
14153
|
+
category: "schema",
|
|
14154
|
+
severity: "pass",
|
|
14155
|
+
message: `Found ${blocks.length} JSON-LD block(s)`
|
|
14156
|
+
});
|
|
14157
|
+
const hotelBlocks = blocks.filter((b) => HOTEL_TYPES.includes(b.type));
|
|
14158
|
+
const supportingBlocks = blocks.filter((b) => SUPPORTING_TYPES.includes(b.type));
|
|
14159
|
+
const unknownBlocks = blocks.filter((b) => !HOTEL_TYPES.includes(b.type) && !SUPPORTING_TYPES.includes(b.type));
|
|
14160
|
+
if (hotelBlocks.length === 0) {
|
|
14161
|
+
checks.push({
|
|
14162
|
+
name: "hotel-schema",
|
|
14163
|
+
category: "schema",
|
|
14164
|
+
severity: "error",
|
|
14165
|
+
message: `No Hotel/Hospitality schema found. Types found: ${blocks.map((b) => b.type).join(", ")}`,
|
|
14166
|
+
remediation: "Add a Hotel, LodgingBusiness, or Resort schema with required properties."
|
|
14167
|
+
});
|
|
14168
|
+
}
|
|
14169
|
+
for (const block of hotelBlocks) {
|
|
14170
|
+
const result = validateHotelSchema(block);
|
|
14171
|
+
if (result.valid) {
|
|
14172
|
+
checks.push({
|
|
14173
|
+
name: `schema-${block.type}`,
|
|
14174
|
+
category: "schema",
|
|
14175
|
+
severity: "pass",
|
|
14176
|
+
message: `${block.type} schema valid`
|
|
14177
|
+
});
|
|
14178
|
+
} else {
|
|
14179
|
+
checks.push({
|
|
14180
|
+
name: `schema-${block.type}`,
|
|
14181
|
+
category: "schema",
|
|
14182
|
+
severity: "error",
|
|
14183
|
+
message: `${block.type} schema invalid: ${result.errors.join("; ")}`,
|
|
14184
|
+
remediation: `Fix the following properties: ${result.errors.join(", ")}`
|
|
14185
|
+
});
|
|
14186
|
+
}
|
|
14187
|
+
if (result.warnings.length > 0) {
|
|
14188
|
+
checks.push({
|
|
14189
|
+
name: `schema-${block.type}-warnings`,
|
|
14190
|
+
category: "schema",
|
|
14191
|
+
severity: "warning",
|
|
14192
|
+
message: `${block.type} recommendations: ${result.warnings.join("; ")}`
|
|
14193
|
+
});
|
|
14194
|
+
}
|
|
14195
|
+
}
|
|
14196
|
+
for (const block of supportingBlocks) {
|
|
14197
|
+
const result = validateSupportingSchema(block);
|
|
14198
|
+
checks.push({
|
|
14199
|
+
name: `schema-${block.type}`,
|
|
14200
|
+
category: "schema",
|
|
14201
|
+
severity: result.valid ? "pass" : "warning",
|
|
14202
|
+
message: result.valid ? `${block.type} schema valid` : `${block.type} schema issues: ${result.errors.join("; ")}`
|
|
14203
|
+
});
|
|
14204
|
+
}
|
|
14205
|
+
for (const block of unknownBlocks) {
|
|
14206
|
+
checks.push({
|
|
14207
|
+
name: `schema-${block.type}`,
|
|
14208
|
+
category: "schema",
|
|
14209
|
+
severity: "info",
|
|
14210
|
+
message: `Unknown schema type: ${block.type}`
|
|
14211
|
+
});
|
|
14212
|
+
}
|
|
14213
|
+
return checks;
|
|
14214
|
+
}
|
|
14215
|
+
|
|
14216
|
+
// src/domain/seo/geo-validator.ts
|
|
14217
|
+
function hasFactualStatements(html) {
|
|
14218
|
+
const bodyText = html.replace(/<[^>]*>/g, " ");
|
|
14219
|
+
const numberPattern = /\d{2,}/g;
|
|
14220
|
+
const datePattern = /\b(20\d{2}|19\d{2})\b/g;
|
|
14221
|
+
const properNounPattern = /\b[A-Z][a-z]{2,}\b/g;
|
|
14222
|
+
const numbers = bodyText.match(numberPattern) ?? [];
|
|
14223
|
+
const dates = bodyText.match(datePattern) ?? [];
|
|
14224
|
+
const properNouns = bodyText.match(properNounPattern) ?? [];
|
|
14225
|
+
return numbers.length >= 3 || dates.length >= 1 || properNouns.length >= 5;
|
|
14226
|
+
}
|
|
14227
|
+
function countStructuredLists(html) {
|
|
14228
|
+
const ulMatches = html.match(/<ul[^>]*>/gi) ?? [];
|
|
14229
|
+
const olMatches = html.match(/<ol[^>]*>/gi) ?? [];
|
|
14230
|
+
const dlMatches = html.match(/<dl[^>]*>/gi) ?? [];
|
|
14231
|
+
return ulMatches.length + olMatches.length + dlMatches.length;
|
|
14232
|
+
}
|
|
14233
|
+
function hasFaqSection(html) {
|
|
14234
|
+
const patterns = [
|
|
14235
|
+
/faqpage/i,
|
|
14236
|
+
/<details[^>]*>/i,
|
|
14237
|
+
/id=["'][^"']*faq[^"']*["']/i,
|
|
14238
|
+
/class=["'][^"']*faq[^"']*["']/i,
|
|
14239
|
+
/<h[2-4][^>]*>.*(?:faq|frequently asked)/i
|
|
14240
|
+
];
|
|
14241
|
+
return patterns.some((p) => p.test(html));
|
|
14242
|
+
}
|
|
14243
|
+
function hasContentDates(html) {
|
|
14244
|
+
const timeRegex2 = /<time[^>]*datetime=["']([^"']*)["']/gi;
|
|
14245
|
+
let match;
|
|
14246
|
+
while ((match = timeRegex2.exec(html)) !== null) {
|
|
14247
|
+
return true;
|
|
14248
|
+
}
|
|
14249
|
+
const datePattern = /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}\b/i;
|
|
14250
|
+
return datePattern.test(html);
|
|
14251
|
+
}
|
|
14252
|
+
function validateGeo(html, url) {
|
|
14253
|
+
const checks = [];
|
|
14254
|
+
if (hasFactualStatements(html)) {
|
|
14255
|
+
checks.push({
|
|
14256
|
+
name: "citable-content",
|
|
14257
|
+
category: "geo",
|
|
14258
|
+
severity: "pass",
|
|
14259
|
+
message: "Page contains factual statements suitable for AI citation"
|
|
14260
|
+
});
|
|
14261
|
+
} else {
|
|
14262
|
+
checks.push({
|
|
14263
|
+
name: "citable-content",
|
|
14264
|
+
category: "geo",
|
|
14265
|
+
severity: "warning",
|
|
14266
|
+
message: "Page lacks factual statements (numbers, dates, proper nouns) for AI citation",
|
|
14267
|
+
remediation: "Add specific facts, numbers, and named entities that AI assistants can cite."
|
|
14268
|
+
});
|
|
14269
|
+
}
|
|
14270
|
+
const listCount = countStructuredLists(html);
|
|
14271
|
+
if (listCount > 0) {
|
|
14272
|
+
checks.push({
|
|
14273
|
+
name: "structured-lists",
|
|
14274
|
+
category: "geo",
|
|
14275
|
+
severity: "pass",
|
|
14276
|
+
message: `Found ${listCount} structured list(s) (ul/ol/dl)`
|
|
14277
|
+
});
|
|
14278
|
+
} else {
|
|
14279
|
+
checks.push({
|
|
14280
|
+
name: "structured-lists",
|
|
14281
|
+
category: "geo",
|
|
14282
|
+
severity: "warning",
|
|
14283
|
+
message: "No structured lists found",
|
|
14284
|
+
remediation: "Add <ul> or <ol> lists for amenities, features, and services. AI tools extract structured lists for citations."
|
|
14285
|
+
});
|
|
14286
|
+
}
|
|
14287
|
+
if (hasFaqSection(html)) {
|
|
14288
|
+
checks.push({
|
|
14289
|
+
name: "faq-section",
|
|
14290
|
+
category: "geo",
|
|
14291
|
+
severity: "pass",
|
|
14292
|
+
message: "FAQ section detected"
|
|
14293
|
+
});
|
|
14294
|
+
} else {
|
|
14295
|
+
checks.push({
|
|
14296
|
+
name: "faq-section",
|
|
14297
|
+
category: "geo",
|
|
14298
|
+
severity: "warning",
|
|
14299
|
+
message: "No FAQ section found",
|
|
14300
|
+
remediation: "Add an FAQ section with FAQPage schema. AI search tools prioritize Q&A formatted content."
|
|
14301
|
+
});
|
|
14302
|
+
}
|
|
14303
|
+
if (hasContentDates(html)) {
|
|
14304
|
+
checks.push({
|
|
14305
|
+
name: "content-freshness",
|
|
14306
|
+
category: "geo",
|
|
14307
|
+
severity: "pass",
|
|
14308
|
+
message: "Content has date signals"
|
|
14309
|
+
});
|
|
14310
|
+
} else {
|
|
14311
|
+
checks.push({
|
|
14312
|
+
name: "content-freshness",
|
|
14313
|
+
category: "geo",
|
|
14314
|
+
severity: "info",
|
|
14315
|
+
message: "No date signals found in content",
|
|
14316
|
+
remediation: "Add <time> elements or visible dates to signal content freshness."
|
|
14317
|
+
});
|
|
14318
|
+
}
|
|
14319
|
+
return checks;
|
|
14320
|
+
}
|
|
14321
|
+
async function checkLlmsTxt(baseUrl) {
|
|
14322
|
+
const checks = [];
|
|
14323
|
+
let root;
|
|
14324
|
+
try {
|
|
14325
|
+
const parsed = new URL(baseUrl);
|
|
14326
|
+
root = `${parsed.protocol}//${parsed.host}`;
|
|
14327
|
+
} catch {
|
|
14328
|
+
return [{
|
|
14329
|
+
name: "llms-txt",
|
|
14330
|
+
category: "geo",
|
|
14331
|
+
severity: "error",
|
|
14332
|
+
message: `Invalid base URL: ${baseUrl}`
|
|
14333
|
+
}];
|
|
14334
|
+
}
|
|
14335
|
+
try {
|
|
14336
|
+
const response = await safeFetch(`${root}/llms.txt`);
|
|
14337
|
+
if (response.status === 200 && response.body.length > 0) {
|
|
14338
|
+
checks.push({
|
|
14339
|
+
name: "llms-txt",
|
|
14340
|
+
category: "geo",
|
|
14341
|
+
severity: "pass",
|
|
14342
|
+
message: `llms.txt found (${response.body.length} bytes)`
|
|
14343
|
+
});
|
|
14344
|
+
if (!response.body.startsWith("#")) {
|
|
14345
|
+
checks.push({
|
|
14346
|
+
name: "llms-txt-format",
|
|
14347
|
+
category: "geo",
|
|
14348
|
+
severity: "warning",
|
|
14349
|
+
message: "llms.txt should start with a # heading",
|
|
14350
|
+
remediation: "Format: # Site Name\\n> Description\\n\\n## Pages\\n- [Title](url): description"
|
|
14351
|
+
});
|
|
14352
|
+
} else {
|
|
14353
|
+
checks.push({
|
|
14354
|
+
name: "llms-txt-format",
|
|
14355
|
+
category: "geo",
|
|
14356
|
+
severity: "pass",
|
|
14357
|
+
message: "llms.txt format looks correct"
|
|
14358
|
+
});
|
|
14359
|
+
}
|
|
14360
|
+
} else {
|
|
14361
|
+
checks.push({
|
|
14362
|
+
name: "llms-txt",
|
|
14363
|
+
category: "geo",
|
|
14364
|
+
severity: "error",
|
|
14365
|
+
message: `llms.txt returned status ${response.status}`,
|
|
14366
|
+
remediation: "Create a llms.txt file at the site root following the llms.txt specification."
|
|
14367
|
+
});
|
|
14368
|
+
}
|
|
14369
|
+
} catch {
|
|
14370
|
+
checks.push({
|
|
14371
|
+
name: "llms-txt",
|
|
14372
|
+
category: "geo",
|
|
14373
|
+
severity: "error",
|
|
14374
|
+
message: "llms.txt not found or unreachable",
|
|
14375
|
+
remediation: "Create a llms.txt file at the site root. Use `codeconductor seo llms` to generate one."
|
|
14376
|
+
});
|
|
14377
|
+
}
|
|
14378
|
+
try {
|
|
14379
|
+
const response = await safeFetch(`${root}/llms-full.txt`);
|
|
14380
|
+
if (response.status === 200 && response.body.length > 0) {
|
|
14381
|
+
checks.push({
|
|
14382
|
+
name: "llms-full-txt",
|
|
14383
|
+
category: "geo",
|
|
14384
|
+
severity: "pass",
|
|
14385
|
+
message: `llms-full.txt found (${response.body.length} bytes)`
|
|
14386
|
+
});
|
|
14387
|
+
}
|
|
14388
|
+
} catch {
|
|
14389
|
+
checks.push({
|
|
14390
|
+
name: "llms-full-txt",
|
|
14391
|
+
category: "geo",
|
|
14392
|
+
severity: "info",
|
|
14393
|
+
message: "llms-full.txt not found (optional)",
|
|
14394
|
+
remediation: "Consider creating llms-full.txt with extended content for AI tools."
|
|
14395
|
+
});
|
|
14396
|
+
}
|
|
14397
|
+
return checks;
|
|
14398
|
+
}
|
|
14399
|
+
|
|
14400
|
+
// src/domain/seo/seo-auditor.ts
|
|
14401
|
+
function computeSummary(pages) {
|
|
14402
|
+
let passed = 0;
|
|
14403
|
+
let warnings = 0;
|
|
14404
|
+
let errors3 = 0;
|
|
14405
|
+
let total = 0;
|
|
14406
|
+
for (const page of pages) {
|
|
14407
|
+
for (const check of page.checks) {
|
|
14408
|
+
total++;
|
|
14409
|
+
if (check.severity === "pass")
|
|
14410
|
+
passed++;
|
|
14411
|
+
else if (check.severity === "warning" || check.severity === "info")
|
|
14412
|
+
warnings++;
|
|
14413
|
+
else if (check.severity === "error")
|
|
14414
|
+
errors3++;
|
|
14415
|
+
}
|
|
14416
|
+
}
|
|
14417
|
+
const score = total > 0 ? Math.round(passed / total * 100) : 0;
|
|
14418
|
+
return { total, passed, warnings, errors: errors3, score };
|
|
14419
|
+
}
|
|
14420
|
+
async function auditSingleUrl(url, options = {}) {
|
|
14421
|
+
const response = await safeFetch(url, {
|
|
14422
|
+
followRedirects: options.followRedirects ?? false
|
|
14423
|
+
});
|
|
14424
|
+
const html = response.body;
|
|
14425
|
+
const checks = [];
|
|
14426
|
+
checks.push(...validateMeta(html, url, response.responseTime));
|
|
14427
|
+
checks.push(...validateSchema(html));
|
|
14428
|
+
checks.push(...validateGeo(html, url));
|
|
14429
|
+
return {
|
|
14430
|
+
url,
|
|
14431
|
+
checks,
|
|
14432
|
+
responseTime: response.responseTime
|
|
14433
|
+
};
|
|
14434
|
+
}
|
|
14435
|
+
async function auditSitemap(sitemapUrl, options = {}) {
|
|
14436
|
+
const { delay: requestDelay = 500, followRedirects = false, maxUrls, onProgress } = options;
|
|
14437
|
+
const sitemapResult = await parseSitemap(sitemapUrl, { delay: requestDelay });
|
|
14438
|
+
let entries = sitemapResult.entries;
|
|
14439
|
+
if (maxUrls && entries.length > maxUrls) {
|
|
14440
|
+
entries = entries.slice(0, maxUrls);
|
|
14441
|
+
}
|
|
14442
|
+
const pages = [];
|
|
14443
|
+
for (let i = 0;i < entries.length; i++) {
|
|
14444
|
+
const entry = entries[i];
|
|
14445
|
+
if (onProgress) {
|
|
14446
|
+
onProgress(i + 1, entries.length, entry.url);
|
|
14447
|
+
}
|
|
14448
|
+
if (i > 0 && requestDelay > 0) {
|
|
14449
|
+
await delay(requestDelay);
|
|
14450
|
+
}
|
|
14451
|
+
try {
|
|
14452
|
+
const result = await auditSingleUrl(entry.url, { followRedirects });
|
|
14453
|
+
pages.push(result);
|
|
14454
|
+
} catch (error) {
|
|
14455
|
+
pages.push({
|
|
14456
|
+
url: entry.url,
|
|
14457
|
+
checks: [{
|
|
14458
|
+
name: "fetch-error",
|
|
14459
|
+
category: "technical",
|
|
14460
|
+
severity: "error",
|
|
14461
|
+
message: `Failed to fetch: ${String(error)}`
|
|
14462
|
+
}],
|
|
14463
|
+
responseTime: 0
|
|
14464
|
+
});
|
|
14465
|
+
}
|
|
14466
|
+
}
|
|
14467
|
+
try {
|
|
14468
|
+
let siteRoot;
|
|
14469
|
+
try {
|
|
14470
|
+
const parsed = new URL(sitemapUrl);
|
|
14471
|
+
siteRoot = `${parsed.protocol}//${parsed.host}`;
|
|
14472
|
+
} catch {
|
|
14473
|
+
siteRoot = sitemapUrl;
|
|
14474
|
+
}
|
|
14475
|
+
const llmsChecks = await checkLlmsTxt(siteRoot);
|
|
14476
|
+
if (pages.length > 0) {
|
|
14477
|
+
pages[0] = {
|
|
14478
|
+
...pages[0],
|
|
14479
|
+
checks: [...pages[0].checks, ...llmsChecks]
|
|
14480
|
+
};
|
|
14481
|
+
}
|
|
14482
|
+
} catch {}
|
|
14483
|
+
const summary = computeSummary(pages);
|
|
14484
|
+
return {
|
|
14485
|
+
target: sitemapUrl,
|
|
14486
|
+
timestamp: new Date().toISOString(),
|
|
14487
|
+
pages,
|
|
14488
|
+
summary
|
|
14489
|
+
};
|
|
14490
|
+
}
|
|
14491
|
+
async function auditUrl(url, options = {}) {
|
|
14492
|
+
const page = await auditSingleUrl(url, options);
|
|
14493
|
+
try {
|
|
14494
|
+
const llmsChecks = await checkLlmsTxt(url);
|
|
14495
|
+
page.checks.push(...llmsChecks);
|
|
14496
|
+
} catch {}
|
|
14497
|
+
const summary = computeSummary([page]);
|
|
14498
|
+
return {
|
|
14499
|
+
target: url,
|
|
14500
|
+
timestamp: new Date().toISOString(),
|
|
14501
|
+
pages: [page],
|
|
14502
|
+
summary
|
|
14503
|
+
};
|
|
14504
|
+
}
|
|
14505
|
+
|
|
14506
|
+
// src/domain/seo/report-formatter.ts
|
|
14507
|
+
var SEVERITY_ICONS = {
|
|
14508
|
+
pass: "✓",
|
|
14509
|
+
warning: "⚠",
|
|
14510
|
+
error: "✗",
|
|
14511
|
+
info: "ℹ"
|
|
14512
|
+
};
|
|
14513
|
+
var SEVERITY_COLORS = {
|
|
14514
|
+
pass: "\x1B[32m",
|
|
14515
|
+
warning: "\x1B[33m",
|
|
14516
|
+
error: "\x1B[31m",
|
|
14517
|
+
info: "\x1B[36m"
|
|
14518
|
+
};
|
|
14519
|
+
var RESET = "\x1B[0m";
|
|
14520
|
+
var BOLD = "\x1B[1m";
|
|
14521
|
+
var DIM = "\x1B[2m";
|
|
14522
|
+
function formatCli(report) {
|
|
14523
|
+
const lines = [];
|
|
14524
|
+
lines.push("");
|
|
14525
|
+
lines.push(`${BOLD}SEO Audit Report${RESET}`);
|
|
14526
|
+
lines.push(`${DIM}Target: ${report.target}${RESET}`);
|
|
14527
|
+
lines.push(`${DIM}Time: ${report.timestamp}${RESET}`);
|
|
14528
|
+
lines.push(`${DIM}Pages: ${report.pages.length}${RESET}`);
|
|
14529
|
+
lines.push("");
|
|
14530
|
+
for (const page of report.pages) {
|
|
14531
|
+
lines.push(`${BOLD}── ${page.url} (${page.responseTime}ms) ──${RESET}`);
|
|
14532
|
+
const grouped = groupByCategory(page.checks);
|
|
14533
|
+
for (const [category, checks] of grouped) {
|
|
14534
|
+
lines.push(` ${DIM}${category}${RESET}`);
|
|
14535
|
+
for (const check of checks) {
|
|
14536
|
+
const icon = SEVERITY_ICONS[check.severity];
|
|
14537
|
+
const color = SEVERITY_COLORS[check.severity];
|
|
14538
|
+
lines.push(` ${color}${icon}${RESET} ${check.name}: ${check.message}`);
|
|
14539
|
+
if (check.remediation && check.severity !== "pass") {
|
|
14540
|
+
lines.push(` ${DIM}→ ${check.remediation}${RESET}`);
|
|
14541
|
+
}
|
|
14542
|
+
}
|
|
14543
|
+
}
|
|
14544
|
+
lines.push("");
|
|
14545
|
+
}
|
|
14546
|
+
const { summary } = report;
|
|
14547
|
+
lines.push(`${BOLD}Summary${RESET}`);
|
|
14548
|
+
lines.push(` Score: ${summary.score}%`);
|
|
14549
|
+
lines.push(` ${SEVERITY_COLORS.pass}✓ ${summary.passed} passed${RESET}`);
|
|
14550
|
+
lines.push(` ${SEVERITY_COLORS.warning}⚠ ${summary.warnings} warnings${RESET}`);
|
|
14551
|
+
lines.push(` ${SEVERITY_COLORS.error}✗ ${summary.errors} errors${RESET}`);
|
|
14552
|
+
lines.push(` Total checks: ${summary.total}`);
|
|
14553
|
+
lines.push("");
|
|
14554
|
+
return lines.join(`
|
|
14555
|
+
`);
|
|
14556
|
+
}
|
|
14557
|
+
function formatJson(report) {
|
|
14558
|
+
return JSON.stringify(report, null, 2);
|
|
14559
|
+
}
|
|
14560
|
+
function formatMarkdown(report) {
|
|
14561
|
+
const lines = [];
|
|
14562
|
+
lines.push("# SEO Audit Report");
|
|
14563
|
+
lines.push("");
|
|
14564
|
+
lines.push(`- **Target:** ${report.target}`);
|
|
14565
|
+
lines.push(`- **Date:** ${report.timestamp}`);
|
|
14566
|
+
lines.push(`- **Pages audited:** ${report.pages.length}`);
|
|
14567
|
+
lines.push("");
|
|
14568
|
+
lines.push("## Summary");
|
|
14569
|
+
lines.push("");
|
|
14570
|
+
lines.push(`| Metric | Value |`);
|
|
14571
|
+
lines.push(`|--------|-------|`);
|
|
14572
|
+
lines.push(`| Score | ${report.summary.score}% |`);
|
|
14573
|
+
lines.push(`| Passed | ${report.summary.passed} |`);
|
|
14574
|
+
lines.push(`| Warnings | ${report.summary.warnings} |`);
|
|
14575
|
+
lines.push(`| Errors | ${report.summary.errors} |`);
|
|
14576
|
+
lines.push(`| Total checks | ${report.summary.total} |`);
|
|
14577
|
+
lines.push("");
|
|
14578
|
+
for (const page of report.pages) {
|
|
14579
|
+
lines.push(`## ${page.url}`);
|
|
14580
|
+
lines.push("");
|
|
14581
|
+
lines.push(`Response time: ${page.responseTime}ms`);
|
|
14582
|
+
lines.push("");
|
|
14583
|
+
const errors3 = page.checks.filter((c) => c.severity === "error");
|
|
14584
|
+
const warnings = page.checks.filter((c) => c.severity === "warning");
|
|
14585
|
+
const passed = page.checks.filter((c) => c.severity === "pass");
|
|
14586
|
+
const info = page.checks.filter((c) => c.severity === "info");
|
|
14587
|
+
if (errors3.length > 0) {
|
|
14588
|
+
lines.push("### Errors");
|
|
14589
|
+
lines.push("");
|
|
14590
|
+
for (const check of errors3) {
|
|
14591
|
+
lines.push(`- **${check.name}** (${check.category}): ${check.message}`);
|
|
14592
|
+
if (check.remediation) {
|
|
14593
|
+
lines.push(` - Fix: ${check.remediation}`);
|
|
14594
|
+
}
|
|
14595
|
+
}
|
|
14596
|
+
lines.push("");
|
|
14597
|
+
}
|
|
14598
|
+
if (warnings.length > 0) {
|
|
14599
|
+
lines.push("### Warnings");
|
|
14600
|
+
lines.push("");
|
|
14601
|
+
for (const check of warnings) {
|
|
14602
|
+
lines.push(`- **${check.name}** (${check.category}): ${check.message}`);
|
|
14603
|
+
if (check.remediation) {
|
|
14604
|
+
lines.push(` - Fix: ${check.remediation}`);
|
|
14605
|
+
}
|
|
14606
|
+
}
|
|
14607
|
+
lines.push("");
|
|
14608
|
+
}
|
|
14609
|
+
if (passed.length > 0) {
|
|
14610
|
+
lines.push("### Passed");
|
|
14611
|
+
lines.push("");
|
|
14612
|
+
for (const check of passed) {
|
|
14613
|
+
lines.push(`- ${check.name}: ${check.message}`);
|
|
14614
|
+
}
|
|
14615
|
+
lines.push("");
|
|
14616
|
+
}
|
|
14617
|
+
if (info.length > 0) {
|
|
14618
|
+
lines.push("### Info");
|
|
14619
|
+
lines.push("");
|
|
14620
|
+
for (const check of info) {
|
|
14621
|
+
lines.push(`- ${check.name}: ${check.message}`);
|
|
14622
|
+
}
|
|
14623
|
+
lines.push("");
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
14626
|
+
lines.push("---");
|
|
14627
|
+
lines.push(`*Generated by CodeConductor SEO Audit on ${report.timestamp.split("T")[0]}*`);
|
|
14628
|
+
return lines.join(`
|
|
14629
|
+
`);
|
|
14630
|
+
}
|
|
14631
|
+
function groupByCategory(checks) {
|
|
14632
|
+
const groups = new Map;
|
|
14633
|
+
for (const check of checks) {
|
|
14634
|
+
if (!groups.has(check.category)) {
|
|
14635
|
+
groups.set(check.category, []);
|
|
14636
|
+
}
|
|
14637
|
+
groups.get(check.category).push(check);
|
|
14638
|
+
}
|
|
14639
|
+
return groups;
|
|
14640
|
+
}
|
|
14641
|
+
function computeExitCode(report, failOn) {
|
|
14642
|
+
if (report.summary.errors > 0)
|
|
14643
|
+
return 1;
|
|
14644
|
+
if (failOn === "warning" && report.summary.warnings > 0)
|
|
14645
|
+
return 2;
|
|
14646
|
+
if (report.summary.warnings > 0)
|
|
14647
|
+
return 0;
|
|
14648
|
+
return 0;
|
|
14649
|
+
}
|
|
14650
|
+
|
|
14651
|
+
// src/commands/seo-audit.command.ts
|
|
14652
|
+
async function seoAuditCommand(options) {
|
|
14653
|
+
const { url, sitemap, format, failOn, delay: delay2, output, followRedirects } = options;
|
|
14654
|
+
if (!url && !sitemap) {
|
|
14655
|
+
return {
|
|
14656
|
+
code: 1,
|
|
14657
|
+
data: {
|
|
14658
|
+
success: false,
|
|
14659
|
+
command: "seo audit",
|
|
14660
|
+
errors: ["Either --url or --sitemap is required"]
|
|
14661
|
+
}
|
|
14662
|
+
};
|
|
14663
|
+
}
|
|
14664
|
+
try {
|
|
14665
|
+
const report = sitemap ? await auditSitemap(sitemap, {
|
|
14666
|
+
delay: delay2,
|
|
14667
|
+
followRedirects,
|
|
14668
|
+
onProgress: format === "cli" ? (current, total, pageUrl) => {
|
|
14669
|
+
process.stderr.write(`\r Auditing ${current}/${total}: ${pageUrl}`);
|
|
14670
|
+
} : undefined
|
|
14671
|
+
}) : await auditUrl(url, { followRedirects });
|
|
14672
|
+
if (format === "cli") {
|
|
14673
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
14674
|
+
}
|
|
14675
|
+
let formattedOutput;
|
|
14676
|
+
switch (format) {
|
|
14677
|
+
case "json":
|
|
14678
|
+
formattedOutput = formatJson(report);
|
|
14679
|
+
break;
|
|
14680
|
+
case "markdown":
|
|
14681
|
+
formattedOutput = formatMarkdown(report);
|
|
14682
|
+
break;
|
|
14683
|
+
default:
|
|
14684
|
+
formattedOutput = formatCli(report);
|
|
14685
|
+
}
|
|
14686
|
+
if (output) {
|
|
14687
|
+
const outputPath = resolve9(options.projectRoot, output);
|
|
14688
|
+
await mkdir6(dirname4(outputPath), { recursive: true });
|
|
14689
|
+
await writeFile5(outputPath, formattedOutput, "utf-8");
|
|
14690
|
+
} else if (format === "markdown") {
|
|
14691
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
14692
|
+
const defaultPath = resolve9(options.projectRoot, "seo-reports", `audit-report-${timestamp}.md`);
|
|
14693
|
+
await mkdir6(dirname4(defaultPath), { recursive: true });
|
|
14694
|
+
await writeFile5(defaultPath, formattedOutput, "utf-8");
|
|
14695
|
+
process.stderr.write(`Report saved to: ${defaultPath}
|
|
14696
|
+
`);
|
|
14697
|
+
}
|
|
14698
|
+
const exitCode = computeExitCode(report, failOn);
|
|
14699
|
+
return {
|
|
14700
|
+
code: exitCode,
|
|
14701
|
+
data: {
|
|
14702
|
+
success: true,
|
|
14703
|
+
command: "seo audit",
|
|
14704
|
+
report,
|
|
14705
|
+
output: formattedOutput,
|
|
14706
|
+
outputFile: output ? resolve9(options.projectRoot, output) : format === "markdown" ? resolve9(options.projectRoot, "seo-reports") : undefined
|
|
14707
|
+
}
|
|
14708
|
+
};
|
|
14709
|
+
} catch (error) {
|
|
14710
|
+
return {
|
|
14711
|
+
code: 3,
|
|
14712
|
+
data: {
|
|
14713
|
+
success: false,
|
|
14714
|
+
command: "seo audit",
|
|
14715
|
+
errors: [String(error)]
|
|
14716
|
+
}
|
|
14717
|
+
};
|
|
14718
|
+
}
|
|
14719
|
+
}
|
|
14720
|
+
|
|
14721
|
+
// src/commands/seo-llms.command.ts
|
|
14722
|
+
import { writeFile as writeFile6, mkdir as mkdir7 } from "node:fs/promises";
|
|
14723
|
+
import { dirname as dirname5, resolve as resolve10 } from "node:path";
|
|
14724
|
+
|
|
14725
|
+
// src/domain/seo/llms-generator.ts
|
|
14726
|
+
function extractTitle2(html) {
|
|
14727
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
14728
|
+
return match ? match[1].trim() : "";
|
|
14729
|
+
}
|
|
14730
|
+
function extractDescription(html) {
|
|
14731
|
+
const patterns = [
|
|
14732
|
+
/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i,
|
|
14733
|
+
/<meta[^>]*content=["']([^"']*)["'][^>]*name=["']description["']/i,
|
|
14734
|
+
/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']*)["']/i
|
|
14735
|
+
];
|
|
14736
|
+
for (const pattern of patterns) {
|
|
14737
|
+
const match = html.match(pattern);
|
|
14738
|
+
if (match)
|
|
14739
|
+
return match[1].trim();
|
|
14740
|
+
}
|
|
14741
|
+
return "";
|
|
14742
|
+
}
|
|
14743
|
+
function extractFirstParagraph(html) {
|
|
14744
|
+
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, "");
|
|
14745
|
+
const match = bodyHtml.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
14746
|
+
if (!match)
|
|
14747
|
+
return "";
|
|
14748
|
+
return match[1].replace(/<[^>]*>/g, "").trim().slice(0, 200);
|
|
14749
|
+
}
|
|
14750
|
+
function groupByPathSegment(entries) {
|
|
14751
|
+
const groups = new Map;
|
|
14752
|
+
for (const entry of entries) {
|
|
14753
|
+
try {
|
|
14754
|
+
const parsed = new URL(entry.url);
|
|
14755
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
14756
|
+
const group = segments.length > 0 ? `/${segments[0]}/` : "/";
|
|
14757
|
+
if (!groups.has(group)) {
|
|
14758
|
+
groups.set(group, []);
|
|
14759
|
+
}
|
|
14760
|
+
groups.get(group).push(entry);
|
|
14761
|
+
} catch {
|
|
14762
|
+
if (!groups.has("/")) {
|
|
14763
|
+
groups.set("/", []);
|
|
14764
|
+
}
|
|
14765
|
+
groups.get("/").push(entry);
|
|
14766
|
+
}
|
|
14767
|
+
}
|
|
14768
|
+
return groups;
|
|
14769
|
+
}
|
|
14770
|
+
function formatLlmsTxt(siteName, siteDescription, entries) {
|
|
14771
|
+
const lines = [];
|
|
14772
|
+
lines.push(`# ${siteName}`);
|
|
14773
|
+
if (siteDescription) {
|
|
14774
|
+
lines.push(`> ${siteDescription}`);
|
|
14775
|
+
}
|
|
14776
|
+
lines.push("");
|
|
14777
|
+
const groups = groupByPathSegment(entries);
|
|
14778
|
+
for (const [group, groupEntries] of groups) {
|
|
14779
|
+
const groupName = group === "/" ? "Main Pages" : group.replace(/\//g, "").replace(/-/g, " ");
|
|
14780
|
+
lines.push(`## ${groupName.charAt(0).toUpperCase() + groupName.slice(1)}`);
|
|
14781
|
+
lines.push("");
|
|
14782
|
+
for (const entry of groupEntries) {
|
|
14783
|
+
const desc = entry.description ? `: ${entry.description}` : "";
|
|
14784
|
+
lines.push(`- [${entry.title}](${entry.url})${desc}`);
|
|
14785
|
+
}
|
|
14786
|
+
lines.push("");
|
|
14787
|
+
}
|
|
14788
|
+
lines.push("---");
|
|
14789
|
+
lines.push(`Generated by CodeConductor SEO on ${new Date().toISOString().split("T")[0]}`);
|
|
14790
|
+
return lines.join(`
|
|
14791
|
+
`);
|
|
14792
|
+
}
|
|
14793
|
+
async function generateLlmsTxtFromSitemap(sitemapUrl, options = {}) {
|
|
14794
|
+
const { delay: requestDelay = 500, maxUrls, onProgress } = options;
|
|
14795
|
+
const sitemapResult = await parseSitemap(sitemapUrl, { delay: requestDelay });
|
|
14796
|
+
let urls = sitemapResult.entries.map((e) => e.url);
|
|
14797
|
+
if (maxUrls && urls.length > maxUrls) {
|
|
14798
|
+
urls = urls.slice(0, maxUrls);
|
|
14799
|
+
}
|
|
14800
|
+
const entries = [];
|
|
14801
|
+
let siteName = "";
|
|
14802
|
+
let siteDescription = "";
|
|
14803
|
+
for (let i = 0;i < urls.length; i++) {
|
|
14804
|
+
const url = urls[i];
|
|
14805
|
+
if (onProgress) {
|
|
14806
|
+
onProgress(i + 1, urls.length, url);
|
|
14807
|
+
}
|
|
14808
|
+
if (i > 0 && requestDelay > 0) {
|
|
14809
|
+
await delay(requestDelay);
|
|
14810
|
+
}
|
|
14811
|
+
try {
|
|
14812
|
+
const response = await safeFetch(url);
|
|
14813
|
+
const html = response.body;
|
|
14814
|
+
const title = extractTitle2(html) || url;
|
|
14815
|
+
const description = extractDescription(html) || extractFirstParagraph(html);
|
|
14816
|
+
if (i === 0) {
|
|
14817
|
+
siteName = title;
|
|
14818
|
+
siteDescription = extractDescription(html);
|
|
14819
|
+
}
|
|
14820
|
+
entries.push({ title, url, description });
|
|
14821
|
+
} catch {
|
|
14822
|
+
entries.push({ title: url, url, description: "" });
|
|
14823
|
+
}
|
|
14824
|
+
}
|
|
14825
|
+
if (!siteName) {
|
|
14826
|
+
try {
|
|
14827
|
+
siteName = new URL(sitemapUrl).hostname;
|
|
14828
|
+
} catch {
|
|
14829
|
+
siteName = "Site";
|
|
14830
|
+
}
|
|
14831
|
+
}
|
|
14832
|
+
const content = formatLlmsTxt(siteName, siteDescription, entries);
|
|
14833
|
+
return { content, entries };
|
|
14834
|
+
}
|
|
14835
|
+
async function generateLlmsTxtFromUrl(url) {
|
|
14836
|
+
const response = await safeFetch(url);
|
|
14837
|
+
const html = response.body;
|
|
14838
|
+
const title = extractTitle2(html) || url;
|
|
14839
|
+
const description = extractDescription(html) || extractFirstParagraph(html);
|
|
14840
|
+
const entries = [{ title, url, description }];
|
|
14841
|
+
const content = formatLlmsTxt(title, description, entries);
|
|
14842
|
+
return { content, entries };
|
|
14843
|
+
}
|
|
14844
|
+
|
|
14845
|
+
// src/commands/seo-llms.command.ts
|
|
14846
|
+
async function seoLlmsCommand(options) {
|
|
14847
|
+
const { url, sitemap, output, delay: delay2 } = options;
|
|
14848
|
+
if (!url && !sitemap) {
|
|
14849
|
+
return {
|
|
14850
|
+
code: 1,
|
|
14851
|
+
data: {
|
|
14852
|
+
success: false,
|
|
14853
|
+
command: "seo llms",
|
|
14854
|
+
errors: ["Either --url or --sitemap is required"]
|
|
14855
|
+
}
|
|
14856
|
+
};
|
|
14857
|
+
}
|
|
14858
|
+
try {
|
|
14859
|
+
const result = sitemap ? await generateLlmsTxtFromSitemap(sitemap, {
|
|
14860
|
+
delay: delay2,
|
|
14861
|
+
onProgress: (current, total, pageUrl) => {
|
|
14862
|
+
process.stderr.write(`\r Processing ${current}/${total}: ${pageUrl}`);
|
|
14863
|
+
}
|
|
14864
|
+
}) : await generateLlmsTxtFromUrl(url);
|
|
14865
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
14866
|
+
const outputPath = output ? resolve10(options.projectRoot, output) : resolve10(options.projectRoot, "llms.txt");
|
|
14867
|
+
await mkdir7(dirname5(outputPath), { recursive: true });
|
|
14868
|
+
await writeFile6(outputPath, result.content, "utf-8");
|
|
14869
|
+
process.stderr.write(`Generated: ${outputPath} (${result.entries.length} entries)
|
|
14870
|
+
`);
|
|
14871
|
+
return {
|
|
14872
|
+
code: 0,
|
|
14873
|
+
data: {
|
|
14874
|
+
success: true,
|
|
14875
|
+
command: "seo llms",
|
|
14876
|
+
outputFile: outputPath,
|
|
14877
|
+
entries: result.entries.length,
|
|
14878
|
+
content: result.content
|
|
14879
|
+
}
|
|
14880
|
+
};
|
|
14881
|
+
} catch (error) {
|
|
14882
|
+
return {
|
|
14883
|
+
code: 3,
|
|
14884
|
+
data: {
|
|
14885
|
+
success: false,
|
|
14886
|
+
command: "seo llms",
|
|
14887
|
+
errors: [String(error)]
|
|
14888
|
+
}
|
|
14889
|
+
};
|
|
14890
|
+
}
|
|
14891
|
+
}
|
|
14892
|
+
|
|
14893
|
+
// src/commands/update.command.ts
|
|
14894
|
+
async function updateCommand(options) {
|
|
14895
|
+
const { dryRun, force, output, projectRoot } = options;
|
|
14896
|
+
try {
|
|
14897
|
+
const configResult = await loadConfig(projectRoot);
|
|
14898
|
+
if (!configResult.success) {
|
|
14899
|
+
return {
|
|
14900
|
+
code: 1,
|
|
14901
|
+
data: {
|
|
14902
|
+
success: false,
|
|
14903
|
+
command: "update",
|
|
14904
|
+
errors: ["No config found. Run `codeconductor init` first."]
|
|
14905
|
+
}
|
|
14906
|
+
};
|
|
14907
|
+
}
|
|
14908
|
+
const config = configResult.data;
|
|
14909
|
+
if (!config.presets.council.enabled) {
|
|
14910
|
+
return {
|
|
14911
|
+
code: 4,
|
|
14912
|
+
data: {
|
|
14913
|
+
success: false,
|
|
14914
|
+
command: "update",
|
|
14915
|
+
errors: ["Council preset is not enabled"]
|
|
14916
|
+
}
|
|
14917
|
+
};
|
|
14918
|
+
}
|
|
14919
|
+
const presetResult = await loadCouncilPreset(projectRoot);
|
|
14920
|
+
if (!presetResult.success) {
|
|
14921
|
+
return {
|
|
14922
|
+
code: 1,
|
|
14923
|
+
data: {
|
|
14924
|
+
success: false,
|
|
14925
|
+
command: "update",
|
|
14926
|
+
errors: ["Failed to load preset"]
|
|
14927
|
+
}
|
|
14928
|
+
};
|
|
14929
|
+
}
|
|
14930
|
+
const spec = presetResult.data;
|
|
14931
|
+
const currentVersion = config.presets.council.version;
|
|
14932
|
+
const newVersion = spec.version;
|
|
14933
|
+
if (currentVersion === newVersion) {
|
|
14934
|
+
return {
|
|
14935
|
+
code: 0,
|
|
14936
|
+
data: {
|
|
14937
|
+
success: true,
|
|
14938
|
+
command: "update",
|
|
14939
|
+
message: "Already up to date",
|
|
14940
|
+
currentVersion,
|
|
14941
|
+
newVersion
|
|
14942
|
+
}
|
|
14943
|
+
};
|
|
14944
|
+
}
|
|
14945
|
+
if (dryRun) {
|
|
14946
|
+
return {
|
|
14947
|
+
code: 0,
|
|
14948
|
+
data: {
|
|
14949
|
+
success: true,
|
|
14950
|
+
command: "update",
|
|
14951
|
+
message: "Dry run - would update",
|
|
14952
|
+
currentVersion,
|
|
14953
|
+
newVersion,
|
|
14954
|
+
wouldUpdate: ["council preset files"]
|
|
14955
|
+
}
|
|
14956
|
+
};
|
|
14957
|
+
}
|
|
14958
|
+
const writeOptions = { dryRun: false, force };
|
|
14959
|
+
const target = config.defaults.target;
|
|
14960
|
+
let installer;
|
|
14961
|
+
switch (target) {
|
|
14962
|
+
case "opencode":
|
|
14963
|
+
installer = createOpenCodeInstaller(spec);
|
|
14964
|
+
break;
|
|
14965
|
+
case "claude":
|
|
14966
|
+
installer = createClaudeInstaller(spec);
|
|
14967
|
+
break;
|
|
14968
|
+
case "codex":
|
|
14969
|
+
installer = createCodexInstaller(spec);
|
|
14970
|
+
break;
|
|
14971
|
+
default:
|
|
14972
|
+
return {
|
|
14973
|
+
code: 1,
|
|
14974
|
+
data: {
|
|
14975
|
+
success: false,
|
|
14976
|
+
command: "update",
|
|
14977
|
+
errors: [`Unknown target: ${target}`]
|
|
14978
|
+
}
|
|
14979
|
+
};
|
|
14980
|
+
}
|
|
14981
|
+
const files = await installer.generate();
|
|
14982
|
+
const results = await writeGeneratedFiles(files, writeOptions);
|
|
14983
|
+
const updated = results.filter((r) => r.success).map((r) => r.path);
|
|
14984
|
+
const errors3 = results.filter((r) => !r.success).map((r) => `${r.path}: ${r.error}`);
|
|
14985
|
+
if (errors3.length > 0) {
|
|
14986
|
+
return {
|
|
14987
|
+
code: 2,
|
|
14988
|
+
data: {
|
|
14989
|
+
success: false,
|
|
14990
|
+
command: "update",
|
|
14991
|
+
errors: errors3
|
|
14992
|
+
}
|
|
14993
|
+
};
|
|
14994
|
+
}
|
|
14995
|
+
return {
|
|
14996
|
+
code: 0,
|
|
14997
|
+
data: {
|
|
14998
|
+
success: true,
|
|
14999
|
+
command: "update",
|
|
15000
|
+
message: "Updated successfully",
|
|
15001
|
+
currentVersion,
|
|
15002
|
+
newVersion,
|
|
15003
|
+
updated
|
|
15004
|
+
}
|
|
15005
|
+
};
|
|
15006
|
+
} catch (error) {
|
|
15007
|
+
return {
|
|
15008
|
+
code: 1,
|
|
15009
|
+
data: {
|
|
15010
|
+
success: false,
|
|
15011
|
+
command: "update",
|
|
15012
|
+
errors: [String(error)]
|
|
15013
|
+
}
|
|
15014
|
+
};
|
|
15015
|
+
}
|
|
15016
|
+
}
|
|
15017
|
+
|
|
15018
|
+
// src/cli/router.ts
|
|
15019
|
+
function parseArgs(args) {
|
|
15020
|
+
const flags = {
|
|
15021
|
+
help: false,
|
|
15022
|
+
version: false,
|
|
15023
|
+
dryRun: false,
|
|
15024
|
+
force: false,
|
|
15025
|
+
output: "human"
|
|
15026
|
+
};
|
|
15027
|
+
const options = {};
|
|
15028
|
+
const remaining = [];
|
|
15029
|
+
for (const arg of args) {
|
|
15030
|
+
if (arg === "--help" || arg === "-h") {
|
|
15031
|
+
flags.help = true;
|
|
15032
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
15033
|
+
flags.version = true;
|
|
15034
|
+
} else if (arg === "--dry-run") {
|
|
15035
|
+
flags.dryRun = true;
|
|
15036
|
+
} else if (arg === "--force") {
|
|
15037
|
+
flags.force = true;
|
|
15038
|
+
} else if (arg === "--output" || arg === "-o") {
|
|
15039
|
+
remaining.push(arg);
|
|
15040
|
+
} else if (arg.startsWith("--output=") || arg.startsWith("-o=")) {
|
|
15041
|
+
const value = arg.split("=")[1];
|
|
15042
|
+
if (value === "json" || value === "human") {
|
|
15043
|
+
flags.output = value;
|
|
15044
|
+
}
|
|
15045
|
+
} else {
|
|
15046
|
+
remaining.push(arg);
|
|
15047
|
+
}
|
|
15048
|
+
}
|
|
15049
|
+
for (let i = 0;i < remaining.length; i++) {
|
|
15050
|
+
if ((remaining[i] === "--output" || remaining[i] === "-o") && remaining[i + 1]) {
|
|
15051
|
+
const value = remaining[i + 1];
|
|
15052
|
+
if (value === "json" || value === "human") {
|
|
15053
|
+
flags.output = value;
|
|
15054
|
+
remaining.splice(i, 2);
|
|
15055
|
+
i--;
|
|
15056
|
+
}
|
|
15057
|
+
}
|
|
15058
|
+
}
|
|
15059
|
+
const command = remaining[0] || "help";
|
|
15060
|
+
const subcommand = remaining[1] && !remaining[1].startsWith("-") ? remaining[1] : undefined;
|
|
15061
|
+
for (let i = 1;i < remaining.length; i++) {
|
|
15062
|
+
const arg = remaining[i];
|
|
15063
|
+
if (arg.startsWith("--")) {
|
|
15064
|
+
const [key, value] = arg.slice(2).split("=");
|
|
15065
|
+
if (key === "lang") {
|
|
15066
|
+
const langValue = value !== undefined ? value : remaining[++i];
|
|
15067
|
+
if (langValue && typeof langValue === "string") {
|
|
15068
|
+
options[key] = langValue.split(",").map((s) => s.trim());
|
|
15069
|
+
}
|
|
15070
|
+
} else if (value !== undefined) {
|
|
15071
|
+
options[key] = value;
|
|
15072
|
+
} else if (remaining[i + 1] && !remaining[i + 1].startsWith("-")) {
|
|
15073
|
+
options[key] = remaining[++i];
|
|
15074
|
+
} else {
|
|
15075
|
+
options[key] = true;
|
|
15076
|
+
}
|
|
15077
|
+
}
|
|
15078
|
+
}
|
|
15079
|
+
return { command, subcommand, options, flags };
|
|
15080
|
+
}
|
|
15081
|
+
function getVersion() {
|
|
15082
|
+
return `${package_default.name} v${package_default.version}`;
|
|
15083
|
+
}
|
|
15084
|
+
function getHelp() {
|
|
15085
|
+
return `CodeConductor CLI v${package_default.version}
|
|
15086
|
+
|
|
15087
|
+
Usage: npx cc-codeconductor <command> [options]
|
|
15088
|
+
|
|
15089
|
+
Commands:
|
|
15090
|
+
init Initialize CodeConductor in a project
|
|
15091
|
+
detect Detect project stack and recommended presets
|
|
12858
15092
|
install council Install generated council spec files to runner targets
|
|
12859
15093
|
install preset Install full preset (agents, prompts, skills, commands)
|
|
15094
|
+
install lsp Install and configure LSP servers for AI coding tools
|
|
15095
|
+
seo audit Run SEO audit on a URL or sitemap
|
|
15096
|
+
seo llms Generate llms.txt from a URL or sitemap
|
|
12860
15097
|
doctor Validate configuration and generated files
|
|
12861
15098
|
update Update installed presets
|
|
12862
15099
|
|
|
@@ -12867,6 +15104,7 @@ Options:
|
|
|
12867
15104
|
--force Allow overwriting existing files
|
|
12868
15105
|
--global Install to home directory (~/.claude, ~/.opencode, etc.)
|
|
12869
15106
|
--output, -o Output mode: human or json
|
|
15107
|
+
--lang Comma-separated list of languages (e.g., typescript,php,python)
|
|
12870
15108
|
|
|
12871
15109
|
Examples:
|
|
12872
15110
|
npx cc-codeconductor init
|
|
@@ -12881,8 +15119,16 @@ Examples:
|
|
|
12881
15119
|
npx cc-codeconductor install council --target claude
|
|
12882
15120
|
npx cc-codeconductor install council --target codex
|
|
12883
15121
|
npx cc-codeconductor install council --target all
|
|
15122
|
+
npx cc-codeconductor install lsp --target opencode
|
|
15123
|
+
npx cc-codeconductor install lsp --target all --lang typescript,python
|
|
15124
|
+
npx cc-codeconductor install lsp --target claude --dry-run
|
|
12884
15125
|
npx cc-codeconductor doctor
|
|
12885
15126
|
npx cc-codeconductor update --dry-run
|
|
15127
|
+
npx cc-codeconductor seo audit --url https://example.com
|
|
15128
|
+
npx cc-codeconductor seo audit --sitemap https://example.com/sitemap.xml
|
|
15129
|
+
npx cc-codeconductor seo audit --sitemap https://example.com/sitemap.xml --format markdown
|
|
15130
|
+
npx cc-codeconductor seo llms --sitemap https://example.com/sitemap.xml
|
|
15131
|
+
npx cc-codeconductor seo llms --url https://example.com --output llms.txt
|
|
12886
15132
|
`;
|
|
12887
15133
|
}
|
|
12888
15134
|
async function routeCommand(args, projectRoot) {
|
|
@@ -12905,7 +15151,7 @@ async function routeCommand(args, projectRoot) {
|
|
|
12905
15151
|
});
|
|
12906
15152
|
case "install": {
|
|
12907
15153
|
const isGlobal = options.global === true || options.global === "true";
|
|
12908
|
-
const VALID_TARGETS = ["opencode", "claude", "codex", "all"];
|
|
15154
|
+
const VALID_TARGETS = ["opencode", "claude", "codex", "gemini", "cursor", "agy", "all"];
|
|
12909
15155
|
let resolvedSubcommand = subcommand;
|
|
12910
15156
|
let target = options.target;
|
|
12911
15157
|
if (!target && subcommand && VALID_TARGETS.includes(subcommand)) {
|
|
@@ -12913,6 +15159,17 @@ async function routeCommand(args, projectRoot) {
|
|
|
12913
15159
|
resolvedSubcommand = undefined;
|
|
12914
15160
|
}
|
|
12915
15161
|
target = target || "opencode";
|
|
15162
|
+
if (resolvedSubcommand === "lsp") {
|
|
15163
|
+
return installLspCommand({
|
|
15164
|
+
projectRoot,
|
|
15165
|
+
target,
|
|
15166
|
+
lang: options.lang,
|
|
15167
|
+
dryRun: flags.dryRun,
|
|
15168
|
+
force: flags.force,
|
|
15169
|
+
global: isGlobal,
|
|
15170
|
+
output: flags.output
|
|
15171
|
+
});
|
|
15172
|
+
}
|
|
12916
15173
|
if (resolvedSubcommand === "preset") {
|
|
12917
15174
|
return installPresetCommand({
|
|
12918
15175
|
projectRoot,
|
|
@@ -12944,6 +15201,37 @@ async function routeCommand(args, projectRoot) {
|
|
|
12944
15201
|
force: flags.force,
|
|
12945
15202
|
output: flags.output
|
|
12946
15203
|
});
|
|
15204
|
+
case "seo": {
|
|
15205
|
+
if (subcommand === "audit") {
|
|
15206
|
+
return seoAuditCommand({
|
|
15207
|
+
url: options.url,
|
|
15208
|
+
sitemap: options.sitemap,
|
|
15209
|
+
format: options.format ?? (flags.output === "json" ? "json" : "cli"),
|
|
15210
|
+
failOn: options["fail-on"] ?? "error",
|
|
15211
|
+
delay: options.delay ? parseInt(String(options.delay), 10) : 500,
|
|
15212
|
+
output: options.output,
|
|
15213
|
+
followRedirects: options["follow-redirects"] === true,
|
|
15214
|
+
projectRoot
|
|
15215
|
+
});
|
|
15216
|
+
}
|
|
15217
|
+
if (subcommand === "llms") {
|
|
15218
|
+
return seoLlmsCommand({
|
|
15219
|
+
url: options.url,
|
|
15220
|
+
sitemap: options.sitemap,
|
|
15221
|
+
output: options.output,
|
|
15222
|
+
delay: options.delay ? parseInt(String(options.delay), 10) : 500,
|
|
15223
|
+
projectRoot
|
|
15224
|
+
});
|
|
15225
|
+
}
|
|
15226
|
+
return {
|
|
15227
|
+
code: 1,
|
|
15228
|
+
data: {
|
|
15229
|
+
success: false,
|
|
15230
|
+
command: "seo",
|
|
15231
|
+
errors: ["Usage: seo audit|llms. Run `codeconductor seo audit --help` for details."]
|
|
15232
|
+
}
|
|
15233
|
+
};
|
|
15234
|
+
}
|
|
12947
15235
|
default:
|
|
12948
15236
|
return {
|
|
12949
15237
|
code: 1,
|
|
@@ -13035,6 +15323,8 @@ ${count} files processed${errCount > 0 ? `, ${errCount} errors` : ""}${note}`);
|
|
|
13035
15323
|
console.log(` - ${key}: ${value}`);
|
|
13036
15324
|
}
|
|
13037
15325
|
});
|
|
15326
|
+
} else if ("output" in data && typeof data.output === "string") {
|
|
15327
|
+
console.log(data.output);
|
|
13038
15328
|
}
|
|
13039
15329
|
}
|
|
13040
15330
|
}
|