skrypt-ai 0.6.1 → 0.8.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/audit/doc-parser.d.ts +5 -0
- package/dist/audit/doc-parser.js +106 -0
- package/dist/audit/index.d.ts +4 -0
- package/dist/audit/index.js +4 -0
- package/dist/audit/matcher.d.ts +6 -0
- package/dist/audit/matcher.js +94 -0
- package/dist/audit/reporter.d.ts +9 -0
- package/dist/audit/reporter.js +106 -0
- package/dist/audit/types.d.ts +37 -0
- package/dist/auth/index.js +6 -4
- package/dist/cli.js +12 -2
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +59 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
- package/dist/commands/generate/index.js +393 -0
- package/dist/commands/generate/scan.d.ts +41 -0
- package/dist/commands/generate/scan.js +256 -0
- package/dist/commands/generate/verify.d.ts +14 -0
- package/dist/commands/generate/verify.js +122 -0
- package/dist/commands/generate/write.d.ts +25 -0
- package/dist/commands/generate/write.js +120 -0
- package/dist/commands/import.js +4 -1
- package/dist/commands/llms-txt.js +6 -4
- package/dist/commands/refresh.d.ts +2 -0
- package/dist/commands/refresh.js +158 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.js +110 -0
- package/dist/commands/test.js +177 -236
- package/dist/commands/watch.js +29 -20
- package/dist/config/loader.d.ts +6 -2
- package/dist/config/loader.js +39 -3
- package/dist/config/types.d.ts +7 -0
- package/dist/generator/agents-md.d.ts +25 -0
- package/dist/generator/agents-md.js +122 -0
- package/dist/generator/generator.js +2 -1
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/mdx-serializer.d.ts +11 -0
- package/dist/generator/mdx-serializer.js +135 -0
- package/dist/generator/organizer.d.ts +1 -16
- package/dist/generator/organizer.js +0 -38
- package/dist/generator/types.d.ts +3 -0
- package/dist/generator/writer.js +65 -32
- package/dist/github/org-discovery.d.ts +17 -0
- package/dist/github/org-discovery.js +93 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.js +8 -2
- package/dist/llm/proxy-client.d.ts +32 -0
- package/dist/llm/proxy-client.js +103 -0
- package/dist/next-actions/actions.d.ts +2 -0
- package/dist/next-actions/actions.js +190 -0
- package/dist/next-actions/index.d.ts +6 -0
- package/dist/next-actions/index.js +39 -0
- package/dist/next-actions/setup.d.ts +2 -0
- package/dist/next-actions/setup.js +72 -0
- package/dist/next-actions/state.d.ts +7 -0
- package/dist/next-actions/state.js +68 -0
- package/dist/next-actions/suggest.d.ts +3 -0
- package/dist/next-actions/suggest.js +47 -0
- package/dist/next-actions/types.d.ts +26 -0
- package/dist/refresh/differ.d.ts +9 -0
- package/dist/refresh/differ.js +67 -0
- package/dist/refresh/index.d.ts +4 -0
- package/dist/refresh/index.js +4 -0
- package/dist/refresh/manifest.d.ts +18 -0
- package/dist/refresh/manifest.js +71 -0
- package/dist/refresh/splicer.d.ts +9 -0
- package/dist/refresh/splicer.js +50 -0
- package/dist/refresh/types.d.ts +37 -0
- package/dist/review/index.d.ts +8 -0
- package/dist/review/index.js +94 -0
- package/dist/review/parser.d.ts +16 -0
- package/dist/review/parser.js +95 -0
- package/dist/review/types.d.ts +18 -0
- package/dist/scanner/csharp.d.ts +0 -4
- package/dist/scanner/csharp.js +9 -49
- package/dist/scanner/go.d.ts +0 -3
- package/dist/scanner/go.js +8 -35
- package/dist/scanner/java.d.ts +0 -4
- package/dist/scanner/java.js +9 -49
- package/dist/scanner/kotlin.d.ts +0 -3
- package/dist/scanner/kotlin.js +6 -33
- package/dist/scanner/php.d.ts +0 -10
- package/dist/scanner/php.js +11 -55
- package/dist/scanner/ruby.d.ts +0 -3
- package/dist/scanner/ruby.js +8 -38
- package/dist/scanner/rust.d.ts +0 -3
- package/dist/scanner/rust.js +10 -37
- package/dist/scanner/swift.d.ts +0 -3
- package/dist/scanner/swift.js +8 -35
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/utils.d.ts +41 -0
- package/dist/scanner/utils.js +97 -0
- package/dist/structure/index.d.ts +19 -0
- package/dist/structure/index.js +92 -0
- package/dist/structure/planner.d.ts +8 -0
- package/dist/structure/planner.js +180 -0
- package/dist/structure/topology.d.ts +16 -0
- package/dist/structure/topology.js +49 -0
- package/dist/structure/types.d.ts +26 -0
- package/dist/template/docs.json +5 -2
- package/dist/template/next.config.mjs +31 -0
- package/dist/template/package.json +5 -3
- package/dist/template/src/app/layout.tsx +13 -13
- package/dist/template/src/app/llms-full.md/route.ts +29 -0
- package/dist/template/src/app/llms.txt/route.ts +29 -0
- package/dist/template/src/app/md/[...slug]/route.ts +174 -0
- package/dist/template/src/app/reference/route.ts +22 -18
- package/dist/template/src/app/sitemap.ts +1 -1
- package/dist/template/src/components/ai-chat-impl.tsx +206 -0
- package/dist/template/src/components/ai-chat.tsx +20 -193
- package/dist/template/src/components/mdx/index.tsx +27 -4
- package/dist/template/src/lib/fonts.ts +135 -0
- package/dist/template/src/middleware.ts +101 -0
- package/dist/template/src/styles/globals.css +28 -20
- package/dist/testing/comparator.d.ts +7 -0
- package/dist/testing/comparator.js +77 -0
- package/dist/testing/docker.d.ts +21 -0
- package/dist/testing/docker.js +234 -0
- package/dist/testing/env.d.ts +16 -0
- package/dist/testing/env.js +58 -0
- package/dist/testing/extractor.d.ts +9 -0
- package/dist/testing/extractor.js +195 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/runner.d.ts +5 -0
- package/dist/testing/runner.js +225 -0
- package/dist/testing/types.d.ts +58 -0
- package/dist/utils/files.d.ts +0 -8
- package/dist/utils/files.js +0 -33
- package/package.json +1 -1
- package/dist/autofix/autofix.test.js +0 -487
- package/dist/commands/generate.js +0 -445
- package/dist/generator/generator.test.js +0 -259
- package/dist/generator/writer.test.js +0 -411
- package/dist/llm/llm.manual-test.js +0 -112
- package/dist/llm/llm.mock-test.d.ts +0 -4
- package/dist/llm/llm.mock-test.js +0 -79
- package/dist/plugins/index.d.ts +0 -47
- package/dist/plugins/index.js +0 -181
- package/dist/scanner/content-type.test.js +0 -231
- package/dist/scanner/integration.test.d.ts +0 -4
- package/dist/scanner/integration.test.js +0 -180
- package/dist/scanner/scanner.test.js +0 -210
- package/dist/scanner/typescript.manual-test.d.ts +0 -1
- package/dist/scanner/typescript.manual-test.js +0 -112
- package/dist/template/src/app/docs/auth/page.mdx +0 -589
- package/dist/template/src/app/docs/autofix/page.mdx +0 -624
- package/dist/template/src/app/docs/cli/page.mdx +0 -217
- package/dist/template/src/app/docs/config/page.mdx +0 -428
- package/dist/template/src/app/docs/configuration/page.mdx +0 -86
- package/dist/template/src/app/docs/deployment/page.mdx +0 -112
- package/dist/template/src/app/docs/generator/generator.md +0 -504
- package/dist/template/src/app/docs/generator/organizer.md +0 -779
- package/dist/template/src/app/docs/generator/page.mdx +0 -613
- package/dist/template/src/app/docs/github/page.mdx +0 -502
- package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
- package/dist/template/src/app/docs/llm/index.md +0 -471
- package/dist/template/src/app/docs/llm/page.mdx +0 -428
- package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
- package/dist/template/src/app/docs/pro/page.mdx +0 -121
- package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
- package/dist/template/src/app/docs/scanner/content-type.md +0 -599
- package/dist/template/src/app/docs/scanner/index.md +0 -212
- package/dist/template/src/app/docs/scanner/page.mdx +0 -307
- package/dist/template/src/app/docs/scanner/python.md +0 -469
- package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
- package/dist/template/src/app/docs/scanner/rust.md +0 -325
- package/dist/template/src/app/docs/scanner/typescript.md +0 -201
- package/dist/template/src/app/icon.tsx +0 -29
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -12
- /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
- /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
- /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
- /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
- /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
- /package/dist/{scanner/scanner.test.d.ts → testing/types.js} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
const SUPPORTED_LANGUAGES = ['typescript', 'ts', 'javascript', 'js', 'python', 'py'];
|
|
4
|
+
/**
|
|
5
|
+
* Normalize language to canonical form for comparison
|
|
6
|
+
*/
|
|
7
|
+
const LANGUAGE_CANONICAL = {
|
|
8
|
+
typescript: 'typescript',
|
|
9
|
+
ts: 'typescript',
|
|
10
|
+
javascript: 'javascript',
|
|
11
|
+
js: 'javascript',
|
|
12
|
+
python: 'python',
|
|
13
|
+
py: 'python',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Find all .md/.mdx files in a directory recursively
|
|
17
|
+
*/
|
|
18
|
+
export function findDocFiles(dir) {
|
|
19
|
+
const files = [];
|
|
20
|
+
function walk(currentDir) {
|
|
21
|
+
const entries = readdirSync(currentDir);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = join(currentDir, entry);
|
|
24
|
+
const stat = statSync(fullPath);
|
|
25
|
+
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
26
|
+
walk(fullPath);
|
|
27
|
+
}
|
|
28
|
+
else if (stat.isFile() && (extname(entry) === '.mdx' || extname(entry) === '.md')) {
|
|
29
|
+
files.push(fullPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
walk(dir);
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a multi-line expected output block.
|
|
38
|
+
* Supports:
|
|
39
|
+
* // Output: single line
|
|
40
|
+
* // Output (contains): partial match
|
|
41
|
+
* // Output:
|
|
42
|
+
* // line1
|
|
43
|
+
* // line2
|
|
44
|
+
*/
|
|
45
|
+
function parseExpectedOutput(code) {
|
|
46
|
+
const lines = code.split('\n');
|
|
47
|
+
let output;
|
|
48
|
+
let mode = 'exact';
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const line = lines[i].trim();
|
|
51
|
+
// Check for "// Output (contains): ..."
|
|
52
|
+
const containsMatch = line.match(/^\/\/\s*Output\s*\(contains\)\s*:\s*(.*)$/);
|
|
53
|
+
if (containsMatch) {
|
|
54
|
+
mode = 'contains';
|
|
55
|
+
const value = containsMatch[1].trim();
|
|
56
|
+
if (value) {
|
|
57
|
+
output = value;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Multi-line: collect subsequent // prefixed lines
|
|
61
|
+
output = collectMultiLineOutput(lines, i + 1);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
// Check for "// Output: ..."
|
|
66
|
+
const exactMatch = line.match(/^\/\/\s*Output\s*:\s*(.*)$/);
|
|
67
|
+
if (exactMatch) {
|
|
68
|
+
const value = exactMatch[1].trim();
|
|
69
|
+
if (value) {
|
|
70
|
+
output = value;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
output = collectMultiLineOutput(lines, i + 1);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// Python: # Output: ...
|
|
78
|
+
const pyContainsMatch = line.match(/^#\s*Output\s*\(contains\)\s*:\s*(.*)$/);
|
|
79
|
+
if (pyContainsMatch) {
|
|
80
|
+
mode = 'contains';
|
|
81
|
+
const value = pyContainsMatch[1].trim();
|
|
82
|
+
if (value) {
|
|
83
|
+
output = value;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
output = collectMultiLineOutput(lines, i + 1, '#');
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const pyMatch = line.match(/^#\s*Output\s*:\s*(.*)$/);
|
|
91
|
+
if (pyMatch) {
|
|
92
|
+
const value = pyMatch[1].trim();
|
|
93
|
+
if (value) {
|
|
94
|
+
output = value;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
output = collectMultiLineOutput(lines, i + 1, '#');
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { output, mode };
|
|
103
|
+
}
|
|
104
|
+
function collectMultiLineOutput(lines, startIdx, commentChar = '//') {
|
|
105
|
+
const outputLines = [];
|
|
106
|
+
for (let j = startIdx; j < lines.length; j++) {
|
|
107
|
+
const l = lines[j].trim();
|
|
108
|
+
if (commentChar === '//') {
|
|
109
|
+
if (l.startsWith('//')) {
|
|
110
|
+
outputLines.push(l.replace(/^\/\/\s?/, ''));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (l.startsWith('#')) {
|
|
118
|
+
outputLines.push(l.replace(/^#\s?/, ''));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return outputLines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse directive comments from a code snippet
|
|
129
|
+
*/
|
|
130
|
+
function parseDirectives(code, language) {
|
|
131
|
+
const commentPrefix = ['python', 'py'].includes(language) ? '#' : '//';
|
|
132
|
+
const lines = code.split('\n');
|
|
133
|
+
let requiredEnv;
|
|
134
|
+
let skipReason;
|
|
135
|
+
let dependencies;
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
// Requires: ENV_VAR1, ENV_VAR2
|
|
139
|
+
const reqMatch = trimmed.match(new RegExp(`^${commentPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*Requires:\\s*(.+)$`));
|
|
140
|
+
if (reqMatch) {
|
|
141
|
+
requiredEnv = reqMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
142
|
+
}
|
|
143
|
+
// Skip: reason
|
|
144
|
+
const skipMatch = trimmed.match(new RegExp(`^${commentPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*Skip:\\s*(.+)$`));
|
|
145
|
+
if (skipMatch) {
|
|
146
|
+
skipReason = skipMatch[1].trim();
|
|
147
|
+
}
|
|
148
|
+
// Deps: pkg1, pkg2
|
|
149
|
+
const depsMatch = trimmed.match(new RegExp(`^${commentPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*Deps:\\s*(.+)$`));
|
|
150
|
+
if (depsMatch) {
|
|
151
|
+
dependencies = depsMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { requiredEnv, skipReason, dependencies };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extract code blocks from a markdown/MDX file
|
|
158
|
+
*/
|
|
159
|
+
export function extractSnippets(filePath, languageFilter) {
|
|
160
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
161
|
+
const snippets = [];
|
|
162
|
+
const codeBlockRegex = /```(\w+)?[^\n]*\n([\s\S]*?)```/g;
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
165
|
+
const language = (match[1] || '').toLowerCase();
|
|
166
|
+
const code = (match[2] || '').trim();
|
|
167
|
+
if (languageFilter) {
|
|
168
|
+
const filterCanonical = LANGUAGE_CANONICAL[languageFilter.toLowerCase()] || languageFilter.toLowerCase();
|
|
169
|
+
const langCanonical = LANGUAGE_CANONICAL[language] || language;
|
|
170
|
+
if (langCanonical !== filterCanonical) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!SUPPORTED_LANGUAGES.includes(language)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const beforeMatch = content.substring(0, match.index);
|
|
178
|
+
const lineNumber = beforeMatch.split('\n').length;
|
|
179
|
+
const { output, mode } = parseExpectedOutput(code);
|
|
180
|
+
const directives = parseDirectives(code, language);
|
|
181
|
+
snippets.push({
|
|
182
|
+
code,
|
|
183
|
+
language,
|
|
184
|
+
filePath,
|
|
185
|
+
lineNumber,
|
|
186
|
+
index: snippets.length,
|
|
187
|
+
expectedOutput: output,
|
|
188
|
+
expectedOutputMode: mode,
|
|
189
|
+
requiredEnv: directives.requiredEnv,
|
|
190
|
+
skipReason: directives.skipReason,
|
|
191
|
+
dependencies: directives.dependencies,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return snippets;
|
|
195
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ExtractedSnippet, TestResult, RunnerConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run a snippet locally in an isolated temp directory with a clean environment
|
|
4
|
+
*/
|
|
5
|
+
export declare function runLocally(snippet: ExtractedSnippet, config: RunnerConfig): Promise<TestResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { compareOutput } from './comparator.js';
|
|
7
|
+
import { buildCleanEnv, checkRequiredEnv } from './env.js';
|
|
8
|
+
/**
|
|
9
|
+
* Run a snippet locally in an isolated temp directory with a clean environment
|
|
10
|
+
*/
|
|
11
|
+
export async function runLocally(snippet, config) {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
// Check for skip directive
|
|
14
|
+
if (snippet.skipReason) {
|
|
15
|
+
return {
|
|
16
|
+
snippet,
|
|
17
|
+
status: 'skip',
|
|
18
|
+
stdout: '',
|
|
19
|
+
stderr: `Skipped: ${snippet.skipReason}`,
|
|
20
|
+
exitCode: 0,
|
|
21
|
+
duration: 0,
|
|
22
|
+
environment: 'local',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Check required env vars
|
|
26
|
+
if (snippet.requiredEnv && snippet.requiredEnv.length > 0) {
|
|
27
|
+
const { ok, missing } = checkRequiredEnv(snippet.requiredEnv, config.envVars);
|
|
28
|
+
if (!ok) {
|
|
29
|
+
return {
|
|
30
|
+
snippet,
|
|
31
|
+
status: 'skip',
|
|
32
|
+
stdout: '',
|
|
33
|
+
stderr: `Missing required env vars: ${missing.join(', ')}`,
|
|
34
|
+
exitCode: 0,
|
|
35
|
+
duration: 0,
|
|
36
|
+
environment: 'local',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const tempDir = join(tmpdir(), `skrypt-test-${randomUUID()}`);
|
|
41
|
+
mkdirSync(tempDir, { recursive: true });
|
|
42
|
+
try {
|
|
43
|
+
// Install dependencies if needed
|
|
44
|
+
if (config.installDeps && snippet.dependencies && snippet.dependencies.length > 0) {
|
|
45
|
+
const isPython = ['python', 'py'].includes(snippet.language);
|
|
46
|
+
if (isPython) {
|
|
47
|
+
await installPythonDeps(snippet.dependencies, tempDir, config.envVars);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
await installNodeDeps(snippet.dependencies, tempDir, config.envVars);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const { command, args, tempFile } = prepareExecution(snippet, tempDir);
|
|
54
|
+
if (!command) {
|
|
55
|
+
return {
|
|
56
|
+
snippet,
|
|
57
|
+
status: 'fail',
|
|
58
|
+
stdout: '',
|
|
59
|
+
stderr: `Unsupported language: ${snippet.language}`,
|
|
60
|
+
exitCode: 1,
|
|
61
|
+
duration: Date.now() - startTime,
|
|
62
|
+
environment: 'local',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
writeFileSync(tempFile, snippet.code);
|
|
66
|
+
const cleanEnv = buildCleanEnv(config.envVars);
|
|
67
|
+
// Set TMPDIR to the snippet's temp dir for extra isolation
|
|
68
|
+
cleanEnv.TMPDIR = tempDir;
|
|
69
|
+
const result = await executeWithTimeout(command, args, config.timeout, tempDir, cleanEnv);
|
|
70
|
+
const duration = Date.now() - startTime;
|
|
71
|
+
// Check output if expected
|
|
72
|
+
if (snippet.expectedOutput !== undefined) {
|
|
73
|
+
const comparison = compareOutput(result.stdout, snippet.expectedOutput, snippet.expectedOutputMode || 'exact');
|
|
74
|
+
if (!comparison.match) {
|
|
75
|
+
return {
|
|
76
|
+
snippet,
|
|
77
|
+
status: 'output_mismatch',
|
|
78
|
+
stdout: result.stdout,
|
|
79
|
+
stderr: result.stderr,
|
|
80
|
+
exitCode: result.exitCode,
|
|
81
|
+
duration,
|
|
82
|
+
environment: 'local',
|
|
83
|
+
outputMatch: false,
|
|
84
|
+
diff: comparison.diff,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
snippet,
|
|
89
|
+
status: result.exitCode === 0 ? 'pass' : 'fail',
|
|
90
|
+
stdout: result.stdout,
|
|
91
|
+
stderr: result.stderr,
|
|
92
|
+
exitCode: result.exitCode,
|
|
93
|
+
duration,
|
|
94
|
+
environment: 'local',
|
|
95
|
+
outputMatch: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
snippet,
|
|
100
|
+
status: result.timedOut ? 'timeout' : (result.exitCode === 0 ? 'pass' : 'fail'),
|
|
101
|
+
stdout: result.stdout,
|
|
102
|
+
stderr: result.timedOut
|
|
103
|
+
? `Timeout: execution exceeded ${config.timeout}ms`
|
|
104
|
+
: result.stderr,
|
|
105
|
+
exitCode: result.exitCode,
|
|
106
|
+
duration,
|
|
107
|
+
environment: 'local',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
+
return {
|
|
113
|
+
snippet,
|
|
114
|
+
status: 'fail',
|
|
115
|
+
stdout: '',
|
|
116
|
+
stderr: message,
|
|
117
|
+
exitCode: 1,
|
|
118
|
+
duration: Date.now() - startTime,
|
|
119
|
+
environment: 'local',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
try {
|
|
124
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore cleanup errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Determine the command and args to execute a snippet
|
|
133
|
+
*/
|
|
134
|
+
function prepareExecution(snippet, tempDir) {
|
|
135
|
+
const isTS = ['typescript', 'ts'].includes(snippet.language);
|
|
136
|
+
const isJS = ['javascript', 'js'].includes(snippet.language);
|
|
137
|
+
const isPy = ['python', 'py'].includes(snippet.language);
|
|
138
|
+
if (isTS) {
|
|
139
|
+
const tempFile = join(tempDir, 'test.ts');
|
|
140
|
+
return { command: 'npx', args: ['tsx', tempFile], tempFile };
|
|
141
|
+
}
|
|
142
|
+
if (isJS) {
|
|
143
|
+
const tempFile = join(tempDir, 'test.js');
|
|
144
|
+
return { command: 'node', args: [tempFile], tempFile };
|
|
145
|
+
}
|
|
146
|
+
if (isPy) {
|
|
147
|
+
const tempFile = join(tempDir, 'test.py');
|
|
148
|
+
return { command: 'python3', args: [tempFile], tempFile };
|
|
149
|
+
}
|
|
150
|
+
return { command: null, args: [], tempFile: join(tempDir, 'test.txt') };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Install Node.js dependencies in a temp directory
|
|
154
|
+
*/
|
|
155
|
+
async function installNodeDeps(deps, tempDir, envVars) {
|
|
156
|
+
const pkg = { name: 'skrypt-test', private: true, dependencies: {} };
|
|
157
|
+
for (const dep of deps) {
|
|
158
|
+
pkg.dependencies[dep] = '*';
|
|
159
|
+
}
|
|
160
|
+
writeFileSync(join(tempDir, 'package.json'), JSON.stringify(pkg));
|
|
161
|
+
const env = buildCleanEnv(envVars);
|
|
162
|
+
await executeWithTimeout('npm', ['install', '--prefix', tempDir, '--no-audit', '--no-fund'], 30000, tempDir, env);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Install Python dependencies in a temp directory using pip
|
|
166
|
+
*/
|
|
167
|
+
async function installPythonDeps(deps, tempDir, envVars) {
|
|
168
|
+
const env = buildCleanEnv(envVars);
|
|
169
|
+
env.PIP_TARGET = join(tempDir, 'pip_packages');
|
|
170
|
+
env.PYTHONPATH = join(tempDir, 'pip_packages');
|
|
171
|
+
mkdirSync(join(tempDir, 'pip_packages'), { recursive: true });
|
|
172
|
+
await executeWithTimeout('python3', ['-m', 'pip', 'install', '--target', join(tempDir, 'pip_packages'), '--quiet', ...deps], 60000, tempDir, env);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Execute a command with timeout
|
|
176
|
+
*/
|
|
177
|
+
function executeWithTimeout(command, args, timeoutMs, cwd, env) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const proc = spawn(command, args, {
|
|
180
|
+
cwd,
|
|
181
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
182
|
+
env,
|
|
183
|
+
});
|
|
184
|
+
let stdout = '';
|
|
185
|
+
let stderr = '';
|
|
186
|
+
let timedOut = false;
|
|
187
|
+
const MAX_BUFFER = 1024 * 1024; // 1MB cap to prevent OOM
|
|
188
|
+
const timeout = setTimeout(() => {
|
|
189
|
+
timedOut = true;
|
|
190
|
+
proc.kill('SIGKILL');
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
proc.stdout?.on('data', (data) => {
|
|
193
|
+
if (stdout.length < MAX_BUFFER) {
|
|
194
|
+
stdout += data.toString();
|
|
195
|
+
if (stdout.length > MAX_BUFFER)
|
|
196
|
+
stdout = stdout.slice(0, MAX_BUFFER);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
proc.stderr?.on('data', (data) => {
|
|
200
|
+
if (stderr.length < MAX_BUFFER) {
|
|
201
|
+
stderr += data.toString();
|
|
202
|
+
if (stderr.length > MAX_BUFFER)
|
|
203
|
+
stderr = stderr.slice(0, MAX_BUFFER);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
proc.on('close', (code) => {
|
|
207
|
+
clearTimeout(timeout);
|
|
208
|
+
resolve({
|
|
209
|
+
exitCode: timedOut ? 1 : (code ?? 1),
|
|
210
|
+
stdout,
|
|
211
|
+
stderr,
|
|
212
|
+
timedOut,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
proc.on('error', (err) => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
resolve({
|
|
218
|
+
exitCode: 1,
|
|
219
|
+
stdout,
|
|
220
|
+
stderr: err.message,
|
|
221
|
+
timedOut: false,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A code snippet extracted from a documentation file
|
|
3
|
+
*/
|
|
4
|
+
export interface ExtractedSnippet {
|
|
5
|
+
code: string;
|
|
6
|
+
language: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
lineNumber: number;
|
|
9
|
+
index: number;
|
|
10
|
+
expectedOutput?: string;
|
|
11
|
+
expectedOutputMode?: 'exact' | 'contains';
|
|
12
|
+
requiredEnv?: string[];
|
|
13
|
+
skipReason?: string;
|
|
14
|
+
dependencies?: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Result of running a single snippet
|
|
18
|
+
*/
|
|
19
|
+
export interface TestResult {
|
|
20
|
+
snippet: ExtractedSnippet;
|
|
21
|
+
status: 'pass' | 'fail' | 'skip' | 'timeout' | 'output_mismatch';
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
duration: number;
|
|
26
|
+
environment: string;
|
|
27
|
+
outputMatch?: boolean;
|
|
28
|
+
diff?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Aggregate report for a test run
|
|
32
|
+
*/
|
|
33
|
+
export interface VerificationReport {
|
|
34
|
+
total: number;
|
|
35
|
+
passed: number;
|
|
36
|
+
failed: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
outputMismatches: number;
|
|
39
|
+
duration: number;
|
|
40
|
+
results: TestResult[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for the local runner
|
|
44
|
+
*/
|
|
45
|
+
export interface RunnerConfig {
|
|
46
|
+
timeout: number;
|
|
47
|
+
envVars: Record<string, string>;
|
|
48
|
+
installDeps: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for Docker-based multi-environment testing
|
|
52
|
+
*/
|
|
53
|
+
export interface DockerEnvironment {
|
|
54
|
+
name: string;
|
|
55
|
+
image: string;
|
|
56
|
+
command: string;
|
|
57
|
+
languages: string[];
|
|
58
|
+
}
|
package/dist/utils/files.d.ts
CHANGED
|
@@ -7,11 +7,3 @@ export declare function findMdxFiles(dir: string): string[];
|
|
|
7
7
|
* Convert string to URL-safe slug
|
|
8
8
|
*/
|
|
9
9
|
export declare function slugify(str: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Simple YAML frontmatter parser — splits on --- markers.
|
|
12
|
-
* Returns parsed key-value data and remaining content body.
|
|
13
|
-
*/
|
|
14
|
-
export declare function parseFrontmatter(content: string): {
|
|
15
|
-
data: Record<string, unknown>;
|
|
16
|
-
content: string;
|
|
17
|
-
};
|
package/dist/utils/files.js
CHANGED
|
@@ -47,36 +47,3 @@ export function slugify(str) {
|
|
|
47
47
|
.replace(/[^a-z0-9]+/g, '-')
|
|
48
48
|
.replace(/^-|-$/g, '');
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Simple YAML frontmatter parser — splits on --- markers.
|
|
52
|
-
* Returns parsed key-value data and remaining content body.
|
|
53
|
-
*/
|
|
54
|
-
export function parseFrontmatter(content) {
|
|
55
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
56
|
-
if (!match)
|
|
57
|
-
return { data: {}, content };
|
|
58
|
-
const yamlStr = match[1];
|
|
59
|
-
const body = match[2];
|
|
60
|
-
const data = {};
|
|
61
|
-
for (const line of yamlStr.split('\n')) {
|
|
62
|
-
const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
63
|
-
if (!kvMatch)
|
|
64
|
-
continue;
|
|
65
|
-
const key = kvMatch[1];
|
|
66
|
-
let value = kvMatch[2].trim();
|
|
67
|
-
if (typeof value === 'string') {
|
|
68
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
69
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
70
|
-
value = value.slice(1, -1);
|
|
71
|
-
}
|
|
72
|
-
else if (value === 'true')
|
|
73
|
-
value = true;
|
|
74
|
-
else if (value === 'false')
|
|
75
|
-
value = false;
|
|
76
|
-
else if (/^\d+$/.test(value))
|
|
77
|
-
value = parseInt(value, 10);
|
|
78
|
-
}
|
|
79
|
-
data[key] = value;
|
|
80
|
-
}
|
|
81
|
-
return { data, content: body };
|
|
82
|
-
}
|