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.
- package/dist/auth/index.js +8 -1
- package/dist/autofix/index.d.ts +0 -4
- package/dist/autofix/index.js +0 -21
- package/dist/capture/browser.d.ts +11 -0
- package/dist/capture/browser.js +173 -0
- package/dist/capture/diff.d.ts +23 -0
- package/dist/capture/diff.js +52 -0
- package/dist/capture/index.d.ts +23 -0
- package/dist/capture/index.js +210 -0
- package/dist/capture/naming.d.ts +17 -0
- package/dist/capture/naming.js +45 -0
- package/dist/capture/parser.d.ts +15 -0
- package/dist/capture/parser.js +80 -0
- package/dist/capture/types.d.ts +57 -0
- package/dist/capture/types.js +1 -0
- package/dist/cli.js +4 -0
- package/dist/commands/autofix.js +136 -120
- package/dist/commands/cron.js +58 -47
- package/dist/commands/deploy.js +123 -102
- package/dist/commands/generate.js +88 -6
- package/dist/commands/heal.d.ts +10 -0
- package/dist/commands/heal.js +201 -0
- package/dist/commands/i18n.js +146 -111
- package/dist/commands/lint.js +50 -44
- package/dist/commands/llms-txt.js +59 -49
- package/dist/commands/login.js +61 -43
- package/dist/commands/mcp.js +6 -0
- package/dist/commands/monitor.js +13 -8
- package/dist/commands/qa.d.ts +2 -0
- package/dist/commands/qa.js +43 -0
- package/dist/commands/review-pr.js +108 -102
- package/dist/commands/sdk.js +128 -122
- package/dist/commands/security.js +86 -80
- package/dist/commands/test.js +91 -92
- package/dist/commands/version.js +104 -75
- package/dist/commands/watch.js +130 -114
- package/dist/config/types.js +2 -2
- package/dist/context-hub/index.d.ts +23 -0
- package/dist/context-hub/index.js +179 -0
- package/dist/context-hub/mappings.d.ts +8 -0
- package/dist/context-hub/mappings.js +55 -0
- package/dist/context-hub/types.d.ts +33 -0
- package/dist/context-hub/types.js +1 -0
- package/dist/generator/generator.js +39 -6
- package/dist/generator/types.d.ts +7 -0
- package/dist/generator/writer.d.ts +3 -1
- package/dist/generator/writer.js +24 -4
- package/dist/llm/anthropic-client.d.ts +1 -0
- package/dist/llm/anthropic-client.js +3 -1
- package/dist/llm/index.d.ts +6 -4
- package/dist/llm/index.js +76 -261
- package/dist/llm/openai-client.d.ts +1 -0
- package/dist/llm/openai-client.js +7 -2
- package/dist/qa/checks.d.ts +10 -0
- package/dist/qa/checks.js +492 -0
- package/dist/qa/fixes.d.ts +30 -0
- package/dist/qa/fixes.js +277 -0
- package/dist/qa/index.d.ts +29 -0
- package/dist/qa/index.js +187 -0
- package/dist/qa/types.d.ts +24 -0
- package/dist/qa/types.js +1 -0
- package/dist/scanner/csharp.d.ts +23 -0
- package/dist/scanner/csharp.js +421 -0
- package/dist/scanner/index.js +16 -2
- package/dist/scanner/java.d.ts +39 -0
- package/dist/scanner/java.js +318 -0
- package/dist/scanner/kotlin.d.ts +23 -0
- package/dist/scanner/kotlin.js +389 -0
- package/dist/scanner/php.d.ts +57 -0
- package/dist/scanner/php.js +351 -0
- package/dist/scanner/ruby.d.ts +36 -0
- package/dist/scanner/ruby.js +431 -0
- package/dist/scanner/swift.d.ts +25 -0
- package/dist/scanner/swift.js +392 -0
- package/dist/scanner/types.d.ts +1 -1
- package/dist/template/content/docs/_navigation.json +46 -0
- package/dist/template/content/docs/_sidebars.json +684 -0
- package/dist/template/content/docs/core.md +4544 -0
- package/dist/template/content/docs/index.mdx +89 -0
- package/dist/template/content/docs/integrations.md +1158 -0
- package/dist/template/content/docs/llms-full.md +403 -0
- package/dist/template/content/docs/llms.txt +4588 -0
- package/dist/template/content/docs/other.md +10379 -0
- package/dist/template/content/docs/tools.md +746 -0
- package/dist/template/content/docs/types.md +531 -0
- package/dist/template/docs.json +13 -11
- package/dist/template/mdx-components.tsx +27 -2
- package/dist/template/package.json +6 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +84 -6
- package/dist/template/src/app/api/chat/route.ts +83 -128
- package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
- package/dist/template/src/app/docs/llms-full.md +151 -4
- package/dist/template/src/app/docs/llms.txt +2464 -847
- package/dist/template/src/app/docs/page.mdx +48 -38
- package/dist/template/src/app/layout.tsx +3 -1
- package/dist/template/src/app/page.tsx +22 -8
- package/dist/template/src/components/ai-chat.tsx +73 -64
- package/dist/template/src/components/breadcrumbs.tsx +21 -23
- package/dist/template/src/components/copy-button.tsx +13 -9
- package/dist/template/src/components/copy-page-button.tsx +54 -0
- package/dist/template/src/components/docs-layout.tsx +37 -25
- package/dist/template/src/components/header.tsx +51 -10
- package/dist/template/src/components/mdx/card.tsx +17 -3
- package/dist/template/src/components/mdx/code-block.tsx +13 -9
- package/dist/template/src/components/mdx/code-group.tsx +13 -8
- package/dist/template/src/components/mdx/heading.tsx +15 -2
- package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
- package/dist/template/src/components/mdx/index.tsx +2 -0
- package/dist/template/src/components/mdx/mermaid.tsx +110 -0
- package/dist/template/src/components/mdx/screenshot.tsx +150 -0
- package/dist/template/src/components/scroll-to-hash.tsx +48 -0
- package/dist/template/src/components/sidebar.tsx +12 -18
- package/dist/template/src/components/table-of-contents.tsx +9 -0
- package/dist/template/src/lib/highlight.ts +3 -88
- package/dist/template/src/lib/navigation.ts +159 -0
- package/dist/template/src/styles/globals.css +17 -6
- package/dist/utils/validation.d.ts +0 -3
- package/dist/utils/validation.js +0 -26
- 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:
|
|
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
|
|
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;
|