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
package/dist/commands/test.js
CHANGED
|
@@ -1,229 +1,136 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import { resolve,
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
6
|
-
import { tmpdir } from 'os';
|
|
7
|
-
import { randomUUID } from 'crypto';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
|
+
import { resolve, relative } from 'path';
|
|
8
4
|
import { requirePro } from '../auth/index.js';
|
|
9
|
-
|
|
5
|
+
import { findDocFiles, extractSnippets, runLocally, isDockerAvailable, parseEnvironments, getCompatibleEnvironments, runInDocker, loadEnvFile, } from '../testing/index.js';
|
|
10
6
|
/**
|
|
11
|
-
*
|
|
7
|
+
* Format duration in human-readable format
|
|
12
8
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
function formatDuration(ms) {
|
|
10
|
+
if (ms < 1000)
|
|
11
|
+
return `${ms}ms`;
|
|
12
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Print a test result line
|
|
16
|
+
*/
|
|
17
|
+
function printResult(result, basePath, verbose) {
|
|
18
|
+
const relPath = relative(basePath, result.snippet.filePath);
|
|
19
|
+
const location = `${relPath}:${result.snippet.lineNumber}`;
|
|
20
|
+
const lang = result.snippet.language;
|
|
21
|
+
const env = result.environment !== 'local' ? ` [${result.environment}]` : '';
|
|
22
|
+
switch (result.status) {
|
|
23
|
+
case 'pass':
|
|
24
|
+
console.log(` \x1b[32m✓\x1b[0m ${location} [${lang}]${env} (${formatDuration(result.duration)})`);
|
|
25
|
+
if (result.outputMatch) {
|
|
26
|
+
console.log(` \x1b[32moutput: ✓ matches\x1b[0m`);
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (verbose && result.stdout) {
|
|
29
|
+
console.log(` \x1b[90mOutput: ${result.stdout.trim().slice(0, 100)}${result.stdout.length > 100 ? '...' : ''}\x1b[0m`);
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
break;
|
|
32
|
+
case 'output_mismatch':
|
|
33
|
+
console.log(` \x1b[33m≠\x1b[0m ${location} [${lang}]${env} (${formatDuration(result.duration)}) output mismatch`);
|
|
34
|
+
if (result.diff) {
|
|
35
|
+
const diffLines = result.diff.split('\n').slice(0, 6);
|
|
36
|
+
for (const line of diffLines) {
|
|
37
|
+
console.log(` \x1b[33m${line}\x1b[0m`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'skip':
|
|
42
|
+
console.log(` \x1b[33m⊘\x1b[0m ${location} [${lang}]${env} — ${result.stderr}`);
|
|
43
|
+
break;
|
|
44
|
+
case 'timeout':
|
|
45
|
+
console.log(` \x1b[31m⏱\x1b[0m ${location} [${lang}]${env} — timeout`);
|
|
46
|
+
break;
|
|
47
|
+
case 'fail':
|
|
48
|
+
console.log(` \x1b[31m✗\x1b[0m ${location} [${lang}]${env} (${formatDuration(result.duration)})`);
|
|
49
|
+
if (result.stderr) {
|
|
50
|
+
const errorLines = result.stderr.trim().split('\n').slice(0, 3);
|
|
51
|
+
for (const line of errorLines) {
|
|
52
|
+
console.log(` \x1b[31m${line}\x1b[0m`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
27
56
|
}
|
|
28
|
-
walk(dir);
|
|
29
|
-
return files;
|
|
30
57
|
}
|
|
31
58
|
/**
|
|
32
|
-
*
|
|
59
|
+
* Print test summary
|
|
33
60
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Skip if language filter is set and doesn't match
|
|
43
|
-
if (languageFilter && !language.toLowerCase().startsWith(languageFilter.toLowerCase())) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
// Skip unsupported languages
|
|
47
|
-
if (!SUPPORTED_LANGUAGES.includes(language.toLowerCase())) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
// Calculate line number
|
|
51
|
-
const beforeMatch = content.substring(0, match.index);
|
|
52
|
-
const lineNumber = beforeMatch.split('\n').length;
|
|
53
|
-
blocks.push({
|
|
54
|
-
code: code.trim(),
|
|
55
|
-
language: language.toLowerCase(),
|
|
56
|
-
file: filePath,
|
|
57
|
-
line: lineNumber,
|
|
58
|
-
index: blocks.length,
|
|
59
|
-
});
|
|
61
|
+
function printSummary(report) {
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log('=== Test Summary ===');
|
|
64
|
+
console.log(` Total: ${report.total}`);
|
|
65
|
+
console.log(` \x1b[32mPassed:\x1b[0m ${report.passed}`);
|
|
66
|
+
console.log(` \x1b[31mFailed:\x1b[0m ${report.failed}`);
|
|
67
|
+
if (report.outputMismatches > 0) {
|
|
68
|
+
console.log(` \x1b[33mOutput mismatch:\x1b[0m ${report.outputMismatches}`);
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
if (report.skipped > 0) {
|
|
71
|
+
console.log(` \x1b[33mSkipped:\x1b[0m ${report.skipped}`);
|
|
72
|
+
}
|
|
73
|
+
console.log(` Duration: ${formatDuration(report.duration)}`);
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
|
-
* Run
|
|
76
|
+
* Run snippets through Docker multi-environment matrix
|
|
65
77
|
*/
|
|
66
|
-
async function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let args;
|
|
77
|
-
if (isTypeScript) {
|
|
78
|
-
tempFile = join(tempDir, 'test.ts');
|
|
79
|
-
writeFileSync(tempFile, block.code);
|
|
80
|
-
// Use tsx for TypeScript execution
|
|
81
|
-
command = 'npx';
|
|
82
|
-
args = ['tsx', tempFile];
|
|
83
|
-
}
|
|
84
|
-
else if (isJavaScript) {
|
|
85
|
-
tempFile = join(tempDir, 'test.js');
|
|
86
|
-
writeFileSync(tempFile, block.code);
|
|
87
|
-
command = 'node';
|
|
88
|
-
args = [tempFile];
|
|
89
|
-
}
|
|
90
|
-
else if (isPython) {
|
|
91
|
-
tempFile = join(tempDir, 'test.py');
|
|
92
|
-
writeFileSync(tempFile, block.code);
|
|
93
|
-
command = 'python3';
|
|
94
|
-
args = [tempFile];
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
return {
|
|
98
|
-
block,
|
|
99
|
-
passed: false,
|
|
100
|
-
error: `Unsupported language: ${block.language}`,
|
|
101
|
-
duration: Date.now() - startTime,
|
|
102
|
-
};
|
|
78
|
+
async function runMatrix(snippets, envString, config, basePath, verbose) {
|
|
79
|
+
if (!isDockerAvailable()) {
|
|
80
|
+
console.log('\n \x1b[33m⚠ Docker not found. Running with local runtimes only.\x1b[0m');
|
|
81
|
+
console.log(' Install Docker for multi-environment testing.\n');
|
|
82
|
+
// Fall back to local execution
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const snippet of snippets) {
|
|
85
|
+
const result = await runLocally(snippet, config);
|
|
86
|
+
results.push(result);
|
|
87
|
+
printResult(result, basePath, verbose);
|
|
103
88
|
}
|
|
104
|
-
|
|
105
|
-
const duration = Date.now() - startTime;
|
|
106
|
-
return {
|
|
107
|
-
block,
|
|
108
|
-
passed: result.exitCode === 0,
|
|
109
|
-
output: result.stdout,
|
|
110
|
-
error: result.stderr || (result.exitCode !== 0 ? `Exit code: ${result.exitCode}` : undefined),
|
|
111
|
-
duration,
|
|
112
|
-
};
|
|
89
|
+
return results;
|
|
113
90
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
passed: false,
|
|
119
|
-
error: message,
|
|
120
|
-
duration: Date.now() - startTime,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
// Cleanup temp directory
|
|
125
|
-
try {
|
|
126
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Ignore cleanup errors — OS will clean tmpdir
|
|
130
|
-
}
|
|
91
|
+
const environments = parseEnvironments(envString);
|
|
92
|
+
if (environments.length === 0) {
|
|
93
|
+
console.error(' No valid environments specified.');
|
|
94
|
+
return [];
|
|
131
95
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const timeout = setTimeout(() => {
|
|
147
|
-
killed = true;
|
|
148
|
-
proc.kill('SIGKILL');
|
|
149
|
-
}, timeoutMs);
|
|
150
|
-
proc.stdout?.on('data', (data) => {
|
|
151
|
-
stdout += data.toString();
|
|
152
|
-
});
|
|
153
|
-
proc.stderr?.on('data', (data) => {
|
|
154
|
-
stderr += data.toString();
|
|
155
|
-
});
|
|
156
|
-
proc.on('close', (code) => {
|
|
157
|
-
clearTimeout(timeout);
|
|
158
|
-
if (killed) {
|
|
159
|
-
resolve({
|
|
160
|
-
exitCode: 1,
|
|
161
|
-
stdout,
|
|
162
|
-
stderr: `Timeout: code execution exceeded ${timeoutMs}ms`,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
resolve({
|
|
167
|
-
exitCode: code ?? 1,
|
|
168
|
-
stdout,
|
|
169
|
-
stderr,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
proc.on('error', (err) => {
|
|
174
|
-
clearTimeout(timeout);
|
|
175
|
-
resolve({
|
|
176
|
-
exitCode: 1,
|
|
177
|
-
stdout,
|
|
178
|
-
stderr: err.message,
|
|
96
|
+
console.log(` Environments: ${environments.map(e => e.name).join(', ')}\n`);
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const snippet of snippets) {
|
|
99
|
+
const compatible = getCompatibleEnvironments(snippet.language, environments);
|
|
100
|
+
if (compatible.length === 0) {
|
|
101
|
+
// No compatible environment — skip
|
|
102
|
+
results.push({
|
|
103
|
+
snippet,
|
|
104
|
+
status: 'skip',
|
|
105
|
+
stdout: '',
|
|
106
|
+
stderr: `No compatible environment for ${snippet.language}`,
|
|
107
|
+
exitCode: 0,
|
|
108
|
+
duration: 0,
|
|
109
|
+
environment: 'none',
|
|
179
110
|
});
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Format duration in human-readable format
|
|
185
|
-
*/
|
|
186
|
-
function formatDuration(ms) {
|
|
187
|
-
if (ms < 1000)
|
|
188
|
-
return `${ms}ms`;
|
|
189
|
-
return `${(ms / 1000).toFixed(2)}s`;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Print test result
|
|
193
|
-
*/
|
|
194
|
-
function printResult(result, basePath, verbose) {
|
|
195
|
-
const relPath = relative(basePath, result.block.file);
|
|
196
|
-
const location = `${relPath}:${result.block.line}`;
|
|
197
|
-
const lang = result.block.language;
|
|
198
|
-
if (result.passed) {
|
|
199
|
-
console.log(` \x1b[32m✓\x1b[0m ${location} [${lang}] (${formatDuration(result.duration)})`);
|
|
200
|
-
if (verbose && result.output) {
|
|
201
|
-
console.log(` \x1b[90mOutput: ${result.output.trim().slice(0, 100)}${result.output.length > 100 ? '...' : ''}\x1b[0m`);
|
|
111
|
+
continue;
|
|
202
112
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
113
|
+
const relPath = relative(basePath, snippet.filePath);
|
|
114
|
+
console.log(` ${relPath}:${snippet.lineNumber} [${snippet.language}]`);
|
|
115
|
+
for (const env of compatible) {
|
|
116
|
+
const result = await runInDocker(snippet, env, config);
|
|
117
|
+
results.push(result);
|
|
118
|
+
const statusIcon = result.status === 'pass' ? '\x1b[32m✓\x1b[0m'
|
|
119
|
+
: result.status === 'output_mismatch' ? '\x1b[33m≠\x1b[0m'
|
|
120
|
+
: result.status === 'skip' ? '\x1b[33m⊘\x1b[0m'
|
|
121
|
+
: '\x1b[31m✗\x1b[0m';
|
|
122
|
+
let outputInfo = '';
|
|
123
|
+
if (result.outputMatch !== undefined) {
|
|
124
|
+
outputInfo = result.outputMatch ? ' output: \x1b[32m✓ matches\x1b[0m' : ' output: \x1b[31m✗ mismatch\x1b[0m';
|
|
125
|
+
}
|
|
126
|
+
console.log(` ${result.environment.padEnd(20)} ${statusIcon} (${formatDuration(result.duration)})${outputInfo}`);
|
|
127
|
+
if (result.status === 'fail' && result.stderr) {
|
|
128
|
+
const firstLine = result.stderr.trim().split('\n')[0] || '';
|
|
129
|
+
console.log(` \x1b[31m${firstLine.slice(0, 80)}\x1b[0m`);
|
|
210
130
|
}
|
|
211
131
|
}
|
|
212
132
|
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Print summary
|
|
216
|
-
*/
|
|
217
|
-
function printSummary(summary) {
|
|
218
|
-
console.log('');
|
|
219
|
-
console.log('=== Test Summary ===');
|
|
220
|
-
console.log(` Total: ${summary.total}`);
|
|
221
|
-
console.log(` \x1b[32mPassed:\x1b[0m ${summary.passed}`);
|
|
222
|
-
console.log(` \x1b[31mFailed:\x1b[0m ${summary.failed}`);
|
|
223
|
-
if (summary.skipped > 0) {
|
|
224
|
-
console.log(` \x1b[33mSkipped:\x1b[0m ${summary.skipped}`);
|
|
225
|
-
}
|
|
226
|
-
console.log(` Duration: ${formatDuration(summary.duration)}`);
|
|
133
|
+
return results;
|
|
227
134
|
}
|
|
228
135
|
export const testCommand = new Command('test')
|
|
229
136
|
.description('Test code examples in documentation files')
|
|
@@ -233,28 +140,52 @@ export const testCommand = new Command('test')
|
|
|
233
140
|
.option('--fix', 'Auto-fix failing examples using autofix command')
|
|
234
141
|
.option('-t, --timeout <ms>', 'Timeout per code block in milliseconds', '10000')
|
|
235
142
|
.option('-v, --verbose', 'Show detailed output')
|
|
143
|
+
.option('--verify-output', 'Compare stdout against // Output: comments')
|
|
144
|
+
.option('--env-file <file>', 'Load environment variables from file')
|
|
145
|
+
.option('--environments <list>', 'Comma-separated Docker environments (e.g. node-20,python-3.12)')
|
|
236
146
|
.action(async (path, options) => {
|
|
237
147
|
try {
|
|
238
|
-
//
|
|
239
|
-
if (
|
|
240
|
-
|
|
148
|
+
// Docker multi-env testing is Pro-only
|
|
149
|
+
if (options.environments) {
|
|
150
|
+
if (!await requirePro('test --environments')) {
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
241
153
|
}
|
|
242
154
|
const targetPath = resolve(options.file || path);
|
|
243
155
|
if (!existsSync(targetPath)) {
|
|
244
156
|
console.error(`Error: Path not found: ${targetPath}`);
|
|
245
157
|
process.exit(1);
|
|
246
158
|
}
|
|
247
|
-
const timeoutMs = parseInt(options.timeout);
|
|
159
|
+
const timeoutMs = parseInt(options.timeout, 10);
|
|
248
160
|
if (isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
249
161
|
console.error(`Error: Invalid timeout value: ${options.timeout}`);
|
|
250
162
|
process.exit(1);
|
|
251
163
|
}
|
|
164
|
+
// Load env vars
|
|
165
|
+
let envVars = {};
|
|
166
|
+
if (options.envFile) {
|
|
167
|
+
const envPath = resolve(options.envFile);
|
|
168
|
+
try {
|
|
169
|
+
envVars = loadEnvFile(envPath);
|
|
170
|
+
console.log(` Loaded ${Object.keys(envVars).length} env var(s) from ${options.envFile}`);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.error(`Error loading env file: ${err instanceof Error ? err.message : err}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
252
177
|
console.log('skrypt test');
|
|
253
178
|
console.log(` path: ${targetPath}`);
|
|
254
179
|
if (options.language) {
|
|
255
180
|
console.log(` language: ${options.language}`);
|
|
256
181
|
}
|
|
257
182
|
console.log(` timeout: ${timeoutMs}ms`);
|
|
183
|
+
if (options.verifyOutput) {
|
|
184
|
+
console.log(' verify-output: enabled');
|
|
185
|
+
}
|
|
186
|
+
if (options.environments) {
|
|
187
|
+
console.log(` environments: ${options.environments}`);
|
|
188
|
+
}
|
|
258
189
|
console.log('');
|
|
259
190
|
// Find all doc files
|
|
260
191
|
const files = statSync(targetPath).isDirectory()
|
|
@@ -264,54 +195,64 @@ export const testCommand = new Command('test')
|
|
|
264
195
|
console.log('No .md or .mdx files found.');
|
|
265
196
|
process.exit(0);
|
|
266
197
|
}
|
|
267
|
-
// Extract all
|
|
268
|
-
const
|
|
198
|
+
// Extract all snippets
|
|
199
|
+
const allSnippets = [];
|
|
269
200
|
for (const file of files) {
|
|
270
|
-
const
|
|
271
|
-
|
|
201
|
+
const snippets = extractSnippets(file, options.language);
|
|
202
|
+
allSnippets.push(...snippets);
|
|
272
203
|
}
|
|
273
|
-
if (
|
|
204
|
+
if (allSnippets.length === 0) {
|
|
274
205
|
console.log('No testable code blocks found.');
|
|
275
206
|
if (options.language) {
|
|
276
207
|
console.log(` (filtered by language: ${options.language})`);
|
|
277
208
|
}
|
|
278
|
-
console.log(
|
|
209
|
+
console.log(' Supported languages: typescript, ts, javascript, js, python, py');
|
|
279
210
|
process.exit(0);
|
|
280
211
|
}
|
|
281
|
-
console.log(`Found ${
|
|
212
|
+
console.log(`Found ${allSnippets.length} code block(s) in ${files.length} file(s)`);
|
|
282
213
|
console.log('');
|
|
283
214
|
console.log('Running tests...\n');
|
|
284
|
-
const
|
|
285
|
-
|
|
215
|
+
const runnerConfig = {
|
|
216
|
+
timeout: timeoutMs,
|
|
217
|
+
envVars,
|
|
218
|
+
installDeps: true,
|
|
219
|
+
};
|
|
220
|
+
let results;
|
|
286
221
|
const startTime = Date.now();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
222
|
+
if (options.environments) {
|
|
223
|
+
// Docker multi-environment matrix
|
|
224
|
+
results = await runMatrix(allSnippets, options.environments, runnerConfig, targetPath, options.verbose || false);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Local execution (default)
|
|
228
|
+
results = [];
|
|
229
|
+
for (const snippet of allSnippets) {
|
|
230
|
+
// If --verify-output is not set, clear expectedOutput so we only check exit code
|
|
231
|
+
const snippetToRun = options.verifyOutput ? snippet : { ...snippet, expectedOutput: undefined };
|
|
232
|
+
const result = await runLocally(snippetToRun, runnerConfig);
|
|
233
|
+
results.push(result);
|
|
234
|
+
printResult(result, targetPath, options.verbose || false);
|
|
296
235
|
}
|
|
297
236
|
}
|
|
298
237
|
const totalDuration = Date.now() - startTime;
|
|
299
|
-
const
|
|
300
|
-
total:
|
|
301
|
-
passed: results.filter(r => r.
|
|
302
|
-
failed: results.filter(r =>
|
|
303
|
-
skipped:
|
|
238
|
+
const report = {
|
|
239
|
+
total: results.length,
|
|
240
|
+
passed: results.filter(r => r.status === 'pass').length,
|
|
241
|
+
failed: results.filter(r => r.status === 'fail' || r.status === 'timeout').length,
|
|
242
|
+
skipped: results.filter(r => r.status === 'skip').length,
|
|
243
|
+
outputMismatches: results.filter(r => r.status === 'output_mismatch').length,
|
|
304
244
|
duration: totalDuration,
|
|
245
|
+
results,
|
|
305
246
|
};
|
|
306
|
-
printSummary(
|
|
247
|
+
printSummary(report);
|
|
307
248
|
// Handle --fix flag
|
|
308
|
-
|
|
249
|
+
const failedSnippets = results.filter(r => r.status === 'fail' || r.status === 'output_mismatch');
|
|
250
|
+
if (options.fix && failedSnippets.length > 0) {
|
|
309
251
|
console.log('');
|
|
310
252
|
console.log('Attempting to auto-fix failing examples...');
|
|
311
253
|
console.log(' Run: skrypt autofix <file> for each failing file');
|
|
312
254
|
console.log('');
|
|
313
|
-
|
|
314
|
-
const failedFiles = [...new Set(failedBlocks.map(b => b.file))];
|
|
255
|
+
const failedFiles = [...new Set(failedSnippets.map(r => r.snippet.filePath))];
|
|
315
256
|
for (const file of failedFiles) {
|
|
316
257
|
console.log(` → ${relative(targetPath, file)}`);
|
|
317
258
|
}
|
|
@@ -322,7 +263,7 @@ export const testCommand = new Command('test')
|
|
|
322
263
|
}
|
|
323
264
|
}
|
|
324
265
|
// Exit with error code if tests failed
|
|
325
|
-
if (
|
|
266
|
+
if (report.failed > 0 || report.outputMismatches > 0) {
|
|
326
267
|
process.exit(1);
|
|
327
268
|
}
|
|
328
269
|
console.log('\nAll tests passed!');
|
package/dist/commands/watch.js
CHANGED
|
@@ -10,17 +10,22 @@ import { generateForElements, groupDocsByFile, writeDocsToDirectory } from '../g
|
|
|
10
10
|
import { runQA, printQAReport, fixQAIssues, printFixReport } from '../qa/index.js';
|
|
11
11
|
export const watchCommand = new Command('watch')
|
|
12
12
|
.description('Watch source files and regenerate docs on changes')
|
|
13
|
-
.argument('
|
|
13
|
+
.argument('[sources...]', 'Source directories to watch')
|
|
14
14
|
.option('-o, --output <dir>', 'Output directory')
|
|
15
15
|
.option('-c, --config <file>', 'Config file path')
|
|
16
16
|
.option('--provider <name>', 'LLM provider')
|
|
17
17
|
.option('--model <name>', 'LLM model name')
|
|
18
18
|
.option('--debounce <ms>', 'Debounce time in milliseconds', '1000')
|
|
19
|
-
.action(async (
|
|
19
|
+
.action(async (sources = [], options) => {
|
|
20
20
|
try {
|
|
21
21
|
const config = loadConfig(options.config);
|
|
22
|
-
if (
|
|
23
|
-
config
|
|
22
|
+
if (sources.length === 0) {
|
|
23
|
+
// Fall back to config source path
|
|
24
|
+
sources = [config.source.path];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
config.source.path = sources[0];
|
|
28
|
+
}
|
|
24
29
|
if (options.output)
|
|
25
30
|
config.output.path = options.output;
|
|
26
31
|
if (options.provider) {
|
|
@@ -41,14 +46,16 @@ export const watchCommand = new Command('watch')
|
|
|
41
46
|
console.error(`Error: ${envKey} required`);
|
|
42
47
|
process.exit(1);
|
|
43
48
|
}
|
|
44
|
-
const
|
|
49
|
+
const sourcePaths = sources.map(s => resolve(s));
|
|
45
50
|
const outputPath = resolve(config.output.path);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
for (const sp of sourcePaths) {
|
|
52
|
+
if (!existsSync(sp)) {
|
|
53
|
+
console.error(`Source not found: ${sp}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
49
56
|
}
|
|
50
57
|
console.log('skrypt watch');
|
|
51
|
-
console.log(` source: ${
|
|
58
|
+
console.log(` source: ${sourcePaths.join(', ')}`);
|
|
52
59
|
console.log(` output: ${outputPath}`);
|
|
53
60
|
console.log(` provider: ${config.llm.provider}`);
|
|
54
61
|
console.log('');
|
|
@@ -73,25 +80,27 @@ export const watchCommand = new Command('watch')
|
|
|
73
80
|
const startTime = Date.now();
|
|
74
81
|
try {
|
|
75
82
|
console.log('\n[' + new Date().toLocaleTimeString() + '] Regenerating docs...');
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
const allElements = [];
|
|
84
|
+
for (const sp of sourcePaths) {
|
|
85
|
+
const scanResult = await scanDirectory(sp, {
|
|
86
|
+
include: config.source.include,
|
|
87
|
+
exclude: config.source.exclude,
|
|
88
|
+
});
|
|
89
|
+
for (const file of scanResult.files) {
|
|
90
|
+
allElements.push(...file.elements);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (allElements.length === 0) {
|
|
81
94
|
console.log(' No elements found');
|
|
82
95
|
return;
|
|
83
96
|
}
|
|
84
|
-
const allElements = [];
|
|
85
|
-
for (const file of scanResult.files) {
|
|
86
|
-
allElements.push(...file.elements);
|
|
87
|
-
}
|
|
88
97
|
const docs = await generateForElements(allElements, client, {
|
|
89
98
|
onProgress: (p) => {
|
|
90
99
|
process.stdout.write(`\r [${p.current}/${p.total}] ${p.element}`.padEnd(60));
|
|
91
100
|
}
|
|
92
101
|
});
|
|
93
102
|
const fileResults = groupDocsByFile(docs);
|
|
94
|
-
await writeDocsToDirectory(fileResults, outputPath,
|
|
103
|
+
await writeDocsToDirectory(fileResults, outputPath, sourcePaths[0]);
|
|
95
104
|
// Auto-fix then run QA on generated output
|
|
96
105
|
const fixReport = fixQAIssues(outputPath);
|
|
97
106
|
printFixReport(fixReport);
|
|
@@ -116,7 +125,7 @@ export const watchCommand = new Command('watch')
|
|
|
116
125
|
// Watch for changes
|
|
117
126
|
console.log('\nWatching for changes... (Ctrl+C to stop)');
|
|
118
127
|
let debounceTimer = null;
|
|
119
|
-
const watcher = watch(
|
|
128
|
+
const watcher = watch(sourcePaths, {
|
|
120
129
|
ignored: [
|
|
121
130
|
'**/node_modules/**',
|
|
122
131
|
'**/.git/**',
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { Config, LLMProvider } from './types.js';
|
|
1
|
+
import { Config, LLMProvider, SourceEntry } from './types.js';
|
|
2
2
|
export declare function findConfigFile(dir: string): string | null;
|
|
3
3
|
export declare function loadConfig(configPath?: string): Config;
|
|
4
4
|
export declare function validateConfig(config: Config): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Resolve v1 `source` or v2 `sources` into a normalized array of SourceEntry.
|
|
7
|
+
* Falls back to the single source config if no sources array is defined.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveSourceEntries(config: Config): SourceEntry[];
|
|
5
10
|
export declare function checkApiKey(provider: LLMProvider): {
|
|
6
11
|
ok: boolean;
|
|
7
12
|
envKey: string | null;
|