skrypt-ai 0.6.0 → 0.7.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/audit/types.js +1 -0
- package/dist/auth/index.js +3 -1
- package/dist/cli.js +11 -1
- 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/cron.js +4 -0
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.js +528 -234
- package/dist/commands/refresh.d.ts +2 -0
- package/dist/commands/refresh.js +158 -0
- package/dist/commands/review-pr.js +5 -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 -1
- package/dist/config/loader.js +38 -2
- package/dist/config/types.d.ts +7 -0
- package/dist/generator/generator.js +2 -1
- package/dist/generator/types.d.ts +3 -0
- package/dist/generator/writer.js +60 -28
- 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/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/next-actions/types.js +1 -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/refresh/types.js +1 -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/review/types.js +1 -0
- package/dist/scanner/types.d.ts +2 -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/structure/types.js +1 -0
- 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/testing/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { spawnSync, 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 { checkRequiredEnv } from './env.js';
|
|
8
|
+
/**
|
|
9
|
+
* Available Docker environments for multi-env testing
|
|
10
|
+
*/
|
|
11
|
+
export const DOCKER_ENVIRONMENTS = [
|
|
12
|
+
{ name: 'node-20', image: 'node:20-slim', command: 'node', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
13
|
+
{ name: 'node-18', image: 'node:18-slim', command: 'node', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
14
|
+
{ name: 'python-3.12', image: 'python:3.12-slim', command: 'python3', languages: ['python', 'py'] },
|
|
15
|
+
{ name: 'python-3.10', image: 'python:3.10-slim', command: 'python3', languages: ['python', 'py'] },
|
|
16
|
+
{ name: 'bun', image: 'oven/bun:latest', command: 'bun', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
17
|
+
{ name: 'deno', image: 'denoland/deno:latest', command: 'deno', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Check if Docker is available on this machine
|
|
21
|
+
*/
|
|
22
|
+
export function isDockerAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
const result = spawnSync('docker', ['info'], {
|
|
25
|
+
stdio: 'pipe',
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
});
|
|
28
|
+
return result.status === 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a comma-separated environments string into DockerEnvironment objects
|
|
36
|
+
*/
|
|
37
|
+
export function parseEnvironments(envString) {
|
|
38
|
+
const names = envString.split(',').map(s => s.trim()).filter(Boolean);
|
|
39
|
+
const envs = [];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const env = DOCKER_ENVIRONMENTS.find(e => e.name === name);
|
|
42
|
+
if (env) {
|
|
43
|
+
envs.push(env);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn(` Warning: Unknown environment "${name}". Available: ${DOCKER_ENVIRONMENTS.map(e => e.name).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return envs;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get compatible Docker environments for a given language
|
|
53
|
+
*/
|
|
54
|
+
export function getCompatibleEnvironments(language, environments) {
|
|
55
|
+
return environments.filter(env => env.languages.includes(language));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a snippet in a Docker container
|
|
59
|
+
*/
|
|
60
|
+
export async function runInDocker(snippet, environment, config) {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const envLabel = `docker:${environment.name}`;
|
|
63
|
+
// Check for skip directive
|
|
64
|
+
if (snippet.skipReason) {
|
|
65
|
+
return {
|
|
66
|
+
snippet,
|
|
67
|
+
status: 'skip',
|
|
68
|
+
stdout: '',
|
|
69
|
+
stderr: `Skipped: ${snippet.skipReason}`,
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
duration: 0,
|
|
72
|
+
environment: envLabel,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Check required env vars
|
|
76
|
+
if (snippet.requiredEnv && snippet.requiredEnv.length > 0) {
|
|
77
|
+
const { ok, missing } = checkRequiredEnv(snippet.requiredEnv, config.envVars);
|
|
78
|
+
if (!ok) {
|
|
79
|
+
return {
|
|
80
|
+
snippet,
|
|
81
|
+
status: 'skip',
|
|
82
|
+
stdout: '',
|
|
83
|
+
stderr: `Missing required env vars: ${missing.join(', ')}`,
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
duration: 0,
|
|
86
|
+
environment: envLabel,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const tempDir = join(tmpdir(), `skrypt-docker-${randomUUID()}`);
|
|
91
|
+
mkdirSync(tempDir, { recursive: true });
|
|
92
|
+
try {
|
|
93
|
+
const isTS = ['typescript', 'ts'].includes(snippet.language);
|
|
94
|
+
const isPy = ['python', 'py'].includes(snippet.language);
|
|
95
|
+
let filename;
|
|
96
|
+
let containerCmd;
|
|
97
|
+
if (isPy) {
|
|
98
|
+
filename = 'test.py';
|
|
99
|
+
containerCmd = ['python3', `/work/${filename}`];
|
|
100
|
+
}
|
|
101
|
+
else if (isTS && environment.name === 'bun') {
|
|
102
|
+
filename = 'test.ts';
|
|
103
|
+
containerCmd = ['bun', 'run', `/work/${filename}`];
|
|
104
|
+
}
|
|
105
|
+
else if (isTS && environment.name === 'deno') {
|
|
106
|
+
filename = 'test.ts';
|
|
107
|
+
containerCmd = ['deno', 'run', '--allow-all', `/work/${filename}`];
|
|
108
|
+
}
|
|
109
|
+
else if (isTS) {
|
|
110
|
+
// Node needs tsx for TypeScript
|
|
111
|
+
filename = 'test.ts';
|
|
112
|
+
containerCmd = ['npx', '-y', 'tsx', `/work/${filename}`];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
filename = 'test.js';
|
|
116
|
+
containerCmd = ['node', `/work/${filename}`];
|
|
117
|
+
}
|
|
118
|
+
writeFileSync(join(tempDir, filename), snippet.code);
|
|
119
|
+
// Build Docker args
|
|
120
|
+
const dockerArgs = ['run', '--rm', '-v', `${tempDir}:/work`, '-w', '/work'];
|
|
121
|
+
// Inject env vars
|
|
122
|
+
for (const [key, value] of Object.entries(config.envVars)) {
|
|
123
|
+
dockerArgs.push('-e', `${key}=${value}`);
|
|
124
|
+
}
|
|
125
|
+
dockerArgs.push(environment.image, ...containerCmd);
|
|
126
|
+
const result = await executeDocker(dockerArgs, config.timeout);
|
|
127
|
+
const duration = Date.now() - startTime;
|
|
128
|
+
// Check output if expected
|
|
129
|
+
if (snippet.expectedOutput !== undefined) {
|
|
130
|
+
const comparison = compareOutput(result.stdout, snippet.expectedOutput, snippet.expectedOutputMode || 'exact');
|
|
131
|
+
if (!comparison.match) {
|
|
132
|
+
return {
|
|
133
|
+
snippet,
|
|
134
|
+
status: 'output_mismatch',
|
|
135
|
+
stdout: result.stdout,
|
|
136
|
+
stderr: result.stderr,
|
|
137
|
+
exitCode: result.exitCode,
|
|
138
|
+
duration,
|
|
139
|
+
environment: envLabel,
|
|
140
|
+
outputMatch: false,
|
|
141
|
+
diff: comparison.diff,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
snippet,
|
|
146
|
+
status: result.exitCode === 0 ? 'pass' : 'fail',
|
|
147
|
+
stdout: result.stdout,
|
|
148
|
+
stderr: result.stderr,
|
|
149
|
+
exitCode: result.exitCode,
|
|
150
|
+
duration,
|
|
151
|
+
environment: envLabel,
|
|
152
|
+
outputMatch: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
snippet,
|
|
157
|
+
status: result.timedOut ? 'timeout' : (result.exitCode === 0 ? 'pass' : 'fail'),
|
|
158
|
+
stdout: result.stdout,
|
|
159
|
+
stderr: result.timedOut
|
|
160
|
+
? `Timeout: execution exceeded ${config.timeout}ms`
|
|
161
|
+
: result.stderr,
|
|
162
|
+
exitCode: result.exitCode,
|
|
163
|
+
duration,
|
|
164
|
+
environment: envLabel,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
return {
|
|
170
|
+
snippet,
|
|
171
|
+
status: 'fail',
|
|
172
|
+
stdout: '',
|
|
173
|
+
stderr: message,
|
|
174
|
+
exitCode: 1,
|
|
175
|
+
duration: Date.now() - startTime,
|
|
176
|
+
environment: envLabel,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
try {
|
|
181
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore cleanup errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function executeDocker(args, timeoutMs) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const proc = spawn('docker', args, {
|
|
191
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
192
|
+
});
|
|
193
|
+
let stdout = '';
|
|
194
|
+
let stderr = '';
|
|
195
|
+
let timedOut = false;
|
|
196
|
+
const MAX_BUFFER = 1024 * 1024; // 1MB cap to prevent OOM
|
|
197
|
+
const timeout = setTimeout(() => {
|
|
198
|
+
timedOut = true;
|
|
199
|
+
proc.kill('SIGKILL');
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
proc.stdout?.on('data', (data) => {
|
|
202
|
+
if (stdout.length < MAX_BUFFER) {
|
|
203
|
+
stdout += data.toString();
|
|
204
|
+
if (stdout.length > MAX_BUFFER)
|
|
205
|
+
stdout = stdout.slice(0, MAX_BUFFER);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
proc.stderr?.on('data', (data) => {
|
|
209
|
+
if (stderr.length < MAX_BUFFER) {
|
|
210
|
+
stderr += data.toString();
|
|
211
|
+
if (stderr.length > MAX_BUFFER)
|
|
212
|
+
stderr = stderr.slice(0, MAX_BUFFER);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
proc.on('close', (code) => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
resolve({
|
|
218
|
+
exitCode: timedOut ? 1 : (code ?? 1),
|
|
219
|
+
stdout,
|
|
220
|
+
stderr,
|
|
221
|
+
timedOut,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
proc.on('error', (err) => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
resolve({
|
|
227
|
+
exitCode: 1,
|
|
228
|
+
stdout,
|
|
229
|
+
stderr: err.message,
|
|
230
|
+
timedOut: false,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load environment variables from a .env file
|
|
3
|
+
*/
|
|
4
|
+
export declare function loadEnvFile(envFilePath: string): Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Build a clean environment for snippet execution.
|
|
7
|
+
* Only passes PATH, HOME, and explicitly injected vars.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildCleanEnv(injectedVars: Record<string, string>): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Check if all required env vars are present
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkRequiredEnv(required: string[], available: Record<string, string>): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
missing: string[];
|
|
16
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Load environment variables from a .env file
|
|
4
|
+
*/
|
|
5
|
+
export function loadEnvFile(envFilePath) {
|
|
6
|
+
if (!existsSync(envFilePath)) {
|
|
7
|
+
throw new Error(`Env file not found: ${envFilePath}`);
|
|
8
|
+
}
|
|
9
|
+
const content = readFileSync(envFilePath, 'utf-8');
|
|
10
|
+
const vars = {};
|
|
11
|
+
for (const line of content.split('\n')) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
14
|
+
continue;
|
|
15
|
+
const eqIdx = trimmed.indexOf('=');
|
|
16
|
+
if (eqIdx === -1)
|
|
17
|
+
continue;
|
|
18
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
19
|
+
let value = trimmed.substring(eqIdx + 1).trim();
|
|
20
|
+
// Strip surrounding quotes
|
|
21
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
22
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
23
|
+
value = value.slice(1, -1);
|
|
24
|
+
}
|
|
25
|
+
vars[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return vars;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a clean environment for snippet execution.
|
|
31
|
+
* Only passes PATH, HOME, and explicitly injected vars.
|
|
32
|
+
*/
|
|
33
|
+
export function buildCleanEnv(injectedVars) {
|
|
34
|
+
const clean = {
|
|
35
|
+
NODE_NO_WARNINGS: '1',
|
|
36
|
+
};
|
|
37
|
+
// Carry over PATH so runtimes can be found
|
|
38
|
+
if (process.env.PATH) {
|
|
39
|
+
clean.PATH = process.env.PATH;
|
|
40
|
+
}
|
|
41
|
+
// HOME / USERPROFILE for tools that need it
|
|
42
|
+
if (process.env.HOME) {
|
|
43
|
+
clean.HOME = process.env.HOME;
|
|
44
|
+
}
|
|
45
|
+
if (process.env.USERPROFILE) {
|
|
46
|
+
clean.USERPROFILE = process.env.USERPROFILE;
|
|
47
|
+
}
|
|
48
|
+
// Inject user-provided vars (API keys, etc.)
|
|
49
|
+
Object.assign(clean, injectedVars);
|
|
50
|
+
return clean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if all required env vars are present
|
|
54
|
+
*/
|
|
55
|
+
export function checkRequiredEnv(required, available) {
|
|
56
|
+
const missing = required.filter(key => !available[key]);
|
|
57
|
+
return { ok: missing.length === 0, missing };
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExtractedSnippet } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find all .md/.mdx files in a directory recursively
|
|
4
|
+
*/
|
|
5
|
+
export declare function findDocFiles(dir: string): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Extract code blocks from a markdown/MDX file
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractSnippets(filePath: string, languageFilter?: string): ExtractedSnippet[];
|
|
@@ -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>;
|