skrypt-ai 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/auth/index.js +8 -1
  2. package/dist/autofix/index.d.ts +0 -4
  3. package/dist/autofix/index.js +0 -21
  4. package/dist/capture/browser.d.ts +11 -0
  5. package/dist/capture/browser.js +173 -0
  6. package/dist/capture/diff.d.ts +23 -0
  7. package/dist/capture/diff.js +52 -0
  8. package/dist/capture/index.d.ts +23 -0
  9. package/dist/capture/index.js +210 -0
  10. package/dist/capture/naming.d.ts +17 -0
  11. package/dist/capture/naming.js +45 -0
  12. package/dist/capture/parser.d.ts +15 -0
  13. package/dist/capture/parser.js +80 -0
  14. package/dist/capture/types.d.ts +57 -0
  15. package/dist/capture/types.js +1 -0
  16. package/dist/cli.js +4 -0
  17. package/dist/commands/autofix.js +136 -120
  18. package/dist/commands/cron.js +58 -47
  19. package/dist/commands/deploy.js +123 -102
  20. package/dist/commands/generate.js +88 -6
  21. package/dist/commands/heal.d.ts +10 -0
  22. package/dist/commands/heal.js +201 -0
  23. package/dist/commands/i18n.js +146 -111
  24. package/dist/commands/lint.js +50 -44
  25. package/dist/commands/llms-txt.js +59 -49
  26. package/dist/commands/login.js +61 -43
  27. package/dist/commands/mcp.js +6 -0
  28. package/dist/commands/monitor.js +13 -8
  29. package/dist/commands/qa.d.ts +2 -0
  30. package/dist/commands/qa.js +43 -0
  31. package/dist/commands/review-pr.js +108 -102
  32. package/dist/commands/sdk.js +128 -122
  33. package/dist/commands/security.js +86 -80
  34. package/dist/commands/test.js +91 -92
  35. package/dist/commands/version.js +104 -75
  36. package/dist/commands/watch.js +130 -114
  37. package/dist/config/types.js +2 -2
  38. package/dist/context-hub/index.d.ts +23 -0
  39. package/dist/context-hub/index.js +179 -0
  40. package/dist/context-hub/mappings.d.ts +8 -0
  41. package/dist/context-hub/mappings.js +55 -0
  42. package/dist/context-hub/types.d.ts +33 -0
  43. package/dist/context-hub/types.js +1 -0
  44. package/dist/generator/generator.js +39 -6
  45. package/dist/generator/types.d.ts +7 -0
  46. package/dist/generator/writer.d.ts +3 -1
  47. package/dist/generator/writer.js +24 -4
  48. package/dist/llm/anthropic-client.d.ts +1 -0
  49. package/dist/llm/anthropic-client.js +3 -1
  50. package/dist/llm/index.d.ts +6 -4
  51. package/dist/llm/index.js +76 -261
  52. package/dist/llm/openai-client.d.ts +1 -0
  53. package/dist/llm/openai-client.js +7 -2
  54. package/dist/qa/checks.d.ts +10 -0
  55. package/dist/qa/checks.js +492 -0
  56. package/dist/qa/fixes.d.ts +30 -0
  57. package/dist/qa/fixes.js +277 -0
  58. package/dist/qa/index.d.ts +29 -0
  59. package/dist/qa/index.js +187 -0
  60. package/dist/qa/types.d.ts +24 -0
  61. package/dist/qa/types.js +1 -0
  62. package/dist/scanner/csharp.d.ts +23 -0
  63. package/dist/scanner/csharp.js +421 -0
  64. package/dist/scanner/index.js +16 -2
  65. package/dist/scanner/java.d.ts +39 -0
  66. package/dist/scanner/java.js +318 -0
  67. package/dist/scanner/kotlin.d.ts +23 -0
  68. package/dist/scanner/kotlin.js +389 -0
  69. package/dist/scanner/php.d.ts +57 -0
  70. package/dist/scanner/php.js +351 -0
  71. package/dist/scanner/ruby.d.ts +36 -0
  72. package/dist/scanner/ruby.js +431 -0
  73. package/dist/scanner/swift.d.ts +25 -0
  74. package/dist/scanner/swift.js +392 -0
  75. package/dist/scanner/types.d.ts +1 -1
  76. package/dist/template/content/docs/_navigation.json +46 -0
  77. package/dist/template/content/docs/_sidebars.json +684 -0
  78. package/dist/template/content/docs/core.md +4544 -0
  79. package/dist/template/content/docs/index.mdx +89 -0
  80. package/dist/template/content/docs/integrations.md +1158 -0
  81. package/dist/template/content/docs/llms-full.md +403 -0
  82. package/dist/template/content/docs/llms.txt +4588 -0
  83. package/dist/template/content/docs/other.md +10379 -0
  84. package/dist/template/content/docs/tools.md +746 -0
  85. package/dist/template/content/docs/types.md +531 -0
  86. package/dist/template/docs.json +13 -11
  87. package/dist/template/mdx-components.tsx +27 -2
  88. package/dist/template/package.json +6 -0
  89. package/dist/template/public/search-index.json +1 -1
  90. package/dist/template/scripts/build-search-index.mjs +84 -6
  91. package/dist/template/src/app/api/chat/route.ts +83 -128
  92. package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
  93. package/dist/template/src/app/docs/llms-full.md +151 -4
  94. package/dist/template/src/app/docs/llms.txt +2464 -847
  95. package/dist/template/src/app/docs/page.mdx +48 -38
  96. package/dist/template/src/app/layout.tsx +3 -1
  97. package/dist/template/src/app/page.tsx +22 -8
  98. package/dist/template/src/components/ai-chat.tsx +73 -64
  99. package/dist/template/src/components/breadcrumbs.tsx +21 -23
  100. package/dist/template/src/components/copy-button.tsx +13 -9
  101. package/dist/template/src/components/copy-page-button.tsx +54 -0
  102. package/dist/template/src/components/docs-layout.tsx +37 -25
  103. package/dist/template/src/components/header.tsx +51 -10
  104. package/dist/template/src/components/mdx/card.tsx +17 -3
  105. package/dist/template/src/components/mdx/code-block.tsx +13 -9
  106. package/dist/template/src/components/mdx/code-group.tsx +13 -8
  107. package/dist/template/src/components/mdx/heading.tsx +15 -2
  108. package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
  109. package/dist/template/src/components/mdx/index.tsx +2 -0
  110. package/dist/template/src/components/mdx/mermaid.tsx +110 -0
  111. package/dist/template/src/components/mdx/screenshot.tsx +150 -0
  112. package/dist/template/src/components/scroll-to-hash.tsx +48 -0
  113. package/dist/template/src/components/sidebar.tsx +12 -18
  114. package/dist/template/src/components/table-of-contents.tsx +9 -0
  115. package/dist/template/src/lib/highlight.ts +3 -88
  116. package/dist/template/src/lib/navigation.ts +159 -0
  117. package/dist/template/src/styles/globals.css +17 -6
  118. package/dist/utils/validation.d.ts +0 -3
  119. package/dist/utils/validation.js +0 -26
  120. package/package.json +3 -2
@@ -9,14 +9,19 @@ export class OpenAICompatibleClient {
9
9
  client;
10
10
  model;
11
11
  maxRetries;
12
+ apiKey;
12
13
  constructor(config) {
13
14
  this.provider = config.provider;
14
15
  this.model = config.model;
15
16
  this.maxRetries = config.maxRetries ?? 3;
16
17
  const baseURL = config.baseUrl || PROVIDER_BASE_URLS[config.provider];
17
18
  const extraHeaders = PROVIDER_EXTRA_HEADERS[config.provider] || {};
19
+ // Ollama doesn't need a real key; other providers get empty string
20
+ // (SDK validates on first request, not construction)
21
+ const effectiveKey = config.apiKey || (config.provider === 'ollama' ? 'ollama' : 'not-set');
22
+ this.apiKey = effectiveKey;
18
23
  this.client = new OpenAI({
19
- apiKey: config.apiKey || 'ollama', // Ollama doesn't need a real key
24
+ apiKey: effectiveKey,
20
25
  baseURL,
21
26
  timeout: config.timeout ?? 60000,
22
27
  maxRetries: this.maxRetries,
@@ -24,7 +29,7 @@ export class OpenAICompatibleClient {
24
29
  });
25
30
  }
26
31
  isConfigured() {
27
- return true; // OpenAI SDK handles validation
32
+ return this.apiKey !== '' && this.apiKey !== 'not-set';
28
33
  }
29
34
  async complete(request) {
30
35
  const model = request.model || this.model;
@@ -0,0 +1,10 @@
1
+ import type { QAIssue } from './types.js';
2
+ export declare function checkFrontmatter(filePath: string, content: string, relPath: string): QAIssue[];
3
+ export declare function checkHeadings(content: string, relPath: string): QAIssue[];
4
+ export declare function checkCodeBlocks(content: string, relPath: string): QAIssue[];
5
+ export declare function checkComponents(content: string, relPath: string): QAIssue[];
6
+ export declare function checkLinks(content: string, relPath: string, _allFiles: Set<string>): QAIssue[];
7
+ export declare function checkSecurity(content: string, relPath: string): QAIssue[];
8
+ export declare function checkContentQuality(content: string, relPath: string): QAIssue[];
9
+ export declare function checkScreenshots(content: string, relPath: string, outputDir: string): QAIssue[];
10
+ export declare function checkMdxSyntax(content: string, relPath: string): QAIssue[];
@@ -0,0 +1,492 @@
1
+ import { existsSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ // ── Registered MDX components (must match template/src/components/mdx/index.tsx) ──
4
+ const KNOWN_COMPONENTS = new Set([
5
+ 'Card', 'CardGroup', 'Tabs', 'TabList', 'Tab', 'TabPanel',
6
+ 'CodeGroup', 'CodeBlock', 'HighlightedCode',
7
+ 'Callout', 'Info', 'Warning', 'Success', 'Error', 'Tip', 'Note',
8
+ 'Accordion', 'AccordionGroup', 'Steps', 'Step',
9
+ 'H1', 'H2', 'H3', 'H4',
10
+ 'ParamTable', 'Schema', 'Mermaid',
11
+ 'LinkPreview', 'Changelog', 'ChangelogEntry', 'Change',
12
+ 'Tooltip', 'Frame', 'DarkImage',
13
+ 'MethodBadge', 'StatusBadge', 'Endpoint',
14
+ 'CodePlayground', 'PythonPlayground', 'GoPlayground',
15
+ 'Screenshot',
16
+ // Passthrough components (handled but rendered as fragments)
17
+ 'TabItem', 'Details',
18
+ ]);
19
+ // ── Check: Frontmatter ──
20
+ export function checkFrontmatter(filePath, content, relPath) {
21
+ const issues = [];
22
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
23
+ if (!fmMatch) {
24
+ issues.push({
25
+ file: relPath,
26
+ line: 1,
27
+ severity: 'warning',
28
+ check: 'frontmatter',
29
+ message: 'Missing frontmatter (title and description recommended)',
30
+ });
31
+ return issues;
32
+ }
33
+ const fm = fmMatch[1];
34
+ if (!/^title\s*:/m.test(fm)) {
35
+ issues.push({
36
+ file: relPath,
37
+ line: 1,
38
+ severity: 'warning',
39
+ check: 'frontmatter',
40
+ message: 'Missing "title" in frontmatter',
41
+ });
42
+ }
43
+ return issues;
44
+ }
45
+ // ── Check: Heading structure ──
46
+ export function checkHeadings(content, relPath) {
47
+ const issues = [];
48
+ const lines = content.split('\n');
49
+ let h1Count = 0;
50
+ let lastLevel = 0;
51
+ let inCodeBlock = false;
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ if (line.startsWith('```')) {
55
+ inCodeBlock = !inCodeBlock;
56
+ continue;
57
+ }
58
+ if (inCodeBlock)
59
+ continue;
60
+ const headingMatch = line.match(/^(#{1,6})\s+/);
61
+ if (!headingMatch)
62
+ continue;
63
+ const level = headingMatch[1].length;
64
+ const lineNum = i + 1;
65
+ if (level === 1) {
66
+ h1Count++;
67
+ if (h1Count > 1) {
68
+ issues.push({
69
+ file: relPath,
70
+ line: lineNum,
71
+ severity: 'warning',
72
+ check: 'headings',
73
+ message: 'Multiple h1 headings found (only one recommended)',
74
+ });
75
+ }
76
+ }
77
+ if (lastLevel > 0 && level > lastLevel + 1) {
78
+ issues.push({
79
+ file: relPath,
80
+ line: lineNum,
81
+ severity: 'warning',
82
+ check: 'headings',
83
+ message: `Heading level skipped: h${lastLevel} → h${level} (should increment by 1)`,
84
+ });
85
+ }
86
+ lastLevel = level;
87
+ }
88
+ return issues;
89
+ }
90
+ // ── Check: Code blocks ──
91
+ export function checkCodeBlocks(content, relPath) {
92
+ const issues = [];
93
+ const lines = content.split('\n');
94
+ let inCodeBlock = false;
95
+ let codeBlockStart = 0;
96
+ let codeBlockLines = 0;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i];
99
+ if (line.startsWith('```')) {
100
+ if (!inCodeBlock) {
101
+ inCodeBlock = true;
102
+ codeBlockStart = i + 1;
103
+ const codeBlockLang = line.slice(3).trim().split(/\s/)[0] || '';
104
+ codeBlockLines = 0;
105
+ if (!codeBlockLang) {
106
+ issues.push({
107
+ file: relPath,
108
+ line: codeBlockStart,
109
+ severity: 'info',
110
+ check: 'code-blocks',
111
+ message: 'Code block missing language specifier',
112
+ });
113
+ }
114
+ }
115
+ else {
116
+ inCodeBlock = false;
117
+ if (codeBlockLines === 0) {
118
+ issues.push({
119
+ file: relPath,
120
+ line: codeBlockStart,
121
+ severity: 'warning',
122
+ check: 'code-blocks',
123
+ message: 'Empty code block',
124
+ });
125
+ }
126
+ }
127
+ }
128
+ else if (inCodeBlock) {
129
+ codeBlockLines++;
130
+ }
131
+ }
132
+ if (inCodeBlock) {
133
+ issues.push({
134
+ file: relPath,
135
+ line: codeBlockStart,
136
+ severity: 'error',
137
+ check: 'code-blocks',
138
+ message: 'Unclosed code block (missing closing ```)',
139
+ });
140
+ }
141
+ return issues;
142
+ }
143
+ // TypeScript/generic types that look like JSX components but aren't
144
+ const TYPE_NAMES = new Set([
145
+ 'T', 'K', 'V', 'U', 'R', 'P', 'S', 'E', // Single-letter generics
146
+ 'Map', 'Set', 'Array', 'Record', 'Promise', 'Partial', 'Required',
147
+ 'Readonly', 'Pick', 'Omit', 'Extract', 'Exclude', 'NonNullable',
148
+ 'ReturnType', 'InstanceType', 'Parameters', 'ConstructorParameters',
149
+ 'Function', 'Object', 'String', 'Number', 'Boolean', 'Symbol', 'RegExp',
150
+ 'Date', 'Error', 'TypeError', 'RangeError', 'Buffer', 'Uint8Array',
151
+ ]);
152
+ // ── Check: Components ──
153
+ export function checkComponents(content, relPath) {
154
+ const issues = [];
155
+ const lines = content.split('\n');
156
+ let inCodeBlock = false;
157
+ for (let i = 0; i < lines.length; i++) {
158
+ const line = lines[i];
159
+ if (line.startsWith('```')) {
160
+ inCodeBlock = !inCodeBlock;
161
+ continue;
162
+ }
163
+ if (inCodeBlock)
164
+ continue;
165
+ // Find JSX component tags (PascalCase) that look like MDX usage
166
+ // Only flag tags that appear at the start of a line or as standalone JSX
167
+ // Skip tags that look like TypeScript generics (inside backticks, followed by >)
168
+ const componentMatches = line.matchAll(/<([A-Z][A-Za-z0-9]*)\s*/g);
169
+ for (const match of componentMatches) {
170
+ const component = match[1];
171
+ // Skip known components
172
+ if (KNOWN_COMPONENTS.has(component))
173
+ continue;
174
+ // Skip TypeScript type names (generics, built-in types)
175
+ if (TYPE_NAMES.has(component))
176
+ continue;
177
+ // Skip if it looks like a TypeScript generic (e.g., "Promise<FixResult>" or "`Map<string>`")
178
+ // Check if the tag is inside backticks or follows a type-like pattern
179
+ const beforeTag = line.slice(0, match.index);
180
+ if (beforeTag.includes('`') || /\w$/.test(beforeTag))
181
+ continue;
182
+ // Skip if preceded by common TypeScript patterns
183
+ if (/:\s*$|=>\s*$|\|\s*$|&\s*$/.test(beforeTag))
184
+ continue;
185
+ // Skip if it ends with > immediately (like "Result>" part of "SomeType<Result>")
186
+ const afterTag = line.slice((match.index || 0) + match[0].length);
187
+ if (/^[A-Za-z0-9_,\s]*>/.test(afterTag) && !afterTag.startsWith('>'))
188
+ continue;
189
+ issues.push({
190
+ file: relPath,
191
+ line: i + 1,
192
+ severity: 'error',
193
+ check: 'components',
194
+ message: `Unknown component <${component}> — not registered in MDX component map`,
195
+ });
196
+ }
197
+ }
198
+ return issues;
199
+ }
200
+ // ── Check: Internal links ──
201
+ export function checkLinks(content, relPath, _allFiles) {
202
+ const issues = [];
203
+ const lines = content.split('\n');
204
+ let inCodeBlock = false;
205
+ for (let i = 0; i < lines.length; i++) {
206
+ const line = lines[i];
207
+ if (line.startsWith('```')) {
208
+ inCodeBlock = !inCodeBlock;
209
+ continue;
210
+ }
211
+ if (inCodeBlock)
212
+ continue;
213
+ // Markdown links
214
+ const linkMatches = line.matchAll(/\[([^\]]*)\]\(([^)]+)\)/g);
215
+ for (const match of linkMatches) {
216
+ const linkText = match[1];
217
+ const href = match[2];
218
+ // Skip external links
219
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:'))
220
+ continue;
221
+ // Skip hash-only links (same-page anchors)
222
+ if (href.startsWith('#'))
223
+ continue;
224
+ // Empty link text
225
+ if (!linkText.trim()) {
226
+ issues.push({
227
+ file: relPath,
228
+ line: i + 1,
229
+ severity: 'warning',
230
+ check: 'links',
231
+ message: `Empty link text for href="${href}"`,
232
+ });
233
+ }
234
+ }
235
+ // Image alt text
236
+ const imgMatches = line.matchAll(/!\[([^\]]*)\]\(/g);
237
+ for (const match of imgMatches) {
238
+ if (!match[1].trim()) {
239
+ issues.push({
240
+ file: relPath,
241
+ line: i + 1,
242
+ severity: 'warning',
243
+ check: 'links',
244
+ message: 'Image missing alt text',
245
+ });
246
+ }
247
+ }
248
+ }
249
+ return issues;
250
+ }
251
+ // ── Check: Security ──
252
+ export function checkSecurity(content, relPath) {
253
+ const issues = [];
254
+ const lines = content.split('\n');
255
+ let inCodeBlock = false;
256
+ for (let i = 0; i < lines.length; i++) {
257
+ const line = lines[i];
258
+ if (line.startsWith('```')) {
259
+ inCodeBlock = !inCodeBlock;
260
+ continue;
261
+ }
262
+ if (inCodeBlock)
263
+ continue;
264
+ // Raw <script> tags (outside code blocks)
265
+ if (/<script[\s>]/i.test(line)) {
266
+ issues.push({
267
+ file: relPath,
268
+ line: i + 1,
269
+ severity: 'error',
270
+ check: 'security',
271
+ message: 'Raw <script> tag found — potential XSS risk',
272
+ });
273
+ }
274
+ // javascript: URLs
275
+ if (/href\s*=\s*["']javascript:/i.test(line)) {
276
+ issues.push({
277
+ file: relPath,
278
+ line: i + 1,
279
+ severity: 'error',
280
+ check: 'security',
281
+ message: 'javascript: URL found — XSS risk',
282
+ });
283
+ }
284
+ // onclick and other event handlers
285
+ if (/\s+on\w+\s*=\s*["']/i.test(line) && !inCodeBlock) {
286
+ issues.push({
287
+ file: relPath,
288
+ line: i + 1,
289
+ severity: 'error',
290
+ check: 'security',
291
+ message: 'Inline event handler found — potential XSS risk',
292
+ });
293
+ }
294
+ // Iframe without sandbox
295
+ if (/<iframe\s/i.test(line) && !/sandbox/i.test(line)) {
296
+ issues.push({
297
+ file: relPath,
298
+ line: i + 1,
299
+ severity: 'warning',
300
+ check: 'security',
301
+ message: '<iframe> without sandbox attribute',
302
+ });
303
+ }
304
+ }
305
+ return issues;
306
+ }
307
+ // ── Check: Content quality ──
308
+ export function checkContentQuality(content, relPath) {
309
+ const issues = [];
310
+ // Strip frontmatter and code blocks for word count
311
+ const stripped = content
312
+ .replace(/^---[\s\S]*?---/, '')
313
+ .replace(/```[\s\S]*?```/g, '')
314
+ .replace(/<[^>]+>/g, '')
315
+ .trim();
316
+ // File is basically empty
317
+ if (stripped.length < 50) {
318
+ issues.push({
319
+ file: relPath,
320
+ severity: 'warning',
321
+ check: 'content',
322
+ message: 'File has very little content (< 50 chars after stripping code/frontmatter)',
323
+ });
324
+ }
325
+ // Check for broken MDX — unclosed JSX tags
326
+ const openTags = [];
327
+ const tagRegex = /<\/?([A-Z][A-Za-z0-9]*)\s*[^>]*\/?>/g;
328
+ let inCodeBlock = false;
329
+ for (const line of content.split('\n')) {
330
+ tagRegex.lastIndex = 0;
331
+ if (line.startsWith('```')) {
332
+ inCodeBlock = !inCodeBlock;
333
+ continue;
334
+ }
335
+ if (inCodeBlock)
336
+ continue;
337
+ let match;
338
+ while ((match = tagRegex.exec(line)) !== null) {
339
+ const fullMatch = match[0];
340
+ const tag = match[1];
341
+ if (fullMatch.endsWith('/>'))
342
+ continue; // self-closing
343
+ if (fullMatch.startsWith('</')) {
344
+ // Closing tag
345
+ if (openTags.length > 0 && openTags[openTags.length - 1] === tag) {
346
+ openTags.pop();
347
+ }
348
+ }
349
+ else {
350
+ openTags.push(tag);
351
+ }
352
+ }
353
+ }
354
+ // Don't flag unclosed tags for components that can wrap large content blocks
355
+ const wrapperComponents = new Set(['CardGroup', 'CodeGroup', 'Steps', 'AccordionGroup', 'Tabs']);
356
+ const unclosed = openTags.filter(t => !wrapperComponents.has(t));
357
+ if (unclosed.length > 5) {
358
+ issues.push({
359
+ file: relPath,
360
+ severity: 'info',
361
+ check: 'content',
362
+ message: `${unclosed.length} potentially unclosed JSX tags: ${unclosed.slice(0, 3).join(', ')}...`,
363
+ });
364
+ }
365
+ return issues;
366
+ }
367
+ // ── Check: Screenshot files ──
368
+ export function checkScreenshots(content, relPath, outputDir) {
369
+ const issues = [];
370
+ const lines = content.split('\n');
371
+ let inCodeBlock = false;
372
+ for (let i = 0; i < lines.length; i++) {
373
+ const line = lines[i];
374
+ if (line === undefined)
375
+ continue;
376
+ if (line.startsWith('```')) {
377
+ inCodeBlock = !inCodeBlock;
378
+ continue;
379
+ }
380
+ if (inCodeBlock)
381
+ continue;
382
+ const tagMatches = line.matchAll(/<Screenshot\s+([^>]*?)\/?>/g);
383
+ for (const match of tagMatches) {
384
+ const propsStr = match[1];
385
+ if (!propsStr)
386
+ continue;
387
+ // Extract url and selector props
388
+ const urlMatch = propsStr.match(/url\s*=\s*["']([^"']*?)["']/);
389
+ const selectorMatch = propsStr.match(/selector\s*=\s*["']([^"']*?)["']/);
390
+ if (!urlMatch?.[1])
391
+ continue;
392
+ const url = urlMatch[1];
393
+ const selector = selectorMatch?.[1];
394
+ // Compute expected filename (same logic as capture/naming.ts)
395
+ let pathname;
396
+ try {
397
+ pathname = new URL(url).pathname;
398
+ }
399
+ catch {
400
+ pathname = url;
401
+ }
402
+ let slug = pathname.replace(/^\/+/, '').replace(/\//g, '-');
403
+ if (!slug)
404
+ slug = 'index';
405
+ slug = slug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
406
+ if (selector) {
407
+ let selSlug = selector.replace(/^[.#]/, '');
408
+ selSlug = selSlug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
409
+ slug = `${slug}--${selSlug}`;
410
+ }
411
+ const expectedFile = `${slug}.png`;
412
+ // Check if screenshot file exists in common locations
413
+ const screenshotDirs = [
414
+ join(outputDir, 'public', 'screenshots'),
415
+ join(outputDir, 'screenshots'),
416
+ ];
417
+ let found = false;
418
+ let stale = false;
419
+ for (const dir of screenshotDirs) {
420
+ const filePath = join(dir, expectedFile);
421
+ if (existsSync(filePath)) {
422
+ found = true;
423
+ // Check if stale (older than 7 days)
424
+ try {
425
+ const stat = statSync(filePath);
426
+ const age = Date.now() - stat.mtimeMs;
427
+ if (age > 7 * 24 * 60 * 60 * 1000) {
428
+ stale = true;
429
+ }
430
+ }
431
+ catch {
432
+ // stat failed, skip staleness check
433
+ }
434
+ break;
435
+ }
436
+ }
437
+ if (!found) {
438
+ issues.push({
439
+ file: relPath,
440
+ line: i + 1,
441
+ severity: 'warning',
442
+ check: 'screenshots',
443
+ message: `Screenshot file missing: ${expectedFile} — run \`skrypt heal --screenshots\``,
444
+ });
445
+ }
446
+ else if (stale) {
447
+ issues.push({
448
+ file: relPath,
449
+ line: i + 1,
450
+ severity: 'info',
451
+ check: 'screenshots',
452
+ message: `Screenshot may be stale (> 7 days old): ${expectedFile}`,
453
+ });
454
+ }
455
+ }
456
+ }
457
+ return issues;
458
+ }
459
+ // ── Check: MDX compilation (lightweight syntax check) ──
460
+ export function checkMdxSyntax(content, relPath) {
461
+ const issues = [];
462
+ const lines = content.split('\n');
463
+ let inCodeBlock = false;
464
+ for (let i = 0; i < lines.length; i++) {
465
+ const line = lines[i];
466
+ if (line.startsWith('```')) {
467
+ inCodeBlock = !inCodeBlock;
468
+ continue;
469
+ }
470
+ if (inCodeBlock)
471
+ continue;
472
+ // import statements (MDX treats these as JS imports)
473
+ if (/^import\s+/.test(line)) {
474
+ // Check if it's importing from a theme (Docusaurus pattern that breaks MDX)
475
+ if (/@theme\//.test(line)) {
476
+ issues.push({
477
+ file: relPath,
478
+ line: i + 1,
479
+ severity: 'error',
480
+ check: 'mdx-syntax',
481
+ message: 'Docusaurus @theme import found — not compatible with this template',
482
+ });
483
+ }
484
+ }
485
+ // Curly braces outside JSX expressions can break MDX
486
+ // Check for lone { or } that aren't in JSX attributes
487
+ if (/^\s*\{[^}]*$/.test(line) && !/<\w/.test(line) && !/^\s*\{\/\*/.test(line)) {
488
+ // Potential issue but too many false positives, skip
489
+ }
490
+ }
491
+ return issues;
492
+ }
@@ -0,0 +1,30 @@
1
+ export interface FixResult {
2
+ content: string;
3
+ fixes: string[];
4
+ }
5
+ /**
6
+ * Fix frontmatter issues:
7
+ * - Add frontmatter with title if none exists
8
+ * - Add title field if frontmatter exists but has no title
9
+ */
10
+ export declare function fixFrontmatter(content: string, filePath: string): FixResult;
11
+ /**
12
+ * Fix MDX syntax issues:
13
+ * - Strip Docusaurus @theme imports
14
+ * - Strip empty <a id="..."></a> anchor tags
15
+ */
16
+ export declare function fixMdxSyntax(content: string): FixResult;
17
+ /**
18
+ * Fix code block issues:
19
+ * - Infer language for code blocks missing a language specifier
20
+ * - Close unclosed code blocks at end of file
21
+ */
22
+ export declare function fixCodeBlocks(content: string): FixResult;
23
+ /**
24
+ * Fix security issues:
25
+ * - Strip <script>...</script> tags (outside code blocks)
26
+ * - Replace javascript: URLs with #
27
+ * - Strip inline on* event handlers from HTML tags
28
+ * - Add sandbox attribute to unsandboxed <iframe> tags
29
+ */
30
+ export declare function fixSecurity(content: string): FixResult;