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
package/dist/qa/fixes.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Convert kebab-case or snake_case filename to Title Case
|
|
4
|
+
*/
|
|
5
|
+
function filenameToTitle(filename) {
|
|
6
|
+
const name = filename.replace(/\.(mdx?|md)$/, '');
|
|
7
|
+
return name
|
|
8
|
+
.split(/[-_]/)
|
|
9
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
10
|
+
.join(' ');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extract the first h1 heading from content
|
|
14
|
+
*/
|
|
15
|
+
function extractFirstH1(content) {
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
let inCodeBlock = false;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
if (line.startsWith('```')) {
|
|
20
|
+
inCodeBlock = !inCodeBlock;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (inCodeBlock)
|
|
24
|
+
continue;
|
|
25
|
+
const match = line.match(/^#\s+(.+)/);
|
|
26
|
+
if (match)
|
|
27
|
+
return match[1].trim();
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fix frontmatter issues:
|
|
33
|
+
* - Add frontmatter with title if none exists
|
|
34
|
+
* - Add title field if frontmatter exists but has no title
|
|
35
|
+
*/
|
|
36
|
+
export function fixFrontmatter(content, filePath) {
|
|
37
|
+
const fixes = [];
|
|
38
|
+
let result = content;
|
|
39
|
+
const fmMatch = result.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
40
|
+
if (!fmMatch) {
|
|
41
|
+
// No frontmatter — add it with a title
|
|
42
|
+
const h1 = extractFirstH1(content);
|
|
43
|
+
const title = h1 || filenameToTitle(basename(filePath));
|
|
44
|
+
result = `---\ntitle: "${title}"\n---\n\n${result}`;
|
|
45
|
+
fixes.push(`added missing frontmatter with title "${title}"`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const fm = fmMatch[1];
|
|
49
|
+
if (!/^title\s*:/m.test(fm)) {
|
|
50
|
+
// Frontmatter exists but no title
|
|
51
|
+
const h1 = extractFirstH1(content);
|
|
52
|
+
const title = h1 || filenameToTitle(basename(filePath));
|
|
53
|
+
// Insert title as first field in frontmatter
|
|
54
|
+
result = result.replace(/^---\s*\n/, `---\ntitle: "${title}"\n`);
|
|
55
|
+
fixes.push(`added missing title "${title}" to frontmatter`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { content: result, fixes };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fix MDX syntax issues:
|
|
62
|
+
* - Strip Docusaurus @theme imports
|
|
63
|
+
* - Strip empty <a id="..."></a> anchor tags
|
|
64
|
+
*/
|
|
65
|
+
export function fixMdxSyntax(content) {
|
|
66
|
+
const fixes = [];
|
|
67
|
+
const lines = content.split('\n');
|
|
68
|
+
const outputLines = [];
|
|
69
|
+
let inCodeBlock = false;
|
|
70
|
+
let strippedImports = 0;
|
|
71
|
+
let strippedAnchors = 0;
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (line.startsWith('```')) {
|
|
74
|
+
inCodeBlock = !inCodeBlock;
|
|
75
|
+
outputLines.push(line);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (inCodeBlock) {
|
|
79
|
+
outputLines.push(line);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Strip @theme imports
|
|
83
|
+
if (/^import\s+.*from\s+['"]@theme\//.test(line)) {
|
|
84
|
+
strippedImports++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Strip empty anchor tags: <a id="..."></a> or <a name="..."></a>
|
|
88
|
+
const anchorStripped = line.replace(/<a\s+(?:id|name)\s*=\s*["'][^"']*["']\s*>\s*<\/a>/g, '');
|
|
89
|
+
if (anchorStripped !== line) {
|
|
90
|
+
strippedAnchors++;
|
|
91
|
+
// Only keep the line if it has content after stripping
|
|
92
|
+
if (anchorStripped.trim()) {
|
|
93
|
+
outputLines.push(anchorStripped);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
outputLines.push(line);
|
|
98
|
+
}
|
|
99
|
+
if (strippedImports > 0) {
|
|
100
|
+
fixes.push(`stripped ${strippedImports} @theme import${strippedImports > 1 ? 's' : ''}`);
|
|
101
|
+
}
|
|
102
|
+
if (strippedAnchors > 0) {
|
|
103
|
+
fixes.push(`stripped ${strippedAnchors} empty anchor tag${strippedAnchors > 1 ? 's' : ''}`);
|
|
104
|
+
}
|
|
105
|
+
return { content: outputLines.join('\n'), fixes };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Infer language for a code block based on content heuristics.
|
|
109
|
+
* More specific languages are checked first to avoid false positives
|
|
110
|
+
* (e.g., Rust's `let mut` matching TypeScript's `let`).
|
|
111
|
+
*/
|
|
112
|
+
function inferLanguage(code) {
|
|
113
|
+
// Rust indicators (check before TS — `let mut` and `fn` are more specific)
|
|
114
|
+
if (/\bfn\s+/.test(code) || /\blet\s+mut\b/.test(code) || /\buse\s+\w+::/.test(code)) {
|
|
115
|
+
return 'rust';
|
|
116
|
+
}
|
|
117
|
+
// Go indicators (check before TS — `func` and `package` are unambiguous)
|
|
118
|
+
if (/\bfunc\s+/.test(code) || /\bpackage\s+/.test(code)) {
|
|
119
|
+
return 'go';
|
|
120
|
+
}
|
|
121
|
+
// Python indicators (`def ` and `class...:` are unambiguous)
|
|
122
|
+
if (/\bdef\s+\w/.test(code) || /\bclass\s+\w[^{]*:\s*$/m.test(code)) {
|
|
123
|
+
return 'python';
|
|
124
|
+
}
|
|
125
|
+
// TypeScript / JavaScript indicators
|
|
126
|
+
if (/\b(const|let|import|export|interface|type)\b/.test(code) || /=>/.test(code)) {
|
|
127
|
+
return 'typescript';
|
|
128
|
+
}
|
|
129
|
+
// Bash / shell indicators
|
|
130
|
+
if (/\$\s/.test(code) || /\b(npm|brew|apt|cd|mkdir|echo|curl|wget)\b/.test(code)) {
|
|
131
|
+
return 'bash';
|
|
132
|
+
}
|
|
133
|
+
// Python import (checked after TS since `import` is ambiguous)
|
|
134
|
+
if (/^\s*import\s+\w/m.test(code) || /^\s*from\s+\w+\s+import\b/m.test(code)) {
|
|
135
|
+
return 'python';
|
|
136
|
+
}
|
|
137
|
+
return 'text';
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Fix code block issues:
|
|
141
|
+
* - Infer language for code blocks missing a language specifier
|
|
142
|
+
* - Close unclosed code blocks at end of file
|
|
143
|
+
*/
|
|
144
|
+
export function fixCodeBlocks(content) {
|
|
145
|
+
const fixes = [];
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
const outputLines = [];
|
|
148
|
+
let inCodeBlock = false;
|
|
149
|
+
let inferredCount = 0;
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
if (line.startsWith('```') && !inCodeBlock) {
|
|
153
|
+
// Opening code fence
|
|
154
|
+
inCodeBlock = true;
|
|
155
|
+
const lang = line.slice(3).trim().split(/\s/)[0] || '';
|
|
156
|
+
if (!lang) {
|
|
157
|
+
// Collect lines until closing fence to infer language
|
|
158
|
+
const codeBlockContent = [];
|
|
159
|
+
let j = i + 1;
|
|
160
|
+
while (j < lines.length && !lines[j].startsWith('```')) {
|
|
161
|
+
codeBlockContent.push(lines[j]);
|
|
162
|
+
j++;
|
|
163
|
+
}
|
|
164
|
+
const inferred = inferLanguage(codeBlockContent.join('\n'));
|
|
165
|
+
outputLines.push('```' + inferred);
|
|
166
|
+
inferredCount++;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
outputLines.push(line);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (line.startsWith('```') && inCodeBlock) {
|
|
173
|
+
// Closing code fence
|
|
174
|
+
inCodeBlock = false;
|
|
175
|
+
outputLines.push(line);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
outputLines.push(line);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Close unclosed code block at end of file
|
|
182
|
+
if (inCodeBlock) {
|
|
183
|
+
outputLines.push('```');
|
|
184
|
+
fixes.push('closed unclosed code block at end of file');
|
|
185
|
+
}
|
|
186
|
+
if (inferredCount > 0) {
|
|
187
|
+
fixes.push(`inferred language for ${inferredCount} code block${inferredCount > 1 ? 's' : ''}`);
|
|
188
|
+
}
|
|
189
|
+
return { content: outputLines.join('\n'), fixes };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Fix security issues:
|
|
193
|
+
* - Strip <script>...</script> tags (outside code blocks)
|
|
194
|
+
* - Replace javascript: URLs with #
|
|
195
|
+
* - Strip inline on* event handlers from HTML tags
|
|
196
|
+
* - Add sandbox attribute to unsandboxed <iframe> tags
|
|
197
|
+
*/
|
|
198
|
+
export function fixSecurity(content) {
|
|
199
|
+
const fixes = [];
|
|
200
|
+
const lines = content.split('\n');
|
|
201
|
+
const outputLines = [];
|
|
202
|
+
let inCodeBlock = false;
|
|
203
|
+
let strippedScripts = 0;
|
|
204
|
+
let fixedUrls = 0;
|
|
205
|
+
let strippedHandlers = 0;
|
|
206
|
+
let sandboxedIframes = 0;
|
|
207
|
+
let inScriptTag = false;
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
if (line.startsWith('```')) {
|
|
210
|
+
inCodeBlock = !inCodeBlock;
|
|
211
|
+
outputLines.push(line);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (inCodeBlock) {
|
|
215
|
+
outputLines.push(line);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Handle multi-line script tags
|
|
219
|
+
if (inScriptTag) {
|
|
220
|
+
if (/<\/script>/i.test(line)) {
|
|
221
|
+
inScriptTag = false;
|
|
222
|
+
strippedScripts++;
|
|
223
|
+
}
|
|
224
|
+
// Skip lines inside script tags
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Single-line script tag: <script>...</script>
|
|
228
|
+
if (/<script[\s>][\s\S]*<\/script>/i.test(line)) {
|
|
229
|
+
const cleaned = line.replace(/<script[\s>][\s\S]*?<\/script>/gi, '');
|
|
230
|
+
strippedScripts++;
|
|
231
|
+
if (cleaned.trim()) {
|
|
232
|
+
outputLines.push(cleaned);
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Opening script tag without closing on same line
|
|
237
|
+
if (/<script[\s>]/i.test(line)) {
|
|
238
|
+
inScriptTag = true;
|
|
239
|
+
// Keep any content before the script tag
|
|
240
|
+
const before = line.replace(/<script[\s>][\s\S]*/i, '').trim();
|
|
241
|
+
if (before) {
|
|
242
|
+
outputLines.push(before);
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let processed = line;
|
|
247
|
+
// Replace javascript: URLs with #
|
|
248
|
+
if (/href\s*=\s*["']javascript:/i.test(processed)) {
|
|
249
|
+
processed = processed.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"');
|
|
250
|
+
fixedUrls++;
|
|
251
|
+
}
|
|
252
|
+
// Strip inline on* event handlers
|
|
253
|
+
if (/\s+on\w+\s*=\s*["']/i.test(processed)) {
|
|
254
|
+
processed = processed.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
255
|
+
strippedHandlers++;
|
|
256
|
+
}
|
|
257
|
+
// Add sandbox to unsandboxed iframes
|
|
258
|
+
if (/<iframe\s/i.test(processed) && !/sandbox/i.test(processed)) {
|
|
259
|
+
processed = processed.replace(/<iframe\s/i, '<iframe sandbox ');
|
|
260
|
+
sandboxedIframes++;
|
|
261
|
+
}
|
|
262
|
+
outputLines.push(processed);
|
|
263
|
+
}
|
|
264
|
+
if (strippedScripts > 0) {
|
|
265
|
+
fixes.push(`stripped ${strippedScripts} <script> tag${strippedScripts > 1 ? 's' : ''}`);
|
|
266
|
+
}
|
|
267
|
+
if (fixedUrls > 0) {
|
|
268
|
+
fixes.push(`replaced ${fixedUrls} javascript: URL${fixedUrls > 1 ? 's' : ''}`);
|
|
269
|
+
}
|
|
270
|
+
if (strippedHandlers > 0) {
|
|
271
|
+
fixes.push(`stripped ${strippedHandlers} inline event handler${strippedHandlers > 1 ? 's' : ''}`);
|
|
272
|
+
}
|
|
273
|
+
if (sandboxedIframes > 0) {
|
|
274
|
+
fixes.push(`sandboxed ${sandboxedIframes} <iframe>${sandboxedIframes > 1 ? 's' : ''}`);
|
|
275
|
+
}
|
|
276
|
+
return { content: outputLines.join('\n'), fixes };
|
|
277
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { QAReport } from './types.js';
|
|
2
|
+
export type { QAReport, QACheckResult, QAIssue } from './types.js';
|
|
3
|
+
export type { Severity } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Run embedded QA on a docs output directory.
|
|
6
|
+
* Returns a report with all issues found.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runQA(outputDir: string): QAReport;
|
|
9
|
+
/**
|
|
10
|
+
* Print QA report to stdout
|
|
11
|
+
*/
|
|
12
|
+
export declare function printQAReport(report: QAReport): void;
|
|
13
|
+
export interface FixReport {
|
|
14
|
+
filesFixed: number;
|
|
15
|
+
totalFixes: number;
|
|
16
|
+
fixes: Array<{
|
|
17
|
+
file: string;
|
|
18
|
+
fixes: string[];
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Auto-fix safe QA issues in a docs output directory.
|
|
23
|
+
* Runs conservative fix functions on all .md/.mdx files and writes changes back.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fixQAIssues(outputDir: string): FixReport;
|
|
26
|
+
/**
|
|
27
|
+
* Print fix report to stdout
|
|
28
|
+
*/
|
|
29
|
+
export declare function printFixReport(report: FixReport): void;
|
package/dist/qa/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { checkFrontmatter, checkHeadings, checkCodeBlocks, checkComponents, checkLinks, checkSecurity, checkContentQuality, checkMdxSyntax, checkScreenshots, } from './checks.js';
|
|
4
|
+
import { fixFrontmatter, fixMdxSyntax, fixCodeBlocks, fixSecurity, } from './fixes.js';
|
|
5
|
+
/**
|
|
6
|
+
* Recursively find all .md and .mdx files in a directory
|
|
7
|
+
*/
|
|
8
|
+
function findDocFiles(dir, maxDepth = 20) {
|
|
9
|
+
const files = [];
|
|
10
|
+
function walk(currentDir, depth) {
|
|
11
|
+
if (depth > maxDepth)
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
17
|
+
continue;
|
|
18
|
+
const fullPath = join(currentDir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
walk(fullPath, depth + 1);
|
|
21
|
+
}
|
|
22
|
+
else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
|
|
23
|
+
// Skip metadata files
|
|
24
|
+
const name = entry.name.replace(/\.(mdx?|md)$/, '');
|
|
25
|
+
if (name.startsWith('_') || name.startsWith('llms') || name === 'README')
|
|
26
|
+
continue;
|
|
27
|
+
files.push(fullPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Directory not accessible
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
walk(dir, 0);
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run a single check across all files and return results
|
|
40
|
+
*/
|
|
41
|
+
function runCheck(name, files, checkFn) {
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const issues = [];
|
|
44
|
+
const allFilePaths = new Set(files.map(f => f.relPath));
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
try {
|
|
47
|
+
const fileIssues = checkFn(file.content, file.relPath, allFilePaths);
|
|
48
|
+
issues.push(...fileIssues);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Check failed for this file — skip
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
check: name,
|
|
56
|
+
passed: issues.filter(i => i.severity === 'error').length === 0,
|
|
57
|
+
issues,
|
|
58
|
+
duration: Date.now() - start,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run embedded QA on a docs output directory.
|
|
63
|
+
* Returns a report with all issues found.
|
|
64
|
+
*/
|
|
65
|
+
export function runQA(outputDir) {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
// Find all doc files
|
|
68
|
+
const filePaths = findDocFiles(outputDir);
|
|
69
|
+
// Read all files
|
|
70
|
+
const files = filePaths.map(p => ({
|
|
71
|
+
path: p,
|
|
72
|
+
content: readFileSync(p, 'utf-8'),
|
|
73
|
+
relPath: relative(outputDir, p),
|
|
74
|
+
}));
|
|
75
|
+
// Run all checks
|
|
76
|
+
const checks = [
|
|
77
|
+
runCheck('frontmatter', files, (content, relPath) => checkFrontmatter('', content, relPath)),
|
|
78
|
+
runCheck('headings', files, (content, relPath) => checkHeadings(content, relPath)),
|
|
79
|
+
runCheck('code-blocks', files, (content, relPath) => checkCodeBlocks(content, relPath)),
|
|
80
|
+
runCheck('components', files, (content, relPath) => checkComponents(content, relPath)),
|
|
81
|
+
runCheck('links', files, (content, relPath, allFiles) => checkLinks(content, relPath, allFiles)),
|
|
82
|
+
runCheck('security', files, (content, relPath) => checkSecurity(content, relPath)),
|
|
83
|
+
runCheck('content', files, (content, relPath) => checkContentQuality(content, relPath)),
|
|
84
|
+
runCheck('mdx-syntax', files, (content, relPath) => checkMdxSyntax(content, relPath)),
|
|
85
|
+
runCheck('screenshots', files, (content, relPath) => checkScreenshots(content, relPath, outputDir)),
|
|
86
|
+
];
|
|
87
|
+
// Aggregate
|
|
88
|
+
const allIssues = checks.flatMap(c => c.issues);
|
|
89
|
+
const errors = allIssues.filter(i => i.severity === 'error').length;
|
|
90
|
+
const warnings = allIssues.filter(i => i.severity === 'warning').length;
|
|
91
|
+
const infos = allIssues.filter(i => i.severity === 'info').length;
|
|
92
|
+
return {
|
|
93
|
+
outputDir,
|
|
94
|
+
filesChecked: files.length,
|
|
95
|
+
checks,
|
|
96
|
+
errors,
|
|
97
|
+
warnings,
|
|
98
|
+
infos,
|
|
99
|
+
passed: errors === 0,
|
|
100
|
+
duration: Date.now() - startTime,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Print QA report to stdout
|
|
105
|
+
*/
|
|
106
|
+
export function printQAReport(report) {
|
|
107
|
+
const statusIcon = report.passed ? '✓' : '✗';
|
|
108
|
+
const statusLabel = report.passed ? 'PASSED' : 'FAILED';
|
|
109
|
+
console.log(`\n QA ${statusIcon} ${statusLabel}`);
|
|
110
|
+
console.log(` ${report.filesChecked} files checked in ${report.duration}ms`);
|
|
111
|
+
// Summary line
|
|
112
|
+
const parts = [];
|
|
113
|
+
if (report.errors > 0)
|
|
114
|
+
parts.push(`${report.errors} error${report.errors > 1 ? 's' : ''}`);
|
|
115
|
+
if (report.warnings > 0)
|
|
116
|
+
parts.push(`${report.warnings} warning${report.warnings > 1 ? 's' : ''}`);
|
|
117
|
+
if (report.infos > 0)
|
|
118
|
+
parts.push(`${report.infos} info${report.infos > 1 ? 's' : ''}`);
|
|
119
|
+
if (parts.length > 0) {
|
|
120
|
+
console.log(` ${parts.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
// Show errors and warnings (not infos unless verbose)
|
|
123
|
+
const significant = report.checks
|
|
124
|
+
.flatMap(c => c.issues)
|
|
125
|
+
.filter(i => i.severity === 'error' || i.severity === 'warning');
|
|
126
|
+
if (significant.length > 0) {
|
|
127
|
+
console.log('');
|
|
128
|
+
const shown = significant.slice(0, 20);
|
|
129
|
+
for (const issue of shown) {
|
|
130
|
+
const icon = issue.severity === 'error' ? '✗' : '⚠';
|
|
131
|
+
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
132
|
+
console.log(` ${icon} [${issue.check}] ${loc}: ${issue.message}`);
|
|
133
|
+
}
|
|
134
|
+
if (significant.length > 20) {
|
|
135
|
+
console.log(` ... and ${significant.length - 20} more`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Auto-fix safe QA issues in a docs output directory.
|
|
141
|
+
* Runs conservative fix functions on all .md/.mdx files and writes changes back.
|
|
142
|
+
*/
|
|
143
|
+
export function fixQAIssues(outputDir) {
|
|
144
|
+
const filePaths = findDocFiles(outputDir);
|
|
145
|
+
const result = { filesFixed: 0, totalFixes: 0, fixes: [] };
|
|
146
|
+
for (const filePath of filePaths) {
|
|
147
|
+
const original = readFileSync(filePath, 'utf-8');
|
|
148
|
+
const relPath = relative(outputDir, filePath);
|
|
149
|
+
let content = original;
|
|
150
|
+
const allFixes = [];
|
|
151
|
+
// Run each fix function in sequence
|
|
152
|
+
const frontmatterResult = fixFrontmatter(content, filePath);
|
|
153
|
+
content = frontmatterResult.content;
|
|
154
|
+
allFixes.push(...frontmatterResult.fixes);
|
|
155
|
+
const mdxResult = fixMdxSyntax(content);
|
|
156
|
+
content = mdxResult.content;
|
|
157
|
+
allFixes.push(...mdxResult.fixes);
|
|
158
|
+
const codeResult = fixCodeBlocks(content);
|
|
159
|
+
content = codeResult.content;
|
|
160
|
+
allFixes.push(...codeResult.fixes);
|
|
161
|
+
const securityResult = fixSecurity(content);
|
|
162
|
+
content = securityResult.content;
|
|
163
|
+
allFixes.push(...securityResult.fixes);
|
|
164
|
+
// Only write if content actually changed
|
|
165
|
+
if (content !== original) {
|
|
166
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
167
|
+
result.filesFixed++;
|
|
168
|
+
result.totalFixes += allFixes.length;
|
|
169
|
+
result.fixes.push({ file: relPath, fixes: allFixes });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Print fix report to stdout
|
|
176
|
+
*/
|
|
177
|
+
export function printFixReport(report) {
|
|
178
|
+
if (report.totalFixes === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(` Auto-fixed ${report.totalFixes} issue${report.totalFixes > 1 ? 's' : ''} in ${report.filesFixed} file${report.filesFixed > 1 ? 's' : ''}`);
|
|
182
|
+
for (const entry of report.fixes) {
|
|
183
|
+
for (const fix of entry.fixes) {
|
|
184
|
+
console.log(` \u2713 ${entry.file}: ${fix}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
2
|
+
export interface QAIssue {
|
|
3
|
+
file: string;
|
|
4
|
+
line?: number;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
check: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface QACheckResult {
|
|
10
|
+
check: string;
|
|
11
|
+
passed: boolean;
|
|
12
|
+
issues: QAIssue[];
|
|
13
|
+
duration: number;
|
|
14
|
+
}
|
|
15
|
+
export interface QAReport {
|
|
16
|
+
outputDir: string;
|
|
17
|
+
filesChecked: number;
|
|
18
|
+
checks: QACheckResult[];
|
|
19
|
+
errors: number;
|
|
20
|
+
warnings: number;
|
|
21
|
+
infos: number;
|
|
22
|
+
passed: boolean;
|
|
23
|
+
duration: number;
|
|
24
|
+
}
|
package/dist/qa/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Scanner, ScanResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scanner for C# source files
|
|
4
|
+
* Extracts: public classes, interfaces, enums, structs, records, methods
|
|
5
|
+
*/
|
|
6
|
+
export declare class CSharpScanner implements Scanner {
|
|
7
|
+
languages: string[];
|
|
8
|
+
canHandle(filePath: string): boolean;
|
|
9
|
+
scanFile(filePath: string): Promise<ScanResult>;
|
|
10
|
+
private extractImports;
|
|
11
|
+
private extractTypes;
|
|
12
|
+
private extractMethods;
|
|
13
|
+
private extractInterfaceMethods;
|
|
14
|
+
private findClassRanges;
|
|
15
|
+
private findMatchingBrace;
|
|
16
|
+
private findParentClass;
|
|
17
|
+
private parseCSharpParams;
|
|
18
|
+
private splitParams;
|
|
19
|
+
private getLineNumber;
|
|
20
|
+
private getDocComment;
|
|
21
|
+
private extractGenerics;
|
|
22
|
+
private getSourceContext;
|
|
23
|
+
}
|