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.
Files changed (88) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/auth/index.js +3 -1
  12. package/dist/cli.js +11 -1
  13. package/dist/commands/audit.d.ts +2 -0
  14. package/dist/commands/audit.js +59 -0
  15. package/dist/commands/config.d.ts +2 -0
  16. package/dist/commands/config.js +73 -0
  17. package/dist/commands/cron.js +4 -0
  18. package/dist/commands/generate.d.ts +7 -0
  19. package/dist/commands/generate.js +528 -234
  20. package/dist/commands/refresh.d.ts +2 -0
  21. package/dist/commands/refresh.js +158 -0
  22. package/dist/commands/review-pr.js +5 -0
  23. package/dist/commands/review.d.ts +2 -0
  24. package/dist/commands/review.js +110 -0
  25. package/dist/commands/test.js +177 -236
  26. package/dist/commands/watch.js +29 -20
  27. package/dist/config/loader.d.ts +6 -1
  28. package/dist/config/loader.js +38 -2
  29. package/dist/config/types.d.ts +7 -0
  30. package/dist/generator/generator.js +2 -1
  31. package/dist/generator/types.d.ts +3 -0
  32. package/dist/generator/writer.js +60 -28
  33. package/dist/github/org-discovery.d.ts +17 -0
  34. package/dist/github/org-discovery.js +93 -0
  35. package/dist/llm/index.d.ts +2 -0
  36. package/dist/llm/index.js +8 -2
  37. package/dist/next-actions/actions.d.ts +2 -0
  38. package/dist/next-actions/actions.js +190 -0
  39. package/dist/next-actions/index.d.ts +6 -0
  40. package/dist/next-actions/index.js +39 -0
  41. package/dist/next-actions/setup.d.ts +2 -0
  42. package/dist/next-actions/setup.js +72 -0
  43. package/dist/next-actions/state.d.ts +7 -0
  44. package/dist/next-actions/state.js +68 -0
  45. package/dist/next-actions/suggest.d.ts +3 -0
  46. package/dist/next-actions/suggest.js +47 -0
  47. package/dist/next-actions/types.d.ts +26 -0
  48. package/dist/next-actions/types.js +1 -0
  49. package/dist/refresh/differ.d.ts +9 -0
  50. package/dist/refresh/differ.js +67 -0
  51. package/dist/refresh/index.d.ts +4 -0
  52. package/dist/refresh/index.js +4 -0
  53. package/dist/refresh/manifest.d.ts +18 -0
  54. package/dist/refresh/manifest.js +71 -0
  55. package/dist/refresh/splicer.d.ts +9 -0
  56. package/dist/refresh/splicer.js +50 -0
  57. package/dist/refresh/types.d.ts +37 -0
  58. package/dist/refresh/types.js +1 -0
  59. package/dist/review/index.d.ts +8 -0
  60. package/dist/review/index.js +94 -0
  61. package/dist/review/parser.d.ts +16 -0
  62. package/dist/review/parser.js +95 -0
  63. package/dist/review/types.d.ts +18 -0
  64. package/dist/review/types.js +1 -0
  65. package/dist/scanner/types.d.ts +2 -0
  66. package/dist/structure/index.d.ts +19 -0
  67. package/dist/structure/index.js +92 -0
  68. package/dist/structure/planner.d.ts +8 -0
  69. package/dist/structure/planner.js +180 -0
  70. package/dist/structure/topology.d.ts +16 -0
  71. package/dist/structure/topology.js +49 -0
  72. package/dist/structure/types.d.ts +26 -0
  73. package/dist/structure/types.js +1 -0
  74. package/dist/testing/comparator.d.ts +7 -0
  75. package/dist/testing/comparator.js +77 -0
  76. package/dist/testing/docker.d.ts +21 -0
  77. package/dist/testing/docker.js +234 -0
  78. package/dist/testing/env.d.ts +16 -0
  79. package/dist/testing/env.js +58 -0
  80. package/dist/testing/extractor.d.ts +9 -0
  81. package/dist/testing/extractor.js +195 -0
  82. package/dist/testing/index.d.ts +6 -0
  83. package/dist/testing/index.js +6 -0
  84. package/dist/testing/runner.d.ts +5 -0
  85. package/dist/testing/runner.js +225 -0
  86. package/dist/testing/types.d.ts +58 -0
  87. package/dist/testing/types.js +1 -0
  88. package/package.json +1 -1
@@ -1,229 +1,136 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
- import { resolve, join, extname, relative } from 'path';
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
- const SUPPORTED_LANGUAGES = ['typescript', 'ts', 'javascript', 'js', 'python', 'py'];
5
+ import { findDocFiles, extractSnippets, runLocally, isDockerAvailable, parseEnvironments, getCompatibleEnvironments, runInDocker, loadEnvFile, } from '../testing/index.js';
10
6
  /**
11
- * Find all MDX/MD files in a directory recursively
7
+ * Format duration in human-readable format
12
8
  */
13
- function findDocFiles(dir) {
14
- const files = [];
15
- function walk(currentDir) {
16
- const entries = readdirSync(currentDir);
17
- for (const entry of entries) {
18
- const fullPath = join(currentDir, entry);
19
- const stat = statSync(fullPath);
20
- if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
21
- walk(fullPath);
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
- else if (stat.isFile() && (extname(entry) === '.mdx' || extname(entry) === '.md')) {
24
- files.push(fullPath);
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
- * Extract code blocks from a markdown/MDX file
59
+ * Print test summary
33
60
  */
34
- function extractCodeBlocks(filePath, languageFilter) {
35
- const content = readFileSync(filePath, 'utf-8');
36
- const blocks = [];
37
- const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
38
- let match;
39
- while ((match = codeBlockRegex.exec(content)) !== null) {
40
- const language = match[1] || '';
41
- const code = match[2] || '';
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
- return blocks;
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 a code block in an isolated environment with timeout
76
+ * Run snippets through Docker multi-environment matrix
65
77
  */
66
- async function runCodeBlock(block, timeoutMs) {
67
- const startTime = Date.now();
68
- const tempDir = join(tmpdir(), `skrypt-test-${randomUUID()}`);
69
- mkdirSync(tempDir, { recursive: true });
70
- try {
71
- const isTypeScript = ['typescript', 'ts'].includes(block.language);
72
- const isPython = ['python', 'py'].includes(block.language);
73
- const isJavaScript = ['javascript', 'js'].includes(block.language);
74
- let tempFile;
75
- let command;
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
- const result = await executeWithTimeout(command, args, timeoutMs, tempDir);
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
- catch (err) {
115
- const message = err instanceof Error ? err.message : String(err);
116
- return {
117
- block,
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
- * Execute a command with timeout
135
- */
136
- function executeWithTimeout(command, args, timeoutMs, cwd) {
137
- return new Promise((resolve) => {
138
- const proc = spawn(command, args, {
139
- cwd,
140
- stdio: ['pipe', 'pipe', 'pipe'],
141
- env: { ...process.env, NODE_NO_WARNINGS: '1' },
142
- });
143
- let stdout = '';
144
- let stderr = '';
145
- let killed = false;
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
- else {
205
- console.log(` \x1b[31m✗\x1b[0m ${location} [${lang}] (${formatDuration(result.duration)})`);
206
- if (result.error) {
207
- const errorLines = result.error.trim().split('\n').slice(0, 3);
208
- for (const line of errorLines) {
209
- console.log(` \x1b[31m${line}\x1b[0m`);
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
- // Pro feature - requires subscription
239
- if (!await requirePro('test')) {
240
- process.exit(1);
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 code blocks
268
- const allBlocks = [];
198
+ // Extract all snippets
199
+ const allSnippets = [];
269
200
  for (const file of files) {
270
- const blocks = extractCodeBlocks(file, options.language);
271
- allBlocks.push(...blocks);
201
+ const snippets = extractSnippets(file, options.language);
202
+ allSnippets.push(...snippets);
272
203
  }
273
- if (allBlocks.length === 0) {
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(` Supported languages: ${SUPPORTED_LANGUAGES.join(', ')}`);
209
+ console.log(' Supported languages: typescript, ts, javascript, js, python, py');
279
210
  process.exit(0);
280
211
  }
281
- console.log(`Found ${allBlocks.length} code block(s) in ${files.length} file(s)`);
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 results = [];
285
- const failedBlocks = [];
215
+ const runnerConfig = {
216
+ timeout: timeoutMs,
217
+ envVars,
218
+ installDeps: true,
219
+ };
220
+ let results;
286
221
  const startTime = Date.now();
287
- for (let i = 0; i < allBlocks.length; i++) {
288
- const block = allBlocks[i];
289
- if (!block)
290
- continue;
291
- const result = await runCodeBlock(block, timeoutMs);
292
- results.push(result);
293
- printResult(result, targetPath, options.verbose || false);
294
- if (!result.passed) {
295
- failedBlocks.push(block);
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 summary = {
300
- total: allBlocks.length,
301
- passed: results.filter(r => r.passed).length,
302
- failed: results.filter(r => !r.passed).length,
303
- skipped: 0,
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(summary);
247
+ printSummary(report);
307
248
  // Handle --fix flag
308
- if (options.fix && failedBlocks.length > 0) {
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
- // Get unique files with failures
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 (summary.failed > 0) {
266
+ if (report.failed > 0 || report.outputMismatches > 0) {
326
267
  process.exit(1);
327
268
  }
328
269
  console.log('\nAll tests passed!');
@@ -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('<source>', 'Source directory to watch')
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 (source, options) => {
19
+ .action(async (sources = [], options) => {
20
20
  try {
21
21
  const config = loadConfig(options.config);
22
- if (source)
23
- config.source.path = source;
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 sourcePath = resolve(config.source.path);
49
+ const sourcePaths = sources.map(s => resolve(s));
45
50
  const outputPath = resolve(config.output.path);
46
- if (!existsSync(sourcePath)) {
47
- console.error(`Source not found: ${sourcePath}`);
48
- process.exit(1);
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: ${sourcePath}`);
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 scanResult = await scanDirectory(sourcePath, {
77
- include: config.source.include,
78
- exclude: config.source.exclude,
79
- });
80
- if (scanResult.totalElements === 0) {
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, sourcePath);
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(sourcePath, {
128
+ const watcher = watch(sourcePaths, {
120
129
  ignored: [
121
130
  '**/node_modules/**',
122
131
  '**/.git/**',
@@ -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;