skrypt-ai 0.5.0 → 0.6.1

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 (120) hide show
  1. package/dist/auth/index.js +8 -1
  2. package/dist/autofix/index.d.ts +0 -4
  3. package/dist/autofix/index.js +0 -21
  4. package/dist/capture/browser.d.ts +11 -0
  5. package/dist/capture/browser.js +173 -0
  6. package/dist/capture/diff.d.ts +23 -0
  7. package/dist/capture/diff.js +52 -0
  8. package/dist/capture/index.d.ts +23 -0
  9. package/dist/capture/index.js +210 -0
  10. package/dist/capture/naming.d.ts +17 -0
  11. package/dist/capture/naming.js +45 -0
  12. package/dist/capture/parser.d.ts +15 -0
  13. package/dist/capture/parser.js +80 -0
  14. package/dist/capture/types.d.ts +57 -0
  15. package/dist/capture/types.js +1 -0
  16. package/dist/cli.js +4 -0
  17. package/dist/commands/autofix.js +136 -120
  18. package/dist/commands/cron.js +58 -47
  19. package/dist/commands/deploy.js +123 -102
  20. package/dist/commands/generate.js +88 -6
  21. package/dist/commands/heal.d.ts +10 -0
  22. package/dist/commands/heal.js +201 -0
  23. package/dist/commands/i18n.js +146 -111
  24. package/dist/commands/lint.js +50 -44
  25. package/dist/commands/llms-txt.js +59 -49
  26. package/dist/commands/login.js +61 -43
  27. package/dist/commands/mcp.js +6 -0
  28. package/dist/commands/monitor.js +13 -8
  29. package/dist/commands/qa.d.ts +2 -0
  30. package/dist/commands/qa.js +43 -0
  31. package/dist/commands/review-pr.js +114 -103
  32. package/dist/commands/sdk.js +128 -122
  33. package/dist/commands/security.js +86 -80
  34. package/dist/commands/test.js +91 -92
  35. package/dist/commands/version.js +104 -75
  36. package/dist/commands/watch.js +130 -114
  37. package/dist/config/types.js +2 -2
  38. package/dist/context-hub/index.d.ts +23 -0
  39. package/dist/context-hub/index.js +179 -0
  40. package/dist/context-hub/mappings.d.ts +8 -0
  41. package/dist/context-hub/mappings.js +55 -0
  42. package/dist/context-hub/types.d.ts +33 -0
  43. package/dist/context-hub/types.js +1 -0
  44. package/dist/generator/generator.js +39 -6
  45. package/dist/generator/types.d.ts +7 -0
  46. package/dist/generator/writer.d.ts +3 -1
  47. package/dist/generator/writer.js +24 -4
  48. package/dist/llm/anthropic-client.d.ts +1 -0
  49. package/dist/llm/anthropic-client.js +3 -1
  50. package/dist/llm/index.d.ts +6 -4
  51. package/dist/llm/index.js +76 -261
  52. package/dist/llm/openai-client.d.ts +1 -0
  53. package/dist/llm/openai-client.js +7 -2
  54. package/dist/qa/checks.d.ts +10 -0
  55. package/dist/qa/checks.js +492 -0
  56. package/dist/qa/fixes.d.ts +30 -0
  57. package/dist/qa/fixes.js +277 -0
  58. package/dist/qa/index.d.ts +29 -0
  59. package/dist/qa/index.js +187 -0
  60. package/dist/qa/types.d.ts +24 -0
  61. package/dist/qa/types.js +1 -0
  62. package/dist/scanner/csharp.d.ts +23 -0
  63. package/dist/scanner/csharp.js +421 -0
  64. package/dist/scanner/index.js +16 -2
  65. package/dist/scanner/java.d.ts +39 -0
  66. package/dist/scanner/java.js +318 -0
  67. package/dist/scanner/kotlin.d.ts +23 -0
  68. package/dist/scanner/kotlin.js +389 -0
  69. package/dist/scanner/php.d.ts +57 -0
  70. package/dist/scanner/php.js +351 -0
  71. package/dist/scanner/ruby.d.ts +36 -0
  72. package/dist/scanner/ruby.js +431 -0
  73. package/dist/scanner/swift.d.ts +25 -0
  74. package/dist/scanner/swift.js +392 -0
  75. package/dist/scanner/types.d.ts +1 -1
  76. package/dist/template/content/docs/_navigation.json +46 -0
  77. package/dist/template/content/docs/_sidebars.json +684 -0
  78. package/dist/template/content/docs/core.md +4544 -0
  79. package/dist/template/content/docs/index.mdx +89 -0
  80. package/dist/template/content/docs/integrations.md +1158 -0
  81. package/dist/template/content/docs/llms-full.md +403 -0
  82. package/dist/template/content/docs/llms.txt +4588 -0
  83. package/dist/template/content/docs/other.md +10379 -0
  84. package/dist/template/content/docs/tools.md +746 -0
  85. package/dist/template/content/docs/types.md +531 -0
  86. package/dist/template/docs.json +13 -11
  87. package/dist/template/mdx-components.tsx +27 -2
  88. package/dist/template/package.json +6 -0
  89. package/dist/template/public/search-index.json +1 -1
  90. package/dist/template/scripts/build-search-index.mjs +84 -6
  91. package/dist/template/src/app/api/chat/route.ts +83 -128
  92. package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
  93. package/dist/template/src/app/docs/llms-full.md +151 -4
  94. package/dist/template/src/app/docs/llms.txt +2464 -847
  95. package/dist/template/src/app/docs/page.mdx +48 -38
  96. package/dist/template/src/app/layout.tsx +3 -1
  97. package/dist/template/src/app/page.tsx +22 -8
  98. package/dist/template/src/components/ai-chat.tsx +73 -64
  99. package/dist/template/src/components/breadcrumbs.tsx +21 -23
  100. package/dist/template/src/components/copy-button.tsx +13 -9
  101. package/dist/template/src/components/copy-page-button.tsx +54 -0
  102. package/dist/template/src/components/docs-layout.tsx +37 -25
  103. package/dist/template/src/components/header.tsx +51 -10
  104. package/dist/template/src/components/mdx/card.tsx +17 -3
  105. package/dist/template/src/components/mdx/code-block.tsx +13 -9
  106. package/dist/template/src/components/mdx/code-group.tsx +13 -8
  107. package/dist/template/src/components/mdx/heading.tsx +15 -2
  108. package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
  109. package/dist/template/src/components/mdx/index.tsx +2 -0
  110. package/dist/template/src/components/mdx/mermaid.tsx +110 -0
  111. package/dist/template/src/components/mdx/screenshot.tsx +150 -0
  112. package/dist/template/src/components/scroll-to-hash.tsx +48 -0
  113. package/dist/template/src/components/sidebar.tsx +12 -18
  114. package/dist/template/src/components/table-of-contents.tsx +9 -0
  115. package/dist/template/src/lib/highlight.ts +3 -88
  116. package/dist/template/src/lib/navigation.ts +159 -0
  117. package/dist/template/src/styles/globals.css +17 -6
  118. package/dist/utils/validation.d.ts +0 -3
  119. package/dist/utils/validation.js +0 -26
  120. package/package.json +3 -2
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { spawnSync, spawn } from 'child_process';
3
- import { existsSync, readFileSync, statSync } from 'fs';
3
+ import { existsSync, readFileSync, statSync, unlinkSync } from 'fs';
4
4
  import { resolve, join } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { createHash } from 'crypto';
@@ -81,8 +81,7 @@ async function buildSite(docsPath) {
81
81
  return new Promise((resolve, reject) => {
82
82
  const child = spawn('npm', ['run', 'build'], {
83
83
  cwd: docsPath,
84
- stdio: ['inherit', 'pipe', 'pipe'],
85
- shell: true
84
+ stdio: ['inherit', 'pipe', 'pipe']
86
85
  });
87
86
  child.stdout?.on('data', (data) => {
88
87
  process.stdout.write(` ${data.toString().trim()}\n`);
@@ -208,113 +207,135 @@ export const deployCommand = new Command('deploy')
208
207
  .option('--project <slug>', 'Project slug (e.g., my-docs)')
209
208
  .option('--token <key>', 'API token (or set SKRYPT_API_KEY env var)')
210
209
  .action(async (directory, options) => {
211
- const startTime = Date.now();
212
- const docsPath = resolve(directory);
213
- console.log('skrypt deploy');
214
- console.log(` directory: ${docsPath}`);
215
- console.log('');
216
- // Validate directory
217
- if (!existsSync(docsPath)) {
218
- console.error(`Error: Directory not found: ${docsPath}`);
219
- process.exit(1);
220
- }
221
- // Check for package.json (Next.js project)
222
- const packageJsonPath = join(docsPath, 'package.json');
223
- if (!existsSync(packageJsonPath)) {
224
- console.error('Error: No package.json found. Is this a Skrypt docs site?');
225
- console.error(' Run: skrypt init <directory>');
226
- process.exit(1);
227
- }
228
- // Get API token
229
- const apiToken = getApiToken(options);
230
- if (!apiToken) {
231
- console.error('Error: No API token found.');
232
- console.error('');
233
- console.error(' Provide a token using one of:');
234
- console.error(' --token <key>');
235
- console.error(' SKRYPT_API_KEY environment variable');
236
- console.error(' skrypt login');
237
- console.error('');
238
- process.exit(1);
239
- }
240
- // Get project slug
241
- const projectSlug = getProjectSlug(options, docsPath);
242
- if (!projectSlug) {
243
- console.error('Error: No project slug found.');
244
- console.error('');
245
- console.error(' Provide a project slug using one of:');
246
- console.error(' --project <slug>');
247
- console.error(' "slug" field in docs.json');
248
- console.error('');
249
- process.exit(1);
250
- }
251
- console.log(` project: ${projectSlug}`);
252
- console.log('');
253
- // Step 1: Build the site
254
- console.log('Step 1: Building documentation site...');
255
- let outDir;
210
+ let bundlePath;
256
211
  try {
257
- outDir = await buildSite(docsPath);
258
- }
259
- catch (err) {
260
- console.error('');
261
- console.error(`Error: Build failed`);
262
- if (err instanceof Error) {
263
- console.error(` ${err.message}`);
212
+ const startTime = Date.now();
213
+ const docsPath = resolve(directory);
214
+ console.log('skrypt deploy');
215
+ console.log(` directory: ${docsPath}`);
216
+ console.log('');
217
+ // Validate directory
218
+ if (!existsSync(docsPath)) {
219
+ console.error(`Error: Directory not found: ${docsPath}`);
220
+ process.exit(1);
264
221
  }
265
- process.exit(1);
266
- }
267
- // Check build output exists
268
- if (!existsSync(outDir)) {
269
- // Try .next/static for non-static export
270
- const nextDir = join(docsPath, '.next');
271
- if (existsSync(nextDir)) {
222
+ // Check for package.json (Next.js project)
223
+ const packageJsonPath = join(docsPath, 'package.json');
224
+ if (!existsSync(packageJsonPath)) {
225
+ console.error('Error: No package.json found. Is this a Skrypt docs site?');
226
+ console.error(' Run: skrypt init <directory>');
227
+ process.exit(1);
228
+ }
229
+ // Get API token
230
+ const apiToken = getApiToken(options);
231
+ if (!apiToken) {
232
+ console.error('Error: No API token found.');
233
+ console.error('');
234
+ console.error(' Provide a token using one of:');
235
+ console.error(' --token <key>');
236
+ console.error(' SKRYPT_API_KEY environment variable');
237
+ console.error(' skrypt login');
272
238
  console.error('');
273
- console.error('Error: Static export not found.');
274
- console.error(' Add to next.config.mjs: output: "export"');
275
239
  process.exit(1);
276
240
  }
277
- console.error('');
278
- console.error('Error: Build output not found.');
279
- process.exit(1);
280
- }
281
- console.log(' Build completed successfully');
282
- console.log('');
283
- // Step 2: Bundle the output
284
- console.log('Step 2: Creating deployment bundle...');
285
- let bundlePath;
286
- try {
287
- bundlePath = await bundleOutput(outDir);
241
+ // Get project slug
242
+ const projectSlug = getProjectSlug(options, docsPath);
243
+ if (!projectSlug) {
244
+ console.error('Error: No project slug found.');
245
+ console.error('');
246
+ console.error(' Provide a project slug using one of:');
247
+ console.error(' --project <slug>');
248
+ console.error(' "slug" field in docs.json');
249
+ console.error('');
250
+ process.exit(1);
251
+ }
252
+ console.log(` project: ${projectSlug}`);
253
+ console.log('');
254
+ // Step 1: Build the site
255
+ console.log('Step 1: Building documentation site...');
256
+ let outDir;
257
+ try {
258
+ outDir = await buildSite(docsPath);
259
+ }
260
+ catch (err) {
261
+ console.error('');
262
+ console.error(`Error: Build failed`);
263
+ if (err instanceof Error) {
264
+ console.error(` ${err.message}`);
265
+ }
266
+ process.exit(1);
267
+ }
268
+ // Check build output exists
269
+ if (!existsSync(outDir)) {
270
+ // Try .next/static for non-static export
271
+ const nextDir = join(docsPath, '.next');
272
+ if (existsSync(nextDir)) {
273
+ console.error('');
274
+ console.error('Error: Static export not found.');
275
+ console.error(' Add to next.config.mjs: output: "export"');
276
+ process.exit(1);
277
+ }
278
+ console.error('');
279
+ console.error('Error: Build output not found.');
280
+ process.exit(1);
281
+ }
282
+ console.log(' Build completed successfully');
283
+ console.log('');
284
+ // Step 2: Bundle the output
285
+ console.log('Step 2: Creating deployment bundle...');
286
+ try {
287
+ bundlePath = await bundleOutput(outDir);
288
+ }
289
+ catch (err) {
290
+ console.error('');
291
+ console.error('Error: Failed to create bundle');
292
+ if (err instanceof Error) {
293
+ console.error(` ${err.message}`);
294
+ }
295
+ process.exit(1);
296
+ }
297
+ console.log('');
298
+ // Step 3: Upload
299
+ console.log('Step 3: Deploying to Skrypt...');
300
+ const result = await uploadBundle(bundlePath, projectSlug, apiToken);
301
+ if (!result.success) {
302
+ console.error('');
303
+ console.error(`Error: Deployment failed`);
304
+ console.error(` ${result.error}`);
305
+ // Clean up temp bundle
306
+ try {
307
+ unlinkSync(bundlePath);
308
+ }
309
+ catch { /* ignore cleanup errors */ }
310
+ process.exit(1);
311
+ }
312
+ // Clean up temp bundle
313
+ try {
314
+ unlinkSync(bundlePath);
315
+ }
316
+ catch { /* ignore cleanup errors */ }
317
+ // Success
318
+ const duration = Math.round((Date.now() - startTime) / 1000);
319
+ console.log('');
320
+ console.log('=== Deployment Successful ===');
321
+ console.log('');
322
+ console.log(` URL: ${result.url}`);
323
+ if (result.customDomain) {
324
+ console.log(` Custom domain: ${result.customDomain}`);
325
+ }
326
+ console.log(` Duration: ${duration}s`);
327
+ console.log('');
328
+ console.log('Your documentation is now live!');
329
+ console.log('');
288
330
  }
289
331
  catch (err) {
290
- console.error('');
291
- console.error('Error: Failed to create bundle');
292
- if (err instanceof Error) {
293
- console.error(` ${err.message}`);
332
+ if (bundlePath) {
333
+ try {
334
+ unlinkSync(bundlePath);
335
+ }
336
+ catch { /* ignore cleanup errors */ }
294
337
  }
338
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
295
339
  process.exit(1);
296
340
  }
297
- console.log('');
298
- // Step 3: Upload
299
- console.log('Step 3: Deploying to Skrypt...');
300
- const result = await uploadBundle(bundlePath, projectSlug, apiToken);
301
- if (!result.success) {
302
- console.error('');
303
- console.error(`Error: Deployment failed`);
304
- console.error(` ${result.error}`);
305
- process.exit(1);
306
- }
307
- // Success
308
- const duration = Math.round((Date.now() - startTime) / 1000);
309
- console.log('');
310
- console.log('=== Deployment Successful ===');
311
- console.log('');
312
- console.log(` URL: ${result.url}`);
313
- if (result.customDomain) {
314
- console.log(` Custom domain: ${result.customDomain}`);
315
- }
316
- console.log(` Duration: ${duration}s`);
317
- console.log('');
318
- console.log('Your documentation is now live!');
319
- console.log('');
320
341
  });
@@ -7,6 +7,9 @@ import { scanDirectory } from '../scanner/index.js';
7
7
  import { createLLMClient } from '../llm/index.js';
8
8
  import { generateForElements, groupDocsByFile, writeDocsToDirectory, writeDocsByTopic, writeLlmsTxt } from '../generator/index.js';
9
9
  import { showSecurityNotice } from '../auth/notices.js';
10
+ import { runQA, printQAReport, fixQAIssues, printFixReport } from '../qa/index.js';
11
+ import { extractDependencyIds, isChubInstalled, fetchContextHubDocs, exportToContextHub } from '../context-hub/index.js';
12
+ import * as readline from 'readline';
10
13
  /**
11
14
  * Read .skryptignore patterns from source directory
12
15
  */
@@ -258,6 +261,53 @@ export const generateCommand = new Command('generate')
258
261
  console.log('\n[dry run - stopping before generation]');
259
262
  process.exit(0);
260
263
  }
264
+ // Auto-read project context from README for richer doc generation
265
+ let projectContext;
266
+ const readmeCandidates = ['README.md', 'README.mdx', 'readme.md', 'README.rst', 'README.txt'];
267
+ for (const candidate of readmeCandidates) {
268
+ const readmePath = join(sourcePath, candidate);
269
+ if (existsSync(readmePath)) {
270
+ try {
271
+ const raw = readFileSync(readmePath, 'utf-8');
272
+ // Take the first ~1500 chars (intro/description section, not the whole file)
273
+ projectContext = raw.slice(0, 1500);
274
+ console.log(` Project context: loaded from ${candidate}`);
275
+ }
276
+ catch {
277
+ // Skip if unreadable
278
+ }
279
+ break;
280
+ }
281
+ }
282
+ // Also check parent directory (common for monorepos where source is a subdirectory)
283
+ if (!projectContext) {
284
+ const parentReadme = join(sourcePath, '..', 'README.md');
285
+ if (existsSync(parentReadme)) {
286
+ try {
287
+ projectContext = readFileSync(parentReadme, 'utf-8').slice(0, 1500);
288
+ console.log(' Project context: loaded from parent README.md');
289
+ }
290
+ catch {
291
+ // Skip
292
+ }
293
+ }
294
+ }
295
+ // Context Hub enrichment: fetch third-party API docs if chub is installed
296
+ let externalContext;
297
+ const allImports = [];
298
+ for (const el of allElements) {
299
+ if (el.imports?.length) {
300
+ allImports.push(...el.imports);
301
+ }
302
+ }
303
+ const chubIds = extractDependencyIds(allImports);
304
+ if (chubIds.length > 0 && isChubInstalled()) {
305
+ console.log(`\n Context Hub: fetching docs for ${chubIds.length} dependencies...`);
306
+ externalContext = fetchContextHubDocs(chubIds);
307
+ if (externalContext.size > 0) {
308
+ console.log(` Context Hub: enriching with ${externalContext.size} API references`);
309
+ }
310
+ }
261
311
  // Step 2: Generate docs
262
312
  console.log('\nStep 2: Generating documentation...');
263
313
  const client = createLLMClient({
@@ -272,6 +322,8 @@ export const generateCommand = new Command('generate')
272
322
  }
273
323
  const docs = await generateForElements(allElements, client, {
274
324
  multiLanguage,
325
+ externalContext,
326
+ projectContext,
275
327
  onProgress: (progress) => {
276
328
  if (progress.element !== lastElement) {
277
329
  if (lastElement)
@@ -328,13 +380,43 @@ export const generateCommand = new Command('generate')
328
380
  console.log(`\n Warning: OpenAPI spec not found: ${specPath}`);
329
381
  }
330
382
  }
331
- // Generate llms.txt for AEO (Answer Engine Optimization)
332
- if (options.llmsTxt) {
333
- await writeLlmsTxt(docs, outputPath, {
334
- projectName: options.projectName,
335
- description: `API documentation for ${options.projectName || basename(sourcePath)}`
383
+ // Always generate llms.txt for AEO (Answer Engine Optimization)
384
+ await writeLlmsTxt(docs, outputPath, {
385
+ projectName: options.projectName,
386
+ description: `API documentation for ${options.projectName || basename(sourcePath)}`
387
+ });
388
+ console.log(`\n Generated llms.txt and llms-full.md for AEO`);
389
+ // Step 4: Embedded QA (auto-fix then check)
390
+ console.log('\nStep 4: Running QA checks...');
391
+ const fixReport = fixQAIssues(outputPath);
392
+ printFixReport(fixReport);
393
+ const qaReport = runQA(outputPath);
394
+ printQAReport(qaReport);
395
+ // Context Hub export prompt (TTY only — skip in CI/piped mode)
396
+ if (process.stdin.isTTY) {
397
+ console.log('');
398
+ console.log(' Context Hub: Make your docs discoverable by AI coding agents.');
399
+ console.log(' Context Hub is a curated registry by Andrew Ng (7K+ stars)');
400
+ console.log(' https://github.com/andrewyng/context-hub');
401
+ console.log('');
402
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
403
+ const answer = await new Promise((resolve) => {
404
+ rl.question(' Export for Context Hub? (y/N) ', (ans) => {
405
+ rl.close();
406
+ resolve(ans.trim().toLowerCase());
407
+ });
336
408
  });
337
- console.log(`\n Generated llms.txt and llms-full.md for AEO`);
409
+ if (answer === 'y' || answer === 'yes') {
410
+ const languages = multiLanguage ? ['typescript', 'python'] : ['typescript'];
411
+ const projName = options.projectName || basename(sourcePath);
412
+ const result = exportToContextHub(docs, outputPath, {
413
+ projectName: projName,
414
+ languages,
415
+ description: `API documentation for ${projName}`,
416
+ });
417
+ console.log(`\n Exported ${result.filesWritten} files to ${result.outputDir}`);
418
+ console.log(' See context-hub/README.md for submission instructions');
419
+ }
338
420
  }
339
421
  console.log('\n=== Summary ===');
340
422
  console.log(` Total elements: ${totalDocs}`);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Self-healing command: skrypt heal
3
+ *
4
+ * Orchestrates 3 phases of documentation healing:
5
+ * Phase 1: QA fixes (frontmatter, syntax, code blocks, security)
6
+ * Phase 2: Code example fixes (LLM-powered)
7
+ * Phase 3: Screenshot capture (headless browser)
8
+ */
9
+ import { Command } from 'commander';
10
+ export declare const healCommand: Command;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Self-healing command: skrypt heal
3
+ *
4
+ * Orchestrates 3 phases of documentation healing:
5
+ * Phase 1: QA fixes (frontmatter, syntax, code blocks, security)
6
+ * Phase 2: Code example fixes (LLM-powered)
7
+ * Phase 3: Screenshot capture (headless browser)
8
+ */
9
+ import { Command } from 'commander';
10
+ import { resolve } from 'path';
11
+ import { readFileSync, writeFileSync } from 'fs';
12
+ import { requirePro } from '../auth/index.js';
13
+ import { fixQAIssues, printFixReport, runQA, printQAReport } from '../qa/index.js';
14
+ import { loadConfig, checkApiKey } from '../config/index.js';
15
+ import { createLLMClient } from '../llm/index.js';
16
+ import { autoFixExample, createTypeScriptValidator, createPythonValidator } from '../autofix/index.js';
17
+ import { findMdxFiles } from '../utils/files.js';
18
+ import { runCapture, printCaptureReport } from '../capture/index.js';
19
+ export const healCommand = new Command('heal')
20
+ .description('Self-heal documentation: fix QA issues, broken code, and capture screenshots')
21
+ .argument('[docs-dir]', 'Documentation directory', '.')
22
+ .option('--base-url <url>', 'Base URL for screenshot capture', 'http://localhost:3000')
23
+ .option('--output <dir>', 'Screenshot output directory', 'public/screenshots')
24
+ .option('--viewport <size>', 'Viewport WxH', '1280x720')
25
+ .option('--dark', 'Also capture dark mode variants')
26
+ .option('--diff', 'Only re-capture changed screenshots')
27
+ .option('--screenshots', 'Run screenshot capture phase')
28
+ .option('--qa-only', 'Run only QA fix phase')
29
+ .option('--code-only', 'Run only code fix phase')
30
+ .option('--deep', 'Run all 3 phases')
31
+ .option('--dry-run', 'Show what would change without writing')
32
+ .option('--timeout <ms>', 'Page load timeout', '30000')
33
+ .option('--wait <ms>', 'Wait after page load', '1000')
34
+ .option('--device <name>', 'Emulate device (e.g. "iPhone 14")')
35
+ .option('--concurrency <n>', 'Parallel capture limit', '3')
36
+ .option('--provider <name>', 'LLM provider (for code fix phase)')
37
+ .option('--model <name>', 'LLM model (for code fix phase)')
38
+ .option('--max-iterations <n>', 'Max fix attempts per code example', '3')
39
+ .option('-c, --config <file>', 'Config file path')
40
+ .action(async (docsDir, options) => {
41
+ try {
42
+ // Pro feature
43
+ if (!await requirePro('heal')) {
44
+ process.exit(1);
45
+ }
46
+ const resolvedDir = resolve(docsDir);
47
+ const [vpWidth, vpHeight] = options.viewport.split('x').map(Number);
48
+ // Determine which phases to run
49
+ const hasPhaseFlag = options.qaOnly || options.codeOnly || options.screenshots;
50
+ const runPhase1 = options.deep || options.qaOnly || !hasPhaseFlag;
51
+ const runPhase2 = options.deep || options.codeOnly;
52
+ const runPhase3 = options.deep || options.screenshots;
53
+ console.log('skrypt heal');
54
+ console.log(` directory: ${resolvedDir}`);
55
+ const phases = [];
56
+ if (runPhase1)
57
+ phases.push('QA fixes');
58
+ if (runPhase2)
59
+ phases.push('code fixes');
60
+ if (runPhase3)
61
+ phases.push('screenshots');
62
+ console.log(` phases: ${phases.join(', ')}`);
63
+ if (options.dryRun)
64
+ console.log(' mode: dry run');
65
+ console.log('');
66
+ // ── Phase 1: QA Fixes ──
67
+ if (runPhase1) {
68
+ console.log(' ── Phase 1: QA Fixes ──');
69
+ if (options.dryRun) {
70
+ const qaReport = runQA(resolvedDir);
71
+ printQAReport(qaReport);
72
+ }
73
+ else {
74
+ const fixReport = fixQAIssues(resolvedDir);
75
+ printFixReport(fixReport);
76
+ if (fixReport.totalFixes > 0) {
77
+ const qaReport = runQA(resolvedDir);
78
+ printQAReport(qaReport);
79
+ }
80
+ else {
81
+ console.log(' No QA issues to fix.');
82
+ }
83
+ }
84
+ console.log('');
85
+ }
86
+ // ── Phase 2: Code Fixes ──
87
+ if (runPhase2) {
88
+ console.log(' ── Phase 2: Code Fixes ──');
89
+ const config = loadConfig(options.config);
90
+ if (options.provider)
91
+ config.llm.provider = options.provider;
92
+ if (options.model)
93
+ config.llm.model = options.model;
94
+ const { ok, envKey } = checkApiKey(config.llm.provider);
95
+ if (!ok && envKey) {
96
+ console.error(` Error: ${envKey} environment variable required for code fixes`);
97
+ console.error(' Skipping Phase 2.\n');
98
+ }
99
+ else {
100
+ const client = createLLMClient({
101
+ provider: config.llm.provider,
102
+ model: config.llm.model,
103
+ baseUrl: config.llm.baseUrl,
104
+ });
105
+ const maxIterations = parseInt(options.maxIterations, 10) || 3;
106
+ const validators = {
107
+ javascript: createTypeScriptValidator(),
108
+ typescript: createTypeScriptValidator(),
109
+ ts: createTypeScriptValidator(),
110
+ js: createTypeScriptValidator(),
111
+ python: createPythonValidator(),
112
+ py: createPythonValidator(),
113
+ };
114
+ const docFiles = findMdxFiles(resolvedDir);
115
+ let totalFixed = 0;
116
+ let totalFailed = 0;
117
+ for (const filePath of docFiles) {
118
+ let content;
119
+ try {
120
+ content = readFileSync(filePath, 'utf-8');
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ // Find code blocks
126
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
127
+ const codeBlocks = [];
128
+ let match;
129
+ while ((match = codeBlockRegex.exec(content)) !== null) {
130
+ const code = match[2];
131
+ if (!code)
132
+ continue;
133
+ codeBlocks.push({
134
+ match: match[0],
135
+ language: match[1] || 'javascript',
136
+ code,
137
+ });
138
+ }
139
+ if (codeBlocks.length === 0)
140
+ continue;
141
+ let newContent = content;
142
+ let fileFixed = 0;
143
+ for (const block of codeBlocks) {
144
+ const validator = validators[block.language.toLowerCase()];
145
+ if (!validator)
146
+ continue;
147
+ const initial = await validator(block.code);
148
+ if (initial.valid)
149
+ continue;
150
+ const result = await autoFixExample({ code: block.code, language: block.language, context: `Code example from ${filePath}` }, client, { maxIterations, validateFn: validator, language: block.language });
151
+ if (result.success) {
152
+ const newBlock = '```' + block.language + '\n' + result.fixedCode + '```';
153
+ newContent = newContent.replace(block.match, newBlock);
154
+ fileFixed++;
155
+ totalFixed++;
156
+ }
157
+ else {
158
+ totalFailed++;
159
+ }
160
+ }
161
+ if (fileFixed > 0 && !options.dryRun) {
162
+ writeFileSync(filePath, newContent, 'utf-8');
163
+ }
164
+ }
165
+ if (totalFixed > 0 || totalFailed > 0) {
166
+ console.log(` Code fixes: ${totalFixed} fixed, ${totalFailed} failed`);
167
+ }
168
+ else {
169
+ console.log(' All code examples are valid.');
170
+ }
171
+ if (options.dryRun && totalFixed > 0) {
172
+ console.log(' [dry run — no changes written]');
173
+ }
174
+ }
175
+ console.log('');
176
+ }
177
+ // ── Phase 3: Screenshot Capture ──
178
+ if (runPhase3) {
179
+ console.log(' ── Phase 3: Screenshot Capture ──');
180
+ const captureOptions = {
181
+ baseUrl: options.baseUrl,
182
+ outputDir: options.output,
183
+ viewport: { width: vpWidth || 1280, height: vpHeight || 720 },
184
+ dark: options.dark || false,
185
+ diff: options.diff || false,
186
+ dryRun: options.dryRun || false,
187
+ timeout: parseInt(options.timeout, 10) || 30000,
188
+ wait: parseInt(options.wait, 10) || 1000,
189
+ device: options.device,
190
+ concurrency: parseInt(options.concurrency, 10) || 3,
191
+ };
192
+ const captureReport = await runCapture(resolvedDir, captureOptions);
193
+ printCaptureReport(captureReport);
194
+ }
195
+ console.log('Done!');
196
+ }
197
+ catch (err) {
198
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
199
+ process.exit(1);
200
+ }
201
+ });