@pas7/llm-seo 0.1.6

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.
@@ -0,0 +1,2232 @@
1
+ #!/usr/bin/env node
2
+ import { z } from 'zod';
3
+ import * as fs from 'fs/promises';
4
+ import { writeFile, rename, unlink, readFile, mkdir } from 'fs/promises';
5
+ import { resolve, dirname, basename, join, extname } from 'path';
6
+ import { randomBytes } from 'crypto';
7
+ import { existsSync } from 'fs';
8
+ import { pathToFileURL } from 'url';
9
+ import { Command } from 'commander';
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+
21
+ // src/cli/exit-codes.ts
22
+ var ExitCodes;
23
+ var init_exit_codes = __esm({
24
+ "src/cli/exit-codes.ts"() {
25
+ ExitCodes = {
26
+ /** Command completed successfully */
27
+ OK: 0,
28
+ /** Warnings found (when --fail-on warn) */
29
+ WARN: 1,
30
+ /** Errors found */
31
+ ERROR: 2,
32
+ /** Network error (for doctor command) */
33
+ NETWORK_ERROR: 3,
34
+ /** Configuration file not found */
35
+ CONFIG_NOT_FOUND: 4,
36
+ /** Invalid configuration */
37
+ INVALID_CONFIG: 5,
38
+ /** File not found */
39
+ FILE_NOT_FOUND: 6,
40
+ /** Validation failed */
41
+ VALIDATION_FAILED: 7,
42
+ /** Generation failed */
43
+ GENERATION_FAILED: 8,
44
+ /** Permission denied */
45
+ PERMISSION_DENIED: 126,
46
+ /** Command not found */
47
+ COMMAND_NOT_FOUND: 127
48
+ };
49
+ }
50
+ });
51
+ var SiteConfigSchema, BrandConfigSchema, SectionsConfigSchema, SocialConfigSchema, ContactConfigSchema, RestrictedClaimsConfigSchema, PolicyConfigSchema, BookingConfigSchema, MachineHintsConfigSchema, OutputPathsConfigSchema, OutputConfigSchema, FormatConfigSchema, LlmsSeoConfigSchema, ConfigSchema, LocaleConfigSchema, CheckConfigSchema;
52
+ var init_config_schema = __esm({
53
+ "src/schema/config.schema.ts"() {
54
+ SiteConfigSchema = z.object({
55
+ /** Site base URL - must be valid URL with http/https, no trailing slash */
56
+ baseUrl: z.string().url({ message: "Must be a valid URL with http or https protocol" }).refine(
57
+ (url) => !url.endsWith("/"),
58
+ { message: "Base URL must not have a trailing slash" }
59
+ ),
60
+ /** Default locale - must be in locales if provided */
61
+ defaultLocale: z.string().min(2).optional()
62
+ });
63
+ BrandConfigSchema = z.object({
64
+ /** Brand name - required */
65
+ name: z.string().min(1, { message: "Brand name is required" }),
66
+ /** Optional tagline */
67
+ tagline: z.string().optional(),
68
+ /** Optional description */
69
+ description: z.string().optional(),
70
+ /** Optional organization name */
71
+ org: z.string().optional(),
72
+ /** Supported locales - e.g., ["en", "uk", "de"] */
73
+ locales: z.array(z.string().min(2)).min(1, { message: "At least one locale is required" })
74
+ });
75
+ SectionsConfigSchema = z.object({
76
+ /** Hub paths - e.g., ["/services", "/blog", "/projects"] */
77
+ hubs: z.array(z.string()).default([])
78
+ });
79
+ SocialConfigSchema = z.object({
80
+ /** Twitter handle or URL */
81
+ twitter: z.string().optional(),
82
+ /** LinkedIn URL */
83
+ linkedin: z.string().optional(),
84
+ /** GitHub URL */
85
+ github: z.string().optional()
86
+ });
87
+ ContactConfigSchema = z.object({
88
+ /** Contact email */
89
+ email: z.string().email().optional(),
90
+ /** Social links */
91
+ social: SocialConfigSchema.optional(),
92
+ /** Phone number */
93
+ phone: z.string().optional()
94
+ });
95
+ RestrictedClaimsConfigSchema = z.object({
96
+ /** Enable restricted claims checking */
97
+ enable: z.boolean(),
98
+ /** Forbidden words/phrases - e.g., ["best", "#1", "guaranteed"] */
99
+ forbidden: z.array(z.string()).optional(),
100
+ /** Allowlisted phrases */
101
+ whitelist: z.array(z.string()).optional()
102
+ });
103
+ PolicyConfigSchema = z.object({
104
+ /** Geographic policy statement */
105
+ geoPolicy: z.string().optional(),
106
+ /** Citation rules */
107
+ citationRules: z.string().optional(),
108
+ /** Restricted claims configuration */
109
+ restrictedClaims: RestrictedClaimsConfigSchema.optional()
110
+ });
111
+ BookingConfigSchema = z.object({
112
+ /** Booking URL - e.g., Cal.com link */
113
+ url: z.string().url().optional(),
114
+ /** Booking label - e.g., "Book a consultation" */
115
+ label: z.string().optional()
116
+ });
117
+ MachineHintsConfigSchema = z.object({
118
+ /** URL to robots.txt */
119
+ robots: z.string().url().optional(),
120
+ /** URL to sitemap.xml */
121
+ sitemap: z.string().url().optional(),
122
+ /** URL to llms.txt */
123
+ llmsTxt: z.string().url().optional(),
124
+ /** URL to llms-full.txt */
125
+ llmsFullTxt: z.string().url().optional()
126
+ });
127
+ OutputPathsConfigSchema = z.object({
128
+ /** Path to llms.txt output - e.g., "public/llms.txt" */
129
+ llmsTxt: z.string().min(1, { message: "llmsTxt output path is required" }),
130
+ /** Path to llms-full.txt output - e.g., "public/llms-full.txt" */
131
+ llmsFullTxt: z.string().min(1, { message: "llmsFullTxt output path is required" }),
132
+ /** Path to citations.json output - e.g., "public/citations.json" */
133
+ citations: z.string().optional()
134
+ });
135
+ OutputConfigSchema = z.object({
136
+ /** Output paths */
137
+ paths: OutputPathsConfigSchema
138
+ });
139
+ FormatConfigSchema = z.object({
140
+ /** Trailing slash handling */
141
+ trailingSlash: z.enum(["always", "never", "preserve"]).default("never"),
142
+ /** Line endings format */
143
+ lineEndings: z.enum(["lf", "crlf"]).default("lf"),
144
+ /** Locale URL strategy */
145
+ localeStrategy: z.enum(["prefix", "subdomain", "none"]).optional()
146
+ });
147
+ LlmsSeoConfigSchema = z.object({
148
+ /** Site configuration */
149
+ site: SiteConfigSchema,
150
+ /** Brand configuration */
151
+ brand: BrandConfigSchema,
152
+ /** Sections configuration */
153
+ sections: SectionsConfigSchema.optional(),
154
+ /** Manifests configuration */
155
+ manifests: z.record(z.unknown()).default({}),
156
+ /** Contact configuration */
157
+ contact: ContactConfigSchema.optional(),
158
+ /** Policy configuration */
159
+ policy: PolicyConfigSchema.optional(),
160
+ /** Booking configuration */
161
+ booking: BookingConfigSchema.optional(),
162
+ /** Machine hints configuration */
163
+ machineHints: MachineHintsConfigSchema.optional(),
164
+ /** Output configuration */
165
+ output: OutputConfigSchema,
166
+ /** Format configuration */
167
+ format: FormatConfigSchema.optional()
168
+ });
169
+ ConfigSchema = z.object({
170
+ baseUrl: z.string().url(),
171
+ title: z.string().min(1),
172
+ description: z.string().optional(),
173
+ outputDir: z.string().default("./public"),
174
+ includeOptionalSections: z.boolean().default(false),
175
+ maxContentLength: z.number().int().nonnegative().default(0)
176
+ });
177
+ LocaleConfigSchema = z.object({
178
+ default: z.string().min(2),
179
+ supported: z.array(z.string().min(2)).min(1),
180
+ strategy: z.enum(["subdirectory", "subdomain", "domain"])
181
+ });
182
+ CheckConfigSchema = z.object({
183
+ strict: z.boolean().default(false),
184
+ maxTitleLength: z.number().int().positive().default(60),
185
+ maxDescriptionLength: z.number().int().positive().default(160),
186
+ enableLint: z.boolean().default(true)
187
+ });
188
+ z.object({
189
+ site: ConfigSchema,
190
+ locale: LocaleConfigSchema.optional(),
191
+ check: CheckConfigSchema.optional()
192
+ });
193
+ }
194
+ });
195
+ async function ensureDir(dirPath) {
196
+ await mkdir(dirPath, { recursive: true });
197
+ }
198
+ async function readFileSafe(filePath) {
199
+ try {
200
+ return await readFile(filePath, "utf-8");
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ async function writeFileAtomic(filePath, content) {
206
+ const dir = dirname(filePath);
207
+ await ensureDir(dir);
208
+ const uniqueId = randomBytes(8).toString("hex");
209
+ const baseName = basename(filePath);
210
+ const tempPath = join(dir, `.tmp-${uniqueId}-${baseName}`);
211
+ try {
212
+ await writeFile(tempPath, content, "utf-8");
213
+ await rename(tempPath, filePath);
214
+ } catch (error) {
215
+ try {
216
+ await unlink(tempPath);
217
+ } catch {
218
+ }
219
+ throw error;
220
+ }
221
+ }
222
+ var init_fs = __esm({
223
+ "src/cli/io/fs.ts"() {
224
+ }
225
+ });
226
+ function findConfigFile(cwd = process.cwd()) {
227
+ for (const fileName of CONFIG_FILE_NAMES) {
228
+ const filePath = resolve(cwd, fileName);
229
+ if (existsSync(filePath)) {
230
+ return filePath;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ function getConfigFileType(filePath) {
236
+ const ext = extname(filePath);
237
+ if (ext === ".ts") return "ts";
238
+ if (ext === ".js") return "js";
239
+ return "json";
240
+ }
241
+ async function importConfigModule(configPath) {
242
+ const absolutePath = resolve(configPath);
243
+ const fileUrl = pathToFileURL(absolutePath).href;
244
+ const module = await import(fileUrl);
245
+ const config = module.default ?? module;
246
+ return config;
247
+ }
248
+ function parseConfig(rawConfig, configPath) {
249
+ const issues = [];
250
+ const result = LlmsSeoConfigSchema.safeParse(rawConfig);
251
+ if (!result.success) {
252
+ const errorMessages = result.error.issues.map((e) => {
253
+ const path = e.path.join(".");
254
+ return `${path}: ${e.message}`;
255
+ }).join("; ");
256
+ throw new Error(`Invalid config at ${configPath}: ${errorMessages}`);
257
+ }
258
+ return { config: result.data, issues };
259
+ }
260
+ async function loadConfig(options) {
261
+ const { path: configPath } = options;
262
+ const absolutePath = resolve(configPath);
263
+ if (!existsSync(absolutePath)) {
264
+ throw new Error(`Config file not found: ${absolutePath}`);
265
+ }
266
+ const fileType = getConfigFileType(absolutePath);
267
+ let rawConfig;
268
+ if (fileType === "json") {
269
+ const content = await readFileSafe(absolutePath);
270
+ if (content === null) {
271
+ throw new Error(`Failed to read config file: ${absolutePath}`);
272
+ }
273
+ try {
274
+ rawConfig = JSON.parse(content);
275
+ } catch {
276
+ throw new Error(`Failed to parse JSON config: ${absolutePath}`);
277
+ }
278
+ } else {
279
+ try {
280
+ rawConfig = await importConfigModule(absolutePath);
281
+ } catch (error) {
282
+ const message = error instanceof Error ? error.message : String(error);
283
+ throw new Error(`Failed to import config module: ${absolutePath}
284
+ ${message}`);
285
+ }
286
+ }
287
+ const { config, issues } = parseConfig(rawConfig, absolutePath);
288
+ return {
289
+ config,
290
+ issues,
291
+ configPath: absolutePath
292
+ };
293
+ }
294
+ var CONFIG_FILE_NAMES;
295
+ var init_load_config = __esm({
296
+ "src/cli/io/load-config.ts"() {
297
+ init_config_schema();
298
+ init_fs();
299
+ CONFIG_FILE_NAMES = [
300
+ "llm-seo.config.ts",
301
+ "llm-seo.config.js",
302
+ "llm-seo.config.json"
303
+ ];
304
+ }
305
+ });
306
+
307
+ // src/cli/io/report.ts
308
+ function shouldUseColors() {
309
+ return process.env.NO_COLOR === void 0 && process.stdout.isTTY === true;
310
+ }
311
+ function color(text, colorCode) {
312
+ if (!shouldUseColors()) {
313
+ return text;
314
+ }
315
+ return `${colors[colorCode]}${text}${colors.reset}`;
316
+ }
317
+ function printError(message) {
318
+ console.error(color("[ERROR]", "red"), message);
319
+ }
320
+ function printWarning(message) {
321
+ console.warn(color("[WARN]", "yellow"), message);
322
+ }
323
+ function printInfo(message) {
324
+ console.log(color("[INFO]", "blue"), message);
325
+ }
326
+ function printVerbose(message) {
327
+ console.log(color(" [debug]", "gray"), message);
328
+ }
329
+ function printHeader(title) {
330
+ console.log("");
331
+ console.log(color(title, "bold"));
332
+ console.log("-".repeat(title.length));
333
+ }
334
+ function printCheckReport(result, verbose) {
335
+ console.log("");
336
+ printHeader("SEO Check Results");
337
+ const { summary, issues } = result;
338
+ console.log("");
339
+ console.log(`Files checked: ${summary.filesChecked}`);
340
+ console.log(`Files missing: ${summary.filesMissing}`);
341
+ console.log(`Files with issues: ${summary.filesMismatch}`);
342
+ console.log("");
343
+ console.log("Summary:");
344
+ const errorText = summary.errors > 0 ? color(`${summary.errors} error${summary.errors !== 1 ? "s" : ""}`, "red") : `${summary.errors} errors`;
345
+ const warningText = summary.warnings > 0 ? color(`${summary.warnings} warning${summary.warnings !== 1 ? "s" : ""}`, "yellow") : `${summary.warnings} warnings`;
346
+ const infoText = summary.info > 0 ? color(`${summary.info} info`, "blue") : `${summary.info} info`;
347
+ console.log(` ${errorText}, ${warningText}, ${infoText}`);
348
+ if (issues.length > 0) {
349
+ console.log("");
350
+ printHeader("Issues");
351
+ const issuesByFile = groupIssuesByFile(issues);
352
+ for (const [filePath, fileIssues] of issuesByFile) {
353
+ console.log("");
354
+ console.log(color(filePath, "cyan"));
355
+ for (const issue of fileIssues) {
356
+ printCheckIssue(issue, verbose);
357
+ }
358
+ }
359
+ }
360
+ console.log("");
361
+ if (summary.errors > 0) {
362
+ console.log(color("FAILED", "red"), "- Errors found");
363
+ } else if (summary.warnings > 0) {
364
+ console.log(color("PASSED WITH WARNINGS", "yellow"));
365
+ } else {
366
+ console.log(color("PASSED", "green"));
367
+ }
368
+ console.log("");
369
+ }
370
+ function printCheckIssue(issue, verbose) {
371
+ const severityIcon = issue.severity === "error" ? color("[ERROR]", "red") : issue.severity === "warning" ? color("[WARN]", "yellow") : color("[INFO]", "blue");
372
+ const lineInfo = issue.line !== void 0 ? `:${issue.line}` : "";
373
+ const prefix = ` ${severityIcon} ${issue.path}${lineInfo}`;
374
+ console.log(`${prefix}: ${issue.message}`);
375
+ if (verbose && issue.context) {
376
+ console.log(color(` Context: "${issue.context}"`, "gray"));
377
+ }
378
+ }
379
+ function groupIssuesByFile(issues) {
380
+ const grouped = /* @__PURE__ */ new Map();
381
+ for (const issue of issues) {
382
+ const existing = grouped.get(issue.path) ?? [];
383
+ existing.push(issue);
384
+ grouped.set(issue.path, existing);
385
+ }
386
+ return grouped;
387
+ }
388
+ function printGenerateReport(files, verbose) {
389
+ console.log("");
390
+ printHeader("Generation Complete");
391
+ console.log("");
392
+ console.log(`Generated ${files.length} file${files.length !== 1 ? "s" : ""}:`);
393
+ for (const file of files) {
394
+ const sizeStr = formatBytes(file.size);
395
+ console.log(` ${color("[OK]", "green")} ${file.path} (${sizeStr})`);
396
+ if (verbose) {
397
+ console.log(color(` Size: ${file.size} bytes`, "gray"));
398
+ }
399
+ }
400
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
401
+ console.log("");
402
+ console.log(`Total size: ${formatBytes(totalSize)}`);
403
+ console.log("");
404
+ }
405
+ function printDoctorReport(checks, verbose) {
406
+ console.log("");
407
+ printHeader("Diagnostic Results");
408
+ console.log("");
409
+ for (const check of checks) {
410
+ printDoctorCheck(check, verbose);
411
+ }
412
+ console.log("");
413
+ const errors = checks.filter((c) => c.status === "error").length;
414
+ const warnings = checks.filter((c) => c.status === "warn").length;
415
+ const ok = checks.filter((c) => c.status === "ok").length;
416
+ const skipped = checks.filter((c) => c.status === "skip").length;
417
+ console.log(
418
+ `${color("[OK]", "green")} ${ok} ok, ${color("[WARN]", "yellow")} ${warnings} warning${warnings !== 1 ? "s" : ""}, ${color("[ERROR]", "red")} ${errors} error${errors !== 1 ? "s" : ""}` + (skipped > 0 ? `, ${skipped} skipped` : "")
419
+ );
420
+ console.log("");
421
+ }
422
+ function printDoctorCheck(check, verbose) {
423
+ const icon = check.status === "ok" ? color("[OK]", "green") : check.status === "warn" ? color("[WARN]", "yellow") : check.status === "error" ? color("[ERROR]", "red") : color("[SKIP]", "gray");
424
+ const timeStr = check.responseTime !== void 0 ? ` (${check.responseTime}ms)` : "";
425
+ console.log(`${icon} ${check.name}: ${check.message}${timeStr}`);
426
+ if (verbose) {
427
+ console.log(color(` URL: ${check.url}`, "gray"));
428
+ }
429
+ }
430
+ function formatBytes(bytes) {
431
+ if (bytes === 0) return "0 B";
432
+ const units = ["B", "KB", "MB", "GB"];
433
+ const k = 1024;
434
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
435
+ const size = bytes / Math.pow(k, i);
436
+ if (i === 0) {
437
+ return `${bytes} B`;
438
+ }
439
+ const unit = units[i] ?? "B";
440
+ return `${size.toFixed(1)} ${unit}`;
441
+ }
442
+ function printDryRunHeader(fileName) {
443
+ console.log("");
444
+ console.log(color(`--- ${fileName} ---`, "cyan"));
445
+ }
446
+ function printSeparator() {
447
+ console.log(color("-".repeat(60), "gray"));
448
+ }
449
+ var colors;
450
+ var init_report = __esm({
451
+ "src/cli/io/report.ts"() {
452
+ colors = {
453
+ reset: "\x1B[0m",
454
+ red: "\x1B[31m",
455
+ green: "\x1B[32m",
456
+ yellow: "\x1B[33m",
457
+ blue: "\x1B[34m",
458
+ cyan: "\x1B[36m",
459
+ gray: "\x1B[90m",
460
+ bold: "\x1B[1m"
461
+ };
462
+ }
463
+ });
464
+
465
+ // src/core/normalize/url.ts
466
+ function normalizePath(path, preserveTrailingSlash = false) {
467
+ if (!path || path === "/") {
468
+ return "/";
469
+ }
470
+ const hadTrailingSlash = path.endsWith("/") && path !== "/";
471
+ let normalized = path.startsWith("/") ? path : `/${path}`;
472
+ normalized = normalized.replace(/\/{2,}/g, "/");
473
+ const segments = [];
474
+ const parts = normalized.split("/");
475
+ for (const part of parts) {
476
+ if (part === "." || part === "") {
477
+ continue;
478
+ } else if (part === "..") {
479
+ if (segments.length > 0) {
480
+ segments.pop();
481
+ }
482
+ } else {
483
+ segments.push(part);
484
+ }
485
+ }
486
+ let result = "/" + segments.join("/");
487
+ if (preserveTrailingSlash && hadTrailingSlash && result !== "/") {
488
+ result += "/";
489
+ }
490
+ return result;
491
+ }
492
+ function joinUrlParts(...parts) {
493
+ if (parts.length === 0) {
494
+ return "/";
495
+ }
496
+ const filteredParts = parts.filter((part) => part.length > 0);
497
+ if (filteredParts.length === 0) {
498
+ return "/";
499
+ }
500
+ const joined = filteredParts.map((part) => {
501
+ let p = part.replace(/^\/+/, "");
502
+ p = p.replace(/\/+$/, "");
503
+ return p;
504
+ }).filter((p) => p.length > 0).join("/");
505
+ return joined.length > 0 ? `/${joined}` : "/";
506
+ }
507
+ function normalizeUrl(options) {
508
+ const { baseUrl, path, trailingSlash, stripQuery = true, stripHash = true } = options;
509
+ let parsedBase;
510
+ try {
511
+ parsedBase = new URL(baseUrl);
512
+ } catch {
513
+ throw new TypeError(`Invalid baseUrl: ${baseUrl}`);
514
+ }
515
+ const shouldPreserveTrailingSlash = trailingSlash === "preserve";
516
+ const normalizedPath = normalizePath(path, shouldPreserveTrailingSlash);
517
+ let finalPath = normalizedPath;
518
+ if (trailingSlash === "always") {
519
+ if (!finalPath.endsWith("/")) {
520
+ finalPath = `${finalPath}/`;
521
+ }
522
+ } else if (trailingSlash === "never") {
523
+ if (finalPath !== "/" && finalPath.endsWith("/")) {
524
+ finalPath = finalPath.slice(0, -1);
525
+ }
526
+ }
527
+ const protocol = parsedBase.protocol.toLowerCase();
528
+ let hostname = parsedBase.hostname.toLowerCase();
529
+ let port = parsedBase.port;
530
+ if (port) {
531
+ const isDefaultPort = protocol === "http:" && port === "80" || protocol === "https:" && port === "443";
532
+ if (!isDefaultPort) {
533
+ hostname = `${hostname}:${port}`;
534
+ }
535
+ }
536
+ let fullUrl = `${protocol}//${hostname}${finalPath}`;
537
+ if (!stripQuery && parsedBase.search) {
538
+ fullUrl += parsedBase.search;
539
+ }
540
+ if (!stripHash && parsedBase.hash) {
541
+ fullUrl += parsedBase.hash;
542
+ }
543
+ return fullUrl;
544
+ }
545
+ var init_url = __esm({
546
+ "src/core/normalize/url.ts"() {
547
+ }
548
+ });
549
+
550
+ // src/core/normalize/sort.ts
551
+ function compareStrings(a, b) {
552
+ return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
553
+ }
554
+ function sortStrings(items) {
555
+ return [...items].sort(compareStrings);
556
+ }
557
+ function sortBy(items, keyFn) {
558
+ return [...items].sort((a, b) => compareStrings(keyFn(a), keyFn(b)));
559
+ }
560
+ function stableSortStrings(items) {
561
+ return [...items].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "case", numeric: true }));
562
+ }
563
+ function countPathSegments(url) {
564
+ try {
565
+ const parsed = new URL(url);
566
+ const path = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
567
+ if (!path) return 0;
568
+ return path.split("/").length;
569
+ } catch {
570
+ const cleaned = url.replace(/^\/+/, "").replace(/\/+$/, "");
571
+ if (!cleaned) return 0;
572
+ return cleaned.split("/").length;
573
+ }
574
+ }
575
+ function sortUrlsByPath(urls) {
576
+ return [...urls].sort((a, b) => {
577
+ const segmentsA = countPathSegments(a);
578
+ const segmentsB = countPathSegments(b);
579
+ if (segmentsA !== segmentsB) {
580
+ return segmentsA - segmentsB;
581
+ }
582
+ return a.localeCompare(b, "en", { sensitivity: "case", numeric: true });
583
+ });
584
+ }
585
+ var init_sort = __esm({
586
+ "src/core/normalize/sort.ts"() {
587
+ }
588
+ });
589
+
590
+ // src/core/normalize/text.ts
591
+ function normalizeLineEndings(text, lineEndings) {
592
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
593
+ return lineEndings === "crlf" ? normalized.replace(/\n/g, "\r\n") : normalized;
594
+ }
595
+ function normalizeLineWhitespace(text) {
596
+ const lines = text.split(/\r?\n/);
597
+ return lines.map((line) => line.trimEnd().replace(/[ \t]+/g, (match) => " ".repeat(match.length === 0 ? 0 : match.length))).map((line) => line.replace(/ +/g, " ")).join("\n");
598
+ }
599
+ var init_text = __esm({
600
+ "src/core/normalize/text.ts"() {
601
+ }
602
+ });
603
+
604
+ // src/core/canonical/locale.ts
605
+ function selectCanonicalLocale(options) {
606
+ const { defaultLocale, availableLocales } = options;
607
+ if (!availableLocales || availableLocales.length === 0) {
608
+ return null;
609
+ }
610
+ const validLocales = availableLocales.filter(
611
+ (locale) => typeof locale === "string" && locale.length > 0
612
+ );
613
+ if (validLocales.length === 0) {
614
+ return null;
615
+ }
616
+ if (defaultLocale && validLocales.includes(defaultLocale)) {
617
+ return defaultLocale;
618
+ }
619
+ const sorted = stableSortStrings(validLocales);
620
+ return sorted[0] ?? null;
621
+ }
622
+ var init_locale = __esm({
623
+ "src/core/canonical/locale.ts"() {
624
+ init_sort();
625
+ }
626
+ });
627
+
628
+ // src/core/canonical/canonical-from-manifest.ts
629
+ function dedupeUrls(urls) {
630
+ return [...new Set(urls)];
631
+ }
632
+ function buildLocalePrefix(locale, strategy, defaultLocale) {
633
+ if (strategy === "none") {
634
+ return "";
635
+ }
636
+ if (strategy === "subdomain") {
637
+ return "";
638
+ }
639
+ if (strategy === "prefix" && locale === defaultLocale) {
640
+ return "";
641
+ }
642
+ return `/${locale}`;
643
+ }
644
+ function buildBaseUrlWithSubdomain(baseUrl, locale, strategy, defaultLocale) {
645
+ if (strategy !== "subdomain" || locale === defaultLocale) {
646
+ return baseUrl;
647
+ }
648
+ try {
649
+ const parsed = new URL(baseUrl);
650
+ return `${parsed.protocol}//${locale}.${parsed.host}`;
651
+ } catch {
652
+ return baseUrl;
653
+ }
654
+ }
655
+ function createCanonicalUrlForItem(item, options) {
656
+ const { baseUrl, routePrefix, defaultLocale, trailingSlash, localeStrategy } = options;
657
+ if (item.canonicalOverride && typeof item.canonicalOverride === "string") {
658
+ return item.canonicalOverride;
659
+ }
660
+ const availableLocales = item.locales ?? [defaultLocale];
661
+ const canonicalLocale = selectCanonicalLocale({
662
+ defaultLocale,
663
+ availableLocales
664
+ });
665
+ const locale = canonicalLocale ?? defaultLocale;
666
+ const localePrefix = buildLocalePrefix(locale, localeStrategy, defaultLocale);
667
+ const effectiveBaseUrl = buildBaseUrlWithSubdomain(baseUrl, locale, localeStrategy, defaultLocale);
668
+ const parts = [];
669
+ if (localePrefix) {
670
+ parts.push(localePrefix);
671
+ }
672
+ if (routePrefix) {
673
+ parts.push(routePrefix);
674
+ }
675
+ parts.push(item.slug);
676
+ const fullPath = joinUrlParts(...parts);
677
+ return normalizeUrl({
678
+ baseUrl: effectiveBaseUrl,
679
+ path: fullPath,
680
+ trailingSlash,
681
+ stripQuery: true,
682
+ stripHash: true
683
+ });
684
+ }
685
+ function createCanonicalUrlsFromManifest(options) {
686
+ const { items } = options;
687
+ if (!items || items.length === 0) {
688
+ return [];
689
+ }
690
+ const urls = items.map((item) => createCanonicalUrlForItem(item, options));
691
+ const deduped = dedupeUrls(urls);
692
+ return sortUrlsByPath(deduped);
693
+ }
694
+ var init_canonical_from_manifest = __esm({
695
+ "src/core/canonical/canonical-from-manifest.ts"() {
696
+ init_url();
697
+ init_sort();
698
+ init_locale();
699
+ }
700
+ });
701
+
702
+ // src/core/generate/llms-txt.ts
703
+ function createLlmsTxt(options) {
704
+ const { config, canonicalUrls } = options;
705
+ const lineEndings = config.format?.lineEndings ?? "lf";
706
+ const lines = [];
707
+ lines.push(`# ${config.brand.name}`);
708
+ lines.push("");
709
+ if (config.brand.tagline) {
710
+ lines.push(`> ${config.brand.tagline}`);
711
+ lines.push("");
712
+ }
713
+ if (config.brand.description) {
714
+ lines.push(config.brand.description);
715
+ lines.push("");
716
+ }
717
+ const hubs = config.sections?.hubs ?? [];
718
+ if (hubs.length > 0) {
719
+ lines.push("## Sections");
720
+ lines.push("");
721
+ const sortedHubs = sortStrings(hubs);
722
+ for (const hub of sortedHubs) {
723
+ const hubLabel = getHubLabel(hub);
724
+ lines.push(`- [${hub}](${hub}) - ${hubLabel}`);
725
+ }
726
+ lines.push("");
727
+ }
728
+ if (canonicalUrls.length > 0) {
729
+ lines.push("## URLs");
730
+ lines.push("");
731
+ const sortedUrls = sortStrings(canonicalUrls);
732
+ for (const url of sortedUrls) {
733
+ lines.push(`- ${url}`);
734
+ }
735
+ lines.push("");
736
+ }
737
+ const hasPolicies = config.policy?.geoPolicy || config.policy?.citationRules || config.policy?.restrictedClaims;
738
+ if (hasPolicies) {
739
+ lines.push("## Policies");
740
+ lines.push("");
741
+ if (config.policy?.geoPolicy) {
742
+ lines.push(`- GEO: ${config.policy.geoPolicy}`);
743
+ }
744
+ if (config.policy?.citationRules) {
745
+ lines.push(`- Citations: ${config.policy.citationRules}`);
746
+ }
747
+ if (config.policy?.restrictedClaims) {
748
+ const status = config.policy.restrictedClaims.enable ? "Enabled" : "Disabled";
749
+ lines.push(`- Restricted Claims: ${status}`);
750
+ }
751
+ lines.push("");
752
+ }
753
+ const hasContact = config.contact?.email || config.contact?.social || config.contact?.phone;
754
+ const hasBooking = config.booking?.url;
755
+ if (hasContact || hasBooking) {
756
+ lines.push("## Contact");
757
+ lines.push("");
758
+ if (config.contact?.email) {
759
+ lines.push(`- Email: ${config.contact.email}`);
760
+ }
761
+ if (config.contact?.phone) {
762
+ lines.push(`- Phone: ${config.contact.phone}`);
763
+ }
764
+ if (config.contact?.social) {
765
+ if (config.contact.social.twitter) {
766
+ lines.push(`- Twitter: ${config.contact.social.twitter}`);
767
+ }
768
+ if (config.contact.social.linkedin) {
769
+ lines.push(`- LinkedIn: ${config.contact.social.linkedin}`);
770
+ }
771
+ if (config.contact.social.github) {
772
+ lines.push(`- GitHub: ${config.contact.social.github}`);
773
+ }
774
+ }
775
+ if (config.booking?.url) {
776
+ const label = config.booking.label ?? "Book consultation";
777
+ lines.push(`- Booking: ${config.booking.url} (${label})`);
778
+ }
779
+ lines.push("");
780
+ }
781
+ const hasMachineHints = config.machineHints?.robots || config.machineHints?.sitemap || config.machineHints?.llmsTxt || config.machineHints?.llmsFullTxt;
782
+ if (hasMachineHints) {
783
+ lines.push("## Machine Hints");
784
+ lines.push("");
785
+ if (config.machineHints?.robots) {
786
+ lines.push(`- robots.txt: ${config.machineHints.robots}`);
787
+ }
788
+ if (config.machineHints?.sitemap) {
789
+ lines.push(`- sitemap.xml: ${config.machineHints.sitemap}`);
790
+ }
791
+ if (config.machineHints?.llmsTxt) {
792
+ lines.push(`- llms.txt: ${config.machineHints.llmsTxt}`);
793
+ }
794
+ if (config.machineHints?.llmsFullTxt) {
795
+ lines.push(`- llms-full.txt: ${config.machineHints.llmsFullTxt}`);
796
+ }
797
+ lines.push("");
798
+ }
799
+ let content = lines.join("\n");
800
+ content = normalizeLineWhitespace(content);
801
+ content = normalizeLineEndings(content, lineEndings);
802
+ const finalLines = content.split(lineEndings === "crlf" ? "\r\n" : "\n");
803
+ return {
804
+ content,
805
+ byteSize: Buffer.byteLength(content, "utf-8"),
806
+ lineCount: finalLines.length
807
+ };
808
+ }
809
+ function getHubLabel(hub) {
810
+ const labels = {
811
+ "/services": "Services overview",
812
+ "/blog": "Blog posts",
813
+ "/projects": "Our projects",
814
+ "/cases": "Case studies",
815
+ "/contact": "Contact us",
816
+ "/about": "About us",
817
+ "/products": "Products",
818
+ "/docs": "Documentation",
819
+ "/faq": "Frequently asked questions",
820
+ "/pricing": "Pricing information",
821
+ "/team": "Our team",
822
+ "/careers": "Career opportunities",
823
+ "/news": "News and updates",
824
+ "/resources": "Resources",
825
+ "/support": "Support center"
826
+ };
827
+ return labels[hub] ?? formatHubLabel(hub);
828
+ }
829
+ function formatHubLabel(hub) {
830
+ const clean = hub.replace(/^\//, "");
831
+ return clean.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
832
+ }
833
+ var init_llms_txt = __esm({
834
+ "src/core/generate/llms-txt.ts"() {
835
+ init_sort();
836
+ init_text();
837
+ }
838
+ });
839
+
840
+ // src/core/generate/llms-full-txt.ts
841
+ function createLlmsFullTxt(options) {
842
+ const { config, canonicalUrls, manifestItems } = options;
843
+ const lineEndings = config.format?.lineEndings ?? "lf";
844
+ const lines = [];
845
+ lines.push(`# ${config.brand.name} - Full LLM Context`);
846
+ lines.push("");
847
+ if (config.brand.tagline) {
848
+ lines.push(`> ${config.brand.tagline}`);
849
+ lines.push("");
850
+ }
851
+ if (config.brand.description) {
852
+ lines.push(config.brand.description);
853
+ lines.push("");
854
+ }
855
+ if (config.brand.org) {
856
+ lines.push(`Organization: ${config.brand.org}`);
857
+ }
858
+ lines.push(`Locales: ${config.brand.locales.join(", ")}`);
859
+ lines.push("");
860
+ if (canonicalUrls.length > 0) {
861
+ lines.push("## All Canonical URLs");
862
+ lines.push("");
863
+ const sortedUrls = sortStrings(canonicalUrls);
864
+ for (const url of sortedUrls) {
865
+ lines.push(`- ${url}`);
866
+ }
867
+ lines.push("");
868
+ }
869
+ const hasPolicies = config.policy?.geoPolicy || config.policy?.citationRules || config.policy?.restrictedClaims;
870
+ if (hasPolicies) {
871
+ lines.push("## Policies");
872
+ lines.push("");
873
+ if (config.policy?.geoPolicy) {
874
+ lines.push("### GEO Policy");
875
+ lines.push(config.policy.geoPolicy);
876
+ lines.push("");
877
+ }
878
+ if (config.policy?.citationRules) {
879
+ lines.push("### Citation Rules");
880
+ lines.push(config.policy.citationRules);
881
+ lines.push("");
882
+ }
883
+ if (config.policy?.restrictedClaims) {
884
+ lines.push("### Restricted Claims");
885
+ const status = config.policy.restrictedClaims.enable ? "Enabled" : "Disabled";
886
+ lines.push(`Status: ${status}`);
887
+ if (config.policy.restrictedClaims.forbidden && config.policy.restrictedClaims.forbidden.length > 0) {
888
+ lines.push(`Forbidden terms: ${config.policy.restrictedClaims.forbidden.join(", ")}`);
889
+ }
890
+ if (config.policy.restrictedClaims.whitelist && config.policy.restrictedClaims.whitelist.length > 0) {
891
+ lines.push(`Exceptions: ${config.policy.restrictedClaims.whitelist.join(", ")}`);
892
+ }
893
+ lines.push("");
894
+ }
895
+ }
896
+ const hasSocial = config.contact?.social?.twitter || config.contact?.social?.linkedin || config.contact?.social?.github;
897
+ const hasBooking = config.booking?.url;
898
+ if (hasSocial || hasBooking) {
899
+ lines.push("## Social & Booking");
900
+ lines.push("");
901
+ if (config.contact?.social?.twitter) {
902
+ lines.push(`- Twitter: ${config.contact.social.twitter}`);
903
+ }
904
+ if (config.contact?.social?.linkedin) {
905
+ lines.push(`- LinkedIn: ${config.contact.social.linkedin}`);
906
+ }
907
+ if (config.contact?.social?.github) {
908
+ lines.push(`- GitHub: ${config.contact.social.github}`);
909
+ }
910
+ if (config.booking?.url) {
911
+ const label = config.booking.label ?? "Book consultation";
912
+ lines.push(`- Booking: ${config.booking.url} (${label})`);
913
+ }
914
+ lines.push("");
915
+ }
916
+ const hasMachineHints = config.machineHints?.robots || config.machineHints?.sitemap || config.machineHints?.llmsTxt || config.machineHints?.llmsFullTxt;
917
+ if (hasMachineHints) {
918
+ lines.push("## Machine Hints");
919
+ lines.push("");
920
+ if (config.machineHints?.robots) {
921
+ lines.push(`- robots.txt: ${config.machineHints.robots}`);
922
+ }
923
+ if (config.machineHints?.sitemap) {
924
+ lines.push(`- sitemap.xml: ${config.machineHints.sitemap}`);
925
+ }
926
+ if (config.machineHints?.llmsTxt) {
927
+ lines.push(`- llms.txt: ${config.machineHints.llmsTxt}`);
928
+ }
929
+ if (config.machineHints?.llmsFullTxt) {
930
+ lines.push(`- llms-full.txt: ${config.machineHints.llmsFullTxt}`);
931
+ }
932
+ lines.push("");
933
+ }
934
+ const hubs = config.sections?.hubs ?? [];
935
+ if (hubs.length > 0 || manifestItems.length > 0) {
936
+ lines.push("## Sitemap");
937
+ lines.push("");
938
+ if (hubs.length > 0) {
939
+ const sortedHubs = sortStrings(hubs);
940
+ for (const hub of sortedHubs) {
941
+ lines.push(`- [${hub}](${hub}) - ${getHubLabel2(hub)}`);
942
+ }
943
+ }
944
+ if (manifestItems.length > 0) {
945
+ const sortedItems = sortBy(manifestItems, (item) => item.slug);
946
+ for (const item of sortedItems) {
947
+ const url = item.canonicalOverride ?? `${config.site.baseUrl}${item.slug}`;
948
+ const title = item.title ?? item.slug;
949
+ const locales = item.locales?.join(", ") ?? config.brand.locales[0] ?? "en";
950
+ lines.push(`- [${title}](${url}) (${locales})`);
951
+ }
952
+ }
953
+ lines.push("");
954
+ }
955
+ let content = lines.join("\n");
956
+ content = normalizeLineWhitespace(content);
957
+ content = normalizeLineEndings(content, lineEndings);
958
+ const finalLines = content.split(lineEndings === "crlf" ? "\r\n" : "\n");
959
+ return {
960
+ content,
961
+ byteSize: Buffer.byteLength(content, "utf-8"),
962
+ lineCount: finalLines.length
963
+ };
964
+ }
965
+ function getHubLabel2(hub) {
966
+ const labels = {
967
+ "/services": "Services overview",
968
+ "/blog": "Blog posts",
969
+ "/projects": "Our projects",
970
+ "/cases": "Case studies",
971
+ "/contact": "Contact us",
972
+ "/about": "About us",
973
+ "/products": "Products",
974
+ "/docs": "Documentation",
975
+ "/faq": "Frequently asked questions",
976
+ "/pricing": "Pricing information",
977
+ "/team": "Our team",
978
+ "/careers": "Career opportunities",
979
+ "/news": "News and updates",
980
+ "/resources": "Resources",
981
+ "/support": "Support center"
982
+ };
983
+ return labels[hub] ?? formatHubLabel2(hub);
984
+ }
985
+ function formatHubLabel2(hub) {
986
+ const clean = hub.replace(/^\//, "");
987
+ return clean.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
988
+ }
989
+ var init_llms_full_txt = __esm({
990
+ "src/core/generate/llms-full-txt.ts"() {
991
+ init_sort();
992
+ init_text();
993
+ }
994
+ });
995
+
996
+ // src/core/generate/citations.ts
997
+ function createCitationsJson(options) {
998
+ const { config, manifestItems, sectionName, fixedTimestamp } = options;
999
+ const sources = manifestItems.map((item) => {
1000
+ const url = item.canonicalOverride ?? `${config.site.baseUrl}${item.slug}`;
1001
+ const defaultLocale = config.site.defaultLocale ?? config.brand.locales[0] ?? "en";
1002
+ return {
1003
+ url,
1004
+ priority: item.priority ?? 50,
1005
+ section: sectionName,
1006
+ locale: item.locales?.[0] ?? defaultLocale,
1007
+ ...item.publishedAt && { publishedAt: item.publishedAt },
1008
+ ...item.updatedAt && { updatedAt: item.updatedAt },
1009
+ ...item.title && { title: item.title }
1010
+ };
1011
+ });
1012
+ const sortedSources = sources.sort((a, b) => {
1013
+ if (a.priority !== b.priority) {
1014
+ return b.priority - a.priority;
1015
+ }
1016
+ return a.url.localeCompare(b.url, "en", { sensitivity: "case", numeric: true });
1017
+ });
1018
+ const policy = {
1019
+ restrictedClaimsEnabled: config.policy?.restrictedClaims?.enable ?? false,
1020
+ ...config.policy?.geoPolicy && { geoPolicy: config.policy.geoPolicy },
1021
+ ...config.policy?.citationRules && { citationRules: config.policy.citationRules }
1022
+ };
1023
+ return {
1024
+ version: "1.0",
1025
+ generated: fixedTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1026
+ site: {
1027
+ baseUrl: config.site.baseUrl,
1028
+ name: config.brand.name
1029
+ },
1030
+ sources: sortedSources,
1031
+ policy
1032
+ };
1033
+ }
1034
+ function createCitationsJsonString(options) {
1035
+ const citations = createCitationsJson(options);
1036
+ return JSON.stringify(citations, null, 2);
1037
+ }
1038
+ var init_citations = __esm({
1039
+ "src/core/generate/citations.ts"() {
1040
+ }
1041
+ });
1042
+
1043
+ // src/core/check/issues.ts
1044
+ function createCheckIssue(severity, code, message, path = "", context) {
1045
+ const issue = {
1046
+ path,
1047
+ code,
1048
+ message,
1049
+ severity
1050
+ };
1051
+ if (context !== void 0) {
1052
+ issue.context = context;
1053
+ }
1054
+ return issue;
1055
+ }
1056
+ function createIssue(overrides) {
1057
+ return {
1058
+ category: "content",
1059
+ ...overrides
1060
+ };
1061
+ }
1062
+ function countSeverities(issues) {
1063
+ const counts = {
1064
+ error: 0,
1065
+ warning: 0,
1066
+ info: 0
1067
+ };
1068
+ for (const issue of issues) {
1069
+ counts[issue.severity]++;
1070
+ }
1071
+ return counts;
1072
+ }
1073
+ var init_issues = __esm({
1074
+ "src/core/check/issues.ts"() {
1075
+ }
1076
+ });
1077
+
1078
+ // src/core/check/rules-linter.ts
1079
+ function lintContent(content, filePath, rules = LINT_RULES.filter((r) => r.enabled)) {
1080
+ const issues = [];
1081
+ for (const rule of rules) {
1082
+ const ruleIssues = rule.lint(content, filePath);
1083
+ issues.push(...ruleIssues);
1084
+ }
1085
+ return {
1086
+ filePath,
1087
+ issues,
1088
+ passed: issues.filter((i) => i.severity === "error").length === 0
1089
+ };
1090
+ }
1091
+ function checkForbiddenTerms(content, forbidden, whitelist = []) {
1092
+ const issues = [];
1093
+ const lines = content.split("\n");
1094
+ const whitelistLower = whitelist.map((w) => w.toLowerCase());
1095
+ for (let i = 0; i < lines.length; i++) {
1096
+ const line = lines[i];
1097
+ if (line === void 0) continue;
1098
+ const trimmed = line.trim();
1099
+ const loweredTrimmed = trimmed.toLowerCase();
1100
+ if (loweredTrimmed.startsWith("forbidden terms:") || loweredTrimmed.startsWith("exceptions:")) {
1101
+ continue;
1102
+ }
1103
+ for (const term of forbidden) {
1104
+ const termLower = term.toLowerCase();
1105
+ const lineLower = line.toLowerCase();
1106
+ if (lineLower.includes(termLower)) {
1107
+ const isWhitelisted = whitelistLower.some(
1108
+ (w) => lineLower.includes(w) && w.includes(termLower)
1109
+ );
1110
+ if (!isWhitelisted) {
1111
+ issues.push({
1112
+ path: "",
1113
+ code: "forbidden_term",
1114
+ message: `Term "${term}" is forbidden by policy`,
1115
+ severity: "warning",
1116
+ line: i + 1,
1117
+ context: trimmed.substring(0, 100)
1118
+ });
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ return issues;
1124
+ }
1125
+ function checkEmptySections(content) {
1126
+ const issues = [];
1127
+ const lines = content.split("\n");
1128
+ let currentSection = null;
1129
+ let sectionStartLine = 0;
1130
+ let sectionHeadingLevel = 0;
1131
+ let sectionHasContent = false;
1132
+ let sectionWasFirst = false;
1133
+ let isFirstHeading = true;
1134
+ for (let i = 0; i < lines.length; i++) {
1135
+ const line = lines[i];
1136
+ if (line === void 0) continue;
1137
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1138
+ if (headingMatch?.[2]) {
1139
+ const nextHeadingLevel = headingMatch[1]?.length ?? 0;
1140
+ if (currentSection !== null && nextHeadingLevel > sectionHeadingLevel) {
1141
+ sectionHasContent = true;
1142
+ }
1143
+ if (currentSection !== null && !sectionHasContent && !sectionWasFirst) {
1144
+ issues.push({
1145
+ path: "",
1146
+ code: "empty_section",
1147
+ message: `Section "${currentSection}" has no content`,
1148
+ severity: "info",
1149
+ line: sectionStartLine
1150
+ });
1151
+ }
1152
+ currentSection = headingMatch[2].trim();
1153
+ sectionStartLine = i + 1;
1154
+ sectionHeadingLevel = nextHeadingLevel;
1155
+ sectionHasContent = false;
1156
+ sectionWasFirst = isFirstHeading;
1157
+ isFirstHeading = false;
1158
+ } else if (currentSection !== null) {
1159
+ if (line.trim().length > 0) {
1160
+ sectionHasContent = true;
1161
+ }
1162
+ }
1163
+ }
1164
+ if (currentSection !== null && !sectionHasContent && !sectionWasFirst) {
1165
+ issues.push({
1166
+ path: "",
1167
+ code: "empty_section",
1168
+ message: `Section "${currentSection}" has no content`,
1169
+ severity: "info",
1170
+ line: sectionStartLine
1171
+ });
1172
+ }
1173
+ return issues;
1174
+ }
1175
+ function checkDuplicateUrls(content) {
1176
+ const issues = [];
1177
+ const lines = content.split("\n");
1178
+ const seenUrls = /* @__PURE__ */ new Map();
1179
+ const urlPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
1180
+ for (let i = 0; i < lines.length; i++) {
1181
+ const line = lines[i];
1182
+ if (line === void 0) continue;
1183
+ let match = urlPattern.exec(line);
1184
+ while (match !== null) {
1185
+ const url = match[2];
1186
+ if (url !== void 0) {
1187
+ const firstOccurrence = seenUrls.get(url);
1188
+ if (firstOccurrence !== void 0) {
1189
+ issues.push({
1190
+ path: "",
1191
+ code: "duplicate_url",
1192
+ message: `URL "${url}" appears multiple times (first at line ${firstOccurrence})`,
1193
+ severity: "warning",
1194
+ line: i + 1,
1195
+ context: url
1196
+ });
1197
+ } else {
1198
+ seenUrls.set(url, i + 1);
1199
+ }
1200
+ }
1201
+ match = urlPattern.exec(line);
1202
+ }
1203
+ urlPattern.lastIndex = 0;
1204
+ }
1205
+ return issues;
1206
+ }
1207
+ function lintHeadingStructure(content, filePath) {
1208
+ const issues = [];
1209
+ const lines = content.split("\n");
1210
+ let prevLevel = 0;
1211
+ for (let i = 0; i < lines.length; i++) {
1212
+ const line = lines[i];
1213
+ if (line === void 0) continue;
1214
+ const match = line.match(/^(#{1,6})\s/);
1215
+ if (match?.[1]) {
1216
+ const level = match[1].length;
1217
+ if (level > prevLevel + 1 && prevLevel > 0) {
1218
+ issues.push(createIssue({
1219
+ id: "heading-skip",
1220
+ pageId: filePath,
1221
+ severity: "warning",
1222
+ message: `Heading level skipped: h${prevLevel} to h${level}`,
1223
+ line: i + 1
1224
+ }));
1225
+ }
1226
+ prevLevel = level;
1227
+ }
1228
+ }
1229
+ return issues;
1230
+ }
1231
+ function lintUrlFormat(content, filePath) {
1232
+ const issues = [];
1233
+ const lines = content.split("\n");
1234
+ const urlPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
1235
+ for (let i = 0; i < lines.length; i++) {
1236
+ const line = lines[i];
1237
+ if (line === void 0) continue;
1238
+ let match = urlPattern.exec(line);
1239
+ while (match !== null) {
1240
+ const url = match[2];
1241
+ if (url && !url.startsWith("/") && !url.startsWith("http") && !url.startsWith("#")) {
1242
+ issues.push(createIssue({
1243
+ id: "invalid-url",
1244
+ pageId: filePath,
1245
+ severity: "warning",
1246
+ message: `Invalid URL format: ${url}`,
1247
+ line: i + 1,
1248
+ column: match.index + 1
1249
+ }));
1250
+ }
1251
+ match = urlPattern.exec(line);
1252
+ }
1253
+ }
1254
+ return issues;
1255
+ }
1256
+ function lintTrailingWhitespace(content, filePath) {
1257
+ const issues = [];
1258
+ const lines = content.split("\n");
1259
+ for (let i = 0; i < lines.length; i++) {
1260
+ const line = lines[i];
1261
+ if (line !== void 0 && line.endsWith(" ")) {
1262
+ issues.push(createIssue({
1263
+ id: "trailing-whitespace",
1264
+ pageId: filePath,
1265
+ severity: "info",
1266
+ message: "Line has trailing whitespace",
1267
+ line: i + 1
1268
+ }));
1269
+ }
1270
+ }
1271
+ return issues;
1272
+ }
1273
+ function lintListMarkers(content, filePath) {
1274
+ const issues = [];
1275
+ const lines = content.split("\n");
1276
+ const dashCount = lines.filter((l) => /^\s*-\s/.test(l)).length;
1277
+ const asteriskCount = lines.filter((l) => /^\s*\*\s/.test(l)).length;
1278
+ if (dashCount > 0 && asteriskCount > 0) {
1279
+ issues.push(createIssue({
1280
+ id: "inconsistent-list-markers",
1281
+ pageId: filePath,
1282
+ severity: "info",
1283
+ message: "Mix of - and * list markers detected"
1284
+ }));
1285
+ }
1286
+ return issues;
1287
+ }
1288
+ var LINT_RULES;
1289
+ var init_rules_linter = __esm({
1290
+ "src/core/check/rules-linter.ts"() {
1291
+ init_issues();
1292
+ LINT_RULES = [
1293
+ {
1294
+ id: "heading-structure",
1295
+ description: "Ensures proper heading structure (h1 -> h2 -> h3)",
1296
+ enabled: true,
1297
+ lint: lintHeadingStructure
1298
+ },
1299
+ {
1300
+ id: "url-format",
1301
+ description: "Validates URL format in links",
1302
+ enabled: true,
1303
+ lint: lintUrlFormat
1304
+ },
1305
+ {
1306
+ id: "trailing-whitespace",
1307
+ description: "Checks for trailing whitespace on lines",
1308
+ enabled: true,
1309
+ lint: lintTrailingWhitespace
1310
+ },
1311
+ {
1312
+ id: "consistent-list-markers",
1313
+ description: "Ensures consistent list marker usage",
1314
+ enabled: true,
1315
+ lint: lintListMarkers
1316
+ }
1317
+ ];
1318
+ }
1319
+ });
1320
+ async function checkGeneratedFiles(options) {
1321
+ const issues = [];
1322
+ let filesChecked = 0;
1323
+ let filesMissing = 0;
1324
+ let filesMismatch = 0;
1325
+ const { config, failOn } = options;
1326
+ const llmsTxtPath = options.llmsTxtPath ?? config.output.paths.llmsTxt;
1327
+ const llmsFullTxtPath = options.llmsFullTxtPath ?? config.output.paths.llmsFullTxt;
1328
+ const citationsPath = options.citationsPath ?? config.output.paths.citations;
1329
+ const llmsTxtResult = await checkFile(llmsTxtPath);
1330
+ if (!llmsTxtResult.exists) {
1331
+ issues.push(createCheckIssue(
1332
+ "error",
1333
+ "file_missing",
1334
+ `Required file does not exist: ${llmsTxtPath}`,
1335
+ llmsTxtPath
1336
+ ));
1337
+ filesMissing++;
1338
+ } else if (llmsTxtResult.content === "") {
1339
+ issues.push(createCheckIssue(
1340
+ "warning",
1341
+ "file_empty",
1342
+ `File is empty: ${llmsTxtPath}`,
1343
+ llmsTxtPath
1344
+ ));
1345
+ filesChecked++;
1346
+ } else {
1347
+ filesChecked++;
1348
+ const lintIssues = await lintFile(llmsTxtPath, llmsTxtResult.content, config);
1349
+ issues.push(...lintIssues);
1350
+ }
1351
+ const llmsFullTxtResult = await checkFile(llmsFullTxtPath);
1352
+ if (!llmsFullTxtResult.exists) {
1353
+ issues.push(createCheckIssue(
1354
+ "error",
1355
+ "file_missing",
1356
+ `Required file does not exist: ${llmsFullTxtPath}`,
1357
+ llmsFullTxtPath
1358
+ ));
1359
+ filesMissing++;
1360
+ } else if (llmsFullTxtResult.content === "") {
1361
+ issues.push(createCheckIssue(
1362
+ "warning",
1363
+ "file_empty",
1364
+ `File is empty: ${llmsFullTxtPath}`,
1365
+ llmsFullTxtPath
1366
+ ));
1367
+ filesChecked++;
1368
+ } else {
1369
+ filesChecked++;
1370
+ const lintIssues = await lintFile(llmsFullTxtPath, llmsFullTxtResult.content, config);
1371
+ issues.push(...lintIssues);
1372
+ }
1373
+ if (citationsPath) {
1374
+ const citationsResult = await checkFile(citationsPath);
1375
+ if (!citationsResult.exists) {
1376
+ issues.push(createCheckIssue(
1377
+ "warning",
1378
+ "file_missing",
1379
+ `Optional citations file does not exist: ${citationsPath}`,
1380
+ citationsPath
1381
+ ));
1382
+ filesMissing++;
1383
+ } else {
1384
+ filesChecked++;
1385
+ }
1386
+ }
1387
+ const severityCounts = countSeverities(issues);
1388
+ let exitCode;
1389
+ if (severityCounts.error > 0) {
1390
+ exitCode = 2;
1391
+ } else if (failOn === "warn" && severityCounts.warning > 0) {
1392
+ exitCode = 1;
1393
+ } else {
1394
+ exitCode = 0;
1395
+ }
1396
+ return {
1397
+ issues,
1398
+ summary: {
1399
+ errors: severityCounts.error,
1400
+ warnings: severityCounts.warning,
1401
+ info: severityCounts.info,
1402
+ filesChecked,
1403
+ filesMissing,
1404
+ filesMismatch
1405
+ },
1406
+ exitCode
1407
+ };
1408
+ }
1409
+ async function checkFile(filePath) {
1410
+ try {
1411
+ const content = await fs.readFile(filePath, "utf-8");
1412
+ return { exists: true, content };
1413
+ } catch {
1414
+ return { exists: false, content: "" };
1415
+ }
1416
+ }
1417
+ async function readFileContent(filePath) {
1418
+ try {
1419
+ return await fs.readFile(filePath, "utf-8");
1420
+ } catch {
1421
+ return null;
1422
+ }
1423
+ }
1424
+ function compareContent(expected, actual, maxContextLines = 5) {
1425
+ if (expected === actual) {
1426
+ return { match: true, context: "" };
1427
+ }
1428
+ const expectedLines = expected.split("\n");
1429
+ const actualLines = actual.split("\n");
1430
+ const contextLines = [];
1431
+ let diffCount = 0;
1432
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
1433
+ for (let i = 0; i < maxLines && diffCount < maxContextLines; i++) {
1434
+ const expectedLine = expectedLines[i];
1435
+ const actualLine = actualLines[i];
1436
+ if (expectedLine !== actualLine) {
1437
+ diffCount++;
1438
+ const lineNum = i + 1;
1439
+ if (expectedLine !== void 0) {
1440
+ contextLines.push(`Expected line ${lineNum}: "${expectedLine}"`);
1441
+ }
1442
+ if (actualLine !== void 0) {
1443
+ contextLines.push(`Actual line ${lineNum}: "${actualLine}"`);
1444
+ }
1445
+ }
1446
+ }
1447
+ return {
1448
+ match: false,
1449
+ context: contextLines.join("\n")
1450
+ };
1451
+ }
1452
+ async function lintFile(filePath, content, config) {
1453
+ const issues = [];
1454
+ const lintResult = lintContent(content, filePath);
1455
+ for (const issue of lintResult.issues) {
1456
+ const checkIssue = {
1457
+ path: filePath,
1458
+ code: issue.id,
1459
+ message: issue.message,
1460
+ severity: issue.severity
1461
+ };
1462
+ if (issue.line !== void 0) {
1463
+ checkIssue.line = issue.line;
1464
+ }
1465
+ if (issue.suggestion !== void 0) {
1466
+ checkIssue.context = issue.suggestion;
1467
+ }
1468
+ issues.push(checkIssue);
1469
+ }
1470
+ if (config.policy?.restrictedClaims?.enable) {
1471
+ const forbidden = config.policy.restrictedClaims.forbidden ?? [];
1472
+ const whitelist = config.policy.restrictedClaims.whitelist ?? [];
1473
+ const forbiddenIssues = checkForbiddenTerms(content, forbidden, whitelist);
1474
+ for (const issue of forbiddenIssues) {
1475
+ const checkIssue = {
1476
+ path: filePath,
1477
+ code: "forbidden_term",
1478
+ message: issue.message,
1479
+ severity: issue.severity
1480
+ };
1481
+ if (issue.line !== void 0) {
1482
+ checkIssue.line = issue.line;
1483
+ }
1484
+ if (issue.context !== void 0) {
1485
+ checkIssue.context = issue.context;
1486
+ }
1487
+ issues.push(checkIssue);
1488
+ }
1489
+ }
1490
+ const emptySectionIssues = checkEmptySections(content);
1491
+ for (const issue of emptySectionIssues) {
1492
+ const checkIssue = {
1493
+ path: filePath,
1494
+ code: "empty_section",
1495
+ message: issue.message,
1496
+ severity: issue.severity
1497
+ };
1498
+ if (issue.line !== void 0) {
1499
+ checkIssue.line = issue.line;
1500
+ }
1501
+ if (issue.context !== void 0) {
1502
+ checkIssue.context = issue.context;
1503
+ }
1504
+ issues.push(checkIssue);
1505
+ }
1506
+ const duplicateUrlIssues = checkDuplicateUrls(content);
1507
+ for (const issue of duplicateUrlIssues) {
1508
+ const checkIssue = {
1509
+ path: filePath,
1510
+ code: "duplicate_url",
1511
+ message: issue.message,
1512
+ severity: issue.severity
1513
+ };
1514
+ if (issue.line !== void 0) {
1515
+ checkIssue.line = issue.line;
1516
+ }
1517
+ if (issue.context !== void 0) {
1518
+ checkIssue.context = issue.context;
1519
+ }
1520
+ issues.push(checkIssue);
1521
+ }
1522
+ return issues;
1523
+ }
1524
+ async function checkFilesAgainstExpected(llmsTxtPath, expectedLlmsTxt, llmsFullTxtPath, expectedLlmsFullTxt, maxContextLines = 5) {
1525
+ const issues = [];
1526
+ const llmsTxtContent = await readFileContent(llmsTxtPath);
1527
+ if (llmsTxtContent === null) {
1528
+ issues.push(createCheckIssue(
1529
+ "error",
1530
+ "file_missing",
1531
+ `Required file does not exist: ${llmsTxtPath}`,
1532
+ llmsTxtPath
1533
+ ));
1534
+ } else if (llmsTxtContent === "") {
1535
+ issues.push(createCheckIssue(
1536
+ "warning",
1537
+ "file_empty",
1538
+ `File is empty: ${llmsTxtPath}`,
1539
+ llmsTxtPath
1540
+ ));
1541
+ } else {
1542
+ const compareResult = compareContent(expectedLlmsTxt, llmsTxtContent, maxContextLines);
1543
+ if (!compareResult.match) {
1544
+ issues.push(createCheckIssue(
1545
+ "error",
1546
+ "file_mismatch",
1547
+ `Content differs from expected output`,
1548
+ llmsTxtPath,
1549
+ compareResult.context
1550
+ ));
1551
+ }
1552
+ }
1553
+ const llmsFullTxtContent = await readFileContent(llmsFullTxtPath);
1554
+ if (llmsFullTxtContent === null) {
1555
+ issues.push(createCheckIssue(
1556
+ "error",
1557
+ "file_missing",
1558
+ `Required file does not exist: ${llmsFullTxtPath}`,
1559
+ llmsFullTxtPath
1560
+ ));
1561
+ } else if (llmsFullTxtContent === "") {
1562
+ issues.push(createCheckIssue(
1563
+ "warning",
1564
+ "file_empty",
1565
+ `File is empty: ${llmsFullTxtPath}`,
1566
+ llmsFullTxtPath
1567
+ ));
1568
+ } else {
1569
+ const compareResult = compareContent(expectedLlmsFullTxt, llmsFullTxtContent, maxContextLines);
1570
+ if (!compareResult.match) {
1571
+ issues.push(createCheckIssue(
1572
+ "error",
1573
+ "file_mismatch",
1574
+ `Content differs from expected output`,
1575
+ llmsFullTxtPath,
1576
+ compareResult.context
1577
+ ));
1578
+ }
1579
+ }
1580
+ return issues;
1581
+ }
1582
+ var init_checker = __esm({
1583
+ "src/core/check/checker.ts"() {
1584
+ init_issues();
1585
+ init_rules_linter();
1586
+ }
1587
+ });
1588
+
1589
+ // src/core/index.ts
1590
+ var init_core = __esm({
1591
+ "src/core/index.ts"() {
1592
+ init_url();
1593
+ init_sort();
1594
+ init_text();
1595
+ init_canonical_from_manifest();
1596
+ init_locale();
1597
+ init_llms_txt();
1598
+ init_llms_full_txt();
1599
+ init_citations();
1600
+ init_checker();
1601
+ init_issues();
1602
+ init_rules_linter();
1603
+ }
1604
+ });
1605
+
1606
+ // src/cli/commands/generate.ts
1607
+ var generate_exports = {};
1608
+ __export(generate_exports, {
1609
+ generateCommand: () => generateCommand
1610
+ });
1611
+ async function generateCommand(options) {
1612
+ const { config: configPath, dryRun, emitCitations, verbose } = options;
1613
+ try {
1614
+ if (verbose) {
1615
+ printVerbose(`Loading config from: ${configPath}`);
1616
+ }
1617
+ let loadResult;
1618
+ try {
1619
+ loadResult = await loadConfig({ path: configPath });
1620
+ } catch (error) {
1621
+ const message = error instanceof Error ? error.message : String(error);
1622
+ printError(`Failed to load config: ${message}`);
1623
+ return ExitCodes.CONFIG_NOT_FOUND;
1624
+ }
1625
+ const { config } = loadResult;
1626
+ for (const issue of loadResult.issues) {
1627
+ if (issue.severity === "warning") {
1628
+ printWarning(issue.message);
1629
+ } else {
1630
+ printError(issue.message);
1631
+ return ExitCodes.INVALID_CONFIG;
1632
+ }
1633
+ }
1634
+ if (verbose) {
1635
+ printVerbose(`Config loaded successfully`);
1636
+ printVerbose(`Site: ${config.site.baseUrl}`);
1637
+ printVerbose(`Brand: ${config.brand.name}`);
1638
+ }
1639
+ const manifestItems = extractManifestItems(config);
1640
+ if (verbose) {
1641
+ printVerbose(`Found ${manifestItems.length} manifest items`);
1642
+ }
1643
+ const canonicalUrls = createCanonicalUrlsFromManifest({
1644
+ items: manifestItems,
1645
+ baseUrl: config.site.baseUrl,
1646
+ defaultLocale: config.site.defaultLocale ?? config.brand.locales[0] ?? "en",
1647
+ trailingSlash: config.format?.trailingSlash ?? "never",
1648
+ localeStrategy: "prefix"
1649
+ });
1650
+ if (verbose) {
1651
+ printVerbose(`Generated ${canonicalUrls.length} canonical URLs`);
1652
+ }
1653
+ const llmsTxtResult = createLlmsTxt({ config, canonicalUrls });
1654
+ const llmsFullTxtResult = createLlmsFullTxt({
1655
+ config,
1656
+ canonicalUrls,
1657
+ manifestItems
1658
+ });
1659
+ let citationsContent = null;
1660
+ if (emitCitations) {
1661
+ citationsContent = createCitationsJsonString({
1662
+ config,
1663
+ manifestItems,
1664
+ sectionName: "all"
1665
+ });
1666
+ }
1667
+ const result = {
1668
+ files: [],
1669
+ warnings: []
1670
+ };
1671
+ if (dryRun) {
1672
+ printDryRunHeader(config.output.paths.llmsTxt);
1673
+ console.log(llmsTxtResult.content);
1674
+ printSeparator();
1675
+ printDryRunHeader(config.output.paths.llmsFullTxt);
1676
+ console.log(llmsFullTxtResult.content);
1677
+ printSeparator();
1678
+ if (citationsContent) {
1679
+ printDryRunHeader(config.output.paths.citations ?? "citations.json");
1680
+ console.log(citationsContent);
1681
+ printSeparator();
1682
+ }
1683
+ printInfo("Dry run complete - no files written");
1684
+ result.files = [
1685
+ { path: config.output.paths.llmsTxt, size: llmsTxtResult.byteSize },
1686
+ { path: config.output.paths.llmsFullTxt, size: llmsFullTxtResult.byteSize }
1687
+ ];
1688
+ if (citationsContent) {
1689
+ result.files.push({
1690
+ path: config.output.paths.citations ?? "citations.json",
1691
+ size: Buffer.byteLength(citationsContent, "utf-8")
1692
+ });
1693
+ }
1694
+ } else {
1695
+ if (verbose) {
1696
+ printVerbose(`Writing ${config.output.paths.llmsTxt}`);
1697
+ }
1698
+ await writeFileAtomic(config.output.paths.llmsTxt, llmsTxtResult.content);
1699
+ result.files.push({ path: config.output.paths.llmsTxt, size: llmsTxtResult.byteSize });
1700
+ if (verbose) {
1701
+ printVerbose(`Writing ${config.output.paths.llmsFullTxt}`);
1702
+ }
1703
+ await writeFileAtomic(config.output.paths.llmsFullTxt, llmsFullTxtResult.content);
1704
+ result.files.push({ path: config.output.paths.llmsFullTxt, size: llmsFullTxtResult.byteSize });
1705
+ if (citationsContent && config.output.paths.citations) {
1706
+ if (verbose) {
1707
+ printVerbose(`Writing ${config.output.paths.citations}`);
1708
+ }
1709
+ await writeFileAtomic(config.output.paths.citations, citationsContent);
1710
+ result.files.push({
1711
+ path: config.output.paths.citations,
1712
+ size: Buffer.byteLength(citationsContent, "utf-8")
1713
+ });
1714
+ }
1715
+ printGenerateReport(result.files, verbose);
1716
+ }
1717
+ for (const warning of result.warnings) {
1718
+ printWarning(warning);
1719
+ }
1720
+ return ExitCodes.OK;
1721
+ } catch (error) {
1722
+ const message = error instanceof Error ? error.message : String(error);
1723
+ printError(`Generation failed: ${message}`);
1724
+ return ExitCodes.GENERATION_FAILED;
1725
+ }
1726
+ }
1727
+ function extractManifestItems(config) {
1728
+ const items = [];
1729
+ const manifests = config.manifests;
1730
+ for (const [_manifestName, manifestData] of Object.entries(manifests)) {
1731
+ if (typeof manifestData === "object" && manifestData !== null) {
1732
+ const data = manifestData;
1733
+ if (Array.isArray(data.pages)) {
1734
+ for (const page of data.pages) {
1735
+ const normalized = toManifestItem(page);
1736
+ if (normalized) {
1737
+ items.push(normalized);
1738
+ }
1739
+ }
1740
+ }
1741
+ if (Array.isArray(data)) {
1742
+ for (const item of data) {
1743
+ const normalized = toManifestItem(item);
1744
+ if (normalized) {
1745
+ items.push(normalized);
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+ }
1751
+ return items;
1752
+ }
1753
+ function toManifestItem(value) {
1754
+ if (typeof value !== "object" || value === null) {
1755
+ return null;
1756
+ }
1757
+ const data = value;
1758
+ const rawSlug = data.slug ?? data.path;
1759
+ if (typeof rawSlug !== "string" || rawSlug.length === 0) {
1760
+ return null;
1761
+ }
1762
+ const item = {
1763
+ slug: rawSlug.startsWith("/") ? rawSlug : `/${rawSlug}`
1764
+ };
1765
+ if (typeof data.title === "string") {
1766
+ item.title = data.title;
1767
+ }
1768
+ if (typeof data.description === "string") {
1769
+ item.description = data.description;
1770
+ }
1771
+ if (Array.isArray(data.locales)) {
1772
+ const locales = data.locales.filter((loc) => typeof loc === "string");
1773
+ if (locales.length > 0) {
1774
+ item.locales = locales;
1775
+ }
1776
+ }
1777
+ if (typeof data.canonicalOverride === "string") {
1778
+ item.canonicalOverride = data.canonicalOverride;
1779
+ }
1780
+ if (typeof data.publishedAt === "string") {
1781
+ item.publishedAt = data.publishedAt;
1782
+ }
1783
+ if (typeof data.updatedAt === "string") {
1784
+ item.updatedAt = data.updatedAt;
1785
+ }
1786
+ if (typeof data.priority === "number") {
1787
+ item.priority = data.priority;
1788
+ }
1789
+ return item;
1790
+ }
1791
+ var init_generate = __esm({
1792
+ "src/cli/commands/generate.ts"() {
1793
+ init_load_config();
1794
+ init_fs();
1795
+ init_report();
1796
+ init_exit_codes();
1797
+ init_core();
1798
+ }
1799
+ });
1800
+
1801
+ // src/cli/commands/check.ts
1802
+ var check_exports = {};
1803
+ __export(check_exports, {
1804
+ checkCommand: () => checkCommand
1805
+ });
1806
+ async function checkCommand(options) {
1807
+ const { config: configPath, failOn, verbose } = options;
1808
+ try {
1809
+ if (verbose) {
1810
+ printVerbose(`Loading config from: ${configPath}`);
1811
+ }
1812
+ let loadResult;
1813
+ try {
1814
+ loadResult = await loadConfig({ path: configPath });
1815
+ } catch (error) {
1816
+ const message = error instanceof Error ? error.message : String(error);
1817
+ printError(`Failed to load config: ${message}`);
1818
+ return ExitCodes.CONFIG_NOT_FOUND;
1819
+ }
1820
+ const { config } = loadResult;
1821
+ for (const issue of loadResult.issues) {
1822
+ if (issue.severity === "warning") {
1823
+ printWarning(issue.message);
1824
+ } else {
1825
+ printError(issue.message);
1826
+ return ExitCodes.INVALID_CONFIG;
1827
+ }
1828
+ }
1829
+ if (verbose) {
1830
+ printVerbose(`Config loaded successfully`);
1831
+ printVerbose(`Site: ${config.site.baseUrl}`);
1832
+ }
1833
+ if (verbose) {
1834
+ printVerbose("Running SEO checks...");
1835
+ }
1836
+ const checkOptions = {
1837
+ config,
1838
+ failOn,
1839
+ llmsTxtPath: config.output.paths.llmsTxt,
1840
+ llmsFullTxtPath: config.output.paths.llmsFullTxt,
1841
+ ...config.output.paths.citations && { citationsPath: config.output.paths.citations }
1842
+ };
1843
+ const result = await checkGeneratedFiles(checkOptions);
1844
+ const manifestItems = extractManifestItems2(config);
1845
+ const canonicalUrls = createCanonicalUrlsFromManifest({
1846
+ items: manifestItems,
1847
+ baseUrl: config.site.baseUrl,
1848
+ defaultLocale: config.site.defaultLocale ?? config.brand.locales[0] ?? "en",
1849
+ trailingSlash: config.format?.trailingSlash ?? "never",
1850
+ localeStrategy: "prefix"
1851
+ });
1852
+ const expectedLlms = createLlmsTxt({ config, canonicalUrls });
1853
+ const expectedLlmsFull = createLlmsFullTxt({
1854
+ config,
1855
+ canonicalUrls,
1856
+ manifestItems
1857
+ });
1858
+ const requiredMissing = result.issues.some((issue) => {
1859
+ return issue.code === "file_missing" && (issue.path === config.output.paths.llmsTxt || issue.path === config.output.paths.llmsFullTxt);
1860
+ });
1861
+ let merged = result;
1862
+ if (!requiredMissing) {
1863
+ const mismatchIssues = await checkFilesAgainstExpected(
1864
+ config.output.paths.llmsTxt,
1865
+ expectedLlms.content,
1866
+ config.output.paths.llmsFullTxt,
1867
+ expectedLlmsFull.content
1868
+ );
1869
+ if (mismatchIssues.length > 0) {
1870
+ const issues = [...result.issues, ...mismatchIssues];
1871
+ const counts = countSeverities(issues);
1872
+ const mismatchCount = mismatchIssues.filter((issue) => issue.code === "file_mismatch").length;
1873
+ merged = {
1874
+ ...result,
1875
+ issues,
1876
+ summary: {
1877
+ ...result.summary,
1878
+ errors: counts.error,
1879
+ warnings: counts.warning,
1880
+ info: counts.info,
1881
+ filesMismatch: result.summary.filesMismatch + mismatchCount
1882
+ }
1883
+ };
1884
+ }
1885
+ }
1886
+ printCheckReport(merged, verbose);
1887
+ if (merged.summary.errors > 0) {
1888
+ return ExitCodes.ERROR;
1889
+ }
1890
+ if (failOn === "warn" && merged.summary.warnings > 0) {
1891
+ return ExitCodes.WARN;
1892
+ }
1893
+ return ExitCodes.OK;
1894
+ } catch (error) {
1895
+ const message = error instanceof Error ? error.message : String(error);
1896
+ printError(`Check failed: ${message}`);
1897
+ return ExitCodes.ERROR;
1898
+ }
1899
+ }
1900
+ function extractManifestItems2(config) {
1901
+ const items = [];
1902
+ const manifests = config.manifests;
1903
+ for (const [_manifestName, manifestData] of Object.entries(manifests)) {
1904
+ if (typeof manifestData === "object" && manifestData !== null) {
1905
+ const data = manifestData;
1906
+ if (Array.isArray(data.pages)) {
1907
+ for (const page of data.pages) {
1908
+ const normalized = toManifestItem2(page);
1909
+ if (normalized) {
1910
+ items.push(normalized);
1911
+ }
1912
+ }
1913
+ }
1914
+ if (Array.isArray(data)) {
1915
+ for (const item of data) {
1916
+ const normalized = toManifestItem2(item);
1917
+ if (normalized) {
1918
+ items.push(normalized);
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ }
1924
+ return items;
1925
+ }
1926
+ function toManifestItem2(value) {
1927
+ if (typeof value !== "object" || value === null) {
1928
+ return null;
1929
+ }
1930
+ const data = value;
1931
+ const rawSlug = data.slug ?? data.path;
1932
+ if (typeof rawSlug !== "string" || rawSlug.length === 0) {
1933
+ return null;
1934
+ }
1935
+ const item = {
1936
+ slug: rawSlug.startsWith("/") ? rawSlug : `/${rawSlug}`
1937
+ };
1938
+ if (typeof data.title === "string") {
1939
+ item.title = data.title;
1940
+ }
1941
+ if (typeof data.description === "string") {
1942
+ item.description = data.description;
1943
+ }
1944
+ if (Array.isArray(data.locales)) {
1945
+ const locales = data.locales.filter((loc) => typeof loc === "string");
1946
+ if (locales.length > 0) {
1947
+ item.locales = locales;
1948
+ }
1949
+ }
1950
+ if (typeof data.canonicalOverride === "string") {
1951
+ item.canonicalOverride = data.canonicalOverride;
1952
+ }
1953
+ if (typeof data.publishedAt === "string") {
1954
+ item.publishedAt = data.publishedAt;
1955
+ }
1956
+ if (typeof data.updatedAt === "string") {
1957
+ item.updatedAt = data.updatedAt;
1958
+ }
1959
+ if (typeof data.priority === "number") {
1960
+ item.priority = data.priority;
1961
+ }
1962
+ return item;
1963
+ }
1964
+ var init_check = __esm({
1965
+ "src/cli/commands/check.ts"() {
1966
+ init_load_config();
1967
+ init_report();
1968
+ init_exit_codes();
1969
+ init_checker();
1970
+ init_issues();
1971
+ init_core();
1972
+ }
1973
+ });
1974
+
1975
+ // src/cli/commands/doctor.ts
1976
+ var doctor_exports = {};
1977
+ __export(doctor_exports, {
1978
+ doctorCommand: () => doctorCommand
1979
+ });
1980
+ async function doctorCommand(options) {
1981
+ const { site: siteUrl, config: configPath, verbose } = options;
1982
+ console.log("Running llm-seo diagnostics...\n");
1983
+ const checks = [];
1984
+ let config = null;
1985
+ checks.push(checkNodeVersion());
1986
+ const configCheck = await checkConfigFile(configPath, verbose);
1987
+ checks.push(configCheck.check);
1988
+ if (configCheck.config) {
1989
+ config = configCheck.config;
1990
+ }
1991
+ let baseUrl = siteUrl;
1992
+ if (!baseUrl && config) {
1993
+ baseUrl = config.site.baseUrl;
1994
+ }
1995
+ if (baseUrl) {
1996
+ if (verbose) {
1997
+ printVerbose(`Checking site: ${baseUrl}`);
1998
+ }
1999
+ checks.push(...await checkEndpoints(baseUrl, config, verbose));
2000
+ } else {
2001
+ checks.push({
2002
+ name: "Site URL",
2003
+ url: "",
2004
+ status: "warn",
2005
+ message: "No site URL specified. Use --site or configure in config file."
2006
+ });
2007
+ }
2008
+ printDoctorReport(checks, verbose);
2009
+ const hasErrors = checks.some((c) => c.status === "error");
2010
+ const hasWarnings = checks.some((c) => c.status === "warn");
2011
+ if (hasErrors) {
2012
+ return ExitCodes.NETWORK_ERROR;
2013
+ }
2014
+ if (hasWarnings) {
2015
+ return ExitCodes.WARN;
2016
+ }
2017
+ return ExitCodes.OK;
2018
+ }
2019
+ function checkNodeVersion(_verbose) {
2020
+ const nodeVersion = process.version;
2021
+ const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
2022
+ if (majorVersion >= 18) {
2023
+ return {
2024
+ name: "Node.js version",
2025
+ url: "",
2026
+ status: "ok",
2027
+ message: `${nodeVersion} (supported)`
2028
+ };
2029
+ }
2030
+ return {
2031
+ name: "Node.js version",
2032
+ url: "",
2033
+ status: "error",
2034
+ message: `${nodeVersion} (requires Node.js 18+)`
2035
+ };
2036
+ }
2037
+ async function checkConfigFile(configPath, verbose) {
2038
+ let foundPath = configPath;
2039
+ if (!foundPath) {
2040
+ foundPath = findConfigFile() ?? void 0;
2041
+ }
2042
+ if (!foundPath) {
2043
+ return {
2044
+ check: {
2045
+ name: "Config file",
2046
+ url: "",
2047
+ status: "warn",
2048
+ message: "No config file found. Run from project directory or specify --config."
2049
+ },
2050
+ config: null
2051
+ };
2052
+ }
2053
+ if (verbose) {
2054
+ printVerbose(`Found config: ${foundPath}`);
2055
+ }
2056
+ try {
2057
+ const loadResult = await loadConfig({ path: foundPath });
2058
+ return {
2059
+ check: {
2060
+ name: "Config file",
2061
+ url: foundPath,
2062
+ status: "ok",
2063
+ message: `Loaded from ${foundPath}`
2064
+ },
2065
+ config: loadResult.config
2066
+ };
2067
+ } catch (error) {
2068
+ const message = error instanceof Error ? error.message : String(error);
2069
+ return {
2070
+ check: {
2071
+ name: "Config file",
2072
+ url: foundPath,
2073
+ status: "error",
2074
+ message: `Invalid config: ${message}`
2075
+ },
2076
+ config: null
2077
+ };
2078
+ }
2079
+ }
2080
+ async function checkEndpoints(baseUrl, config, verbose) {
2081
+ const checks = [];
2082
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
2083
+ const robotsUrl = config?.machineHints?.robots ?? `${normalizedBaseUrl}/robots.txt`;
2084
+ checks.push(await checkEndpoint("robots.txt", robotsUrl));
2085
+ const sitemapUrl = config?.machineHints?.sitemap ?? `${normalizedBaseUrl}/sitemap.xml`;
2086
+ checks.push(await checkEndpoint("sitemap.xml", sitemapUrl));
2087
+ const sitemapIndexUrl = `${normalizedBaseUrl}/sitemap-index.xml`;
2088
+ const sitemapIndexCheck = await checkEndpoint("sitemap-index.xml", sitemapIndexUrl, verbose, true);
2089
+ if (sitemapIndexCheck.status === "error") {
2090
+ sitemapIndexCheck.status = "skip";
2091
+ sitemapIndexCheck.message = "Not found (optional)";
2092
+ }
2093
+ checks.push(sitemapIndexCheck);
2094
+ const llmsTxtUrl = config?.machineHints?.llmsTxt ?? `${normalizedBaseUrl}/llms.txt`;
2095
+ checks.push(await checkEndpoint("llms.txt", llmsTxtUrl));
2096
+ const llmsFullTxtUrl = config?.machineHints?.llmsFullTxt ?? `${normalizedBaseUrl}/llms-full.txt`;
2097
+ checks.push(await checkEndpoint("llms-full.txt", llmsFullTxtUrl));
2098
+ return checks;
2099
+ }
2100
+ async function checkEndpoint(name, url, _verbose, optional = false) {
2101
+ const startTime = Date.now();
2102
+ try {
2103
+ const response = await fetch(url, {
2104
+ method: "GET",
2105
+ signal: AbortSignal.timeout(1e4)
2106
+ // 10 second timeout
2107
+ });
2108
+ const responseTime = Date.now() - startTime;
2109
+ if (response.ok) {
2110
+ return {
2111
+ name,
2112
+ url,
2113
+ status: "ok",
2114
+ message: "OK",
2115
+ responseTime
2116
+ };
2117
+ }
2118
+ if (response.status === 404) {
2119
+ return {
2120
+ name,
2121
+ url,
2122
+ status: optional ? "skip" : "error",
2123
+ message: optional ? "Not found (optional)" : "HTTP 404 Not Found",
2124
+ responseTime
2125
+ };
2126
+ }
2127
+ return {
2128
+ name,
2129
+ url,
2130
+ status: "error",
2131
+ message: `HTTP ${response.status} ${response.statusText}`,
2132
+ responseTime
2133
+ };
2134
+ } catch (error) {
2135
+ const responseTime = Date.now() - startTime;
2136
+ const message = error instanceof Error ? error.message : String(error);
2137
+ if (message.includes("fetch failed") || message.includes("ENOTFOUND")) {
2138
+ return {
2139
+ name,
2140
+ url,
2141
+ status: "error",
2142
+ message: "Connection failed (DNS or network error)",
2143
+ responseTime
2144
+ };
2145
+ }
2146
+ if (message.includes("timeout") || message.includes("Timeout")) {
2147
+ return {
2148
+ name,
2149
+ url,
2150
+ status: "error",
2151
+ message: "Request timed out (10s)",
2152
+ responseTime
2153
+ };
2154
+ }
2155
+ return {
2156
+ name,
2157
+ url,
2158
+ status: optional ? "skip" : "error",
2159
+ message: optional ? "Not available" : `Failed: ${message}`,
2160
+ responseTime
2161
+ };
2162
+ }
2163
+ }
2164
+ var init_doctor = __esm({
2165
+ "src/cli/commands/doctor.ts"() {
2166
+ init_load_config();
2167
+ init_report();
2168
+ init_exit_codes();
2169
+ }
2170
+ });
2171
+
2172
+ // src/cli/bin.ts
2173
+ init_exit_codes();
2174
+ var VERSION = "0.1.0";
2175
+ var program = new Command();
2176
+ program.name("llm-seo").description("Deterministic LLM SEO artifacts generator and validator for modern static sites").version(VERSION).option("-c, --config <path>", "Path to config file", "llm-seo.config.ts").option("-v, --verbose", "Enable verbose output", false);
2177
+ program.command("generate").description("Generate llms.txt and llms-full.txt from site configuration").option("-c, --config <path>", "Path to config file").option("--dry-run", "Output to stdout instead of writing files", false).option("--emit-citations", "Generate citations.json file", false).option("-v, --verbose", "Show detailed progress", false).action(async (options) => {
2178
+ const { generateCommand: generateCommand2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
2179
+ const exitCode = await generateCommand2({
2180
+ config: options.config ?? program.opts().config,
2181
+ dryRun: options.dryRun ?? false,
2182
+ emitCitations: options.emitCitations ?? false,
2183
+ verbose: options.verbose ?? program.opts().verbose ?? false
2184
+ });
2185
+ process.exit(exitCode);
2186
+ });
2187
+ program.command("check").description("Validate generated llms.txt files against configuration").option("-c, --config <path>", "Path to config file").option("--fail-on <level>", "Fail on warnings (warn) or only errors (error)", "error").option("-v, --verbose", "Show detailed output", false).action(async (options) => {
2188
+ const { checkCommand: checkCommand2 } = await Promise.resolve().then(() => (init_check(), check_exports));
2189
+ const failOn = options.failOn;
2190
+ if (failOn !== "warn" && failOn !== "error") {
2191
+ console.error(`Invalid --fail-on value: ${options.failOn}. Must be 'warn' or 'error'.`);
2192
+ process.exit(ExitCodes.ERROR);
2193
+ }
2194
+ const exitCode = await checkCommand2({
2195
+ config: options.config ?? program.opts().config,
2196
+ failOn,
2197
+ verbose: options.verbose ?? program.opts().verbose ?? false
2198
+ });
2199
+ process.exit(exitCode);
2200
+ });
2201
+ program.command("doctor").description("Diagnose common issues with llm-seo setup and site endpoints").option("-s, --site <url>", "Site URL to check (e.g., https://example.com)").option("-c, --config <path>", "Path to config file").option("-v, --verbose", "Show detailed output", false).action(async (options) => {
2202
+ const { doctorCommand: doctorCommand2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
2203
+ const exitCode = await doctorCommand2({
2204
+ site: options.site,
2205
+ config: options.config ?? program.opts().config,
2206
+ verbose: options.verbose ?? program.opts().verbose ?? false
2207
+ });
2208
+ process.exit(exitCode);
2209
+ });
2210
+ program.command("help [command]").description("Display help for a specific command").action((commandName) => {
2211
+ if (commandName) {
2212
+ const command = program.commands.find((cmd) => cmd.name() === commandName);
2213
+ if (command) {
2214
+ command.outputHelp();
2215
+ } else {
2216
+ console.error(`Unknown command: ${commandName}`);
2217
+ process.exit(ExitCodes.COMMAND_NOT_FOUND);
2218
+ }
2219
+ } else {
2220
+ program.outputHelp();
2221
+ }
2222
+ });
2223
+ program.on("command:*", () => {
2224
+ console.error("Invalid command: %s\nSee --help for a list of available commands.", program.args.join(" "));
2225
+ process.exit(ExitCodes.COMMAND_NOT_FOUND);
2226
+ });
2227
+ program.parse(process.argv);
2228
+ if (!process.argv.slice(2).length) {
2229
+ program.outputHelp();
2230
+ }
2231
+ //# sourceMappingURL=bin.js.map
2232
+ //# sourceMappingURL=bin.js.map