skrypt-ai 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) 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/auth/index.js +6 -4
  11. package/dist/cli.js +12 -2
  12. package/dist/commands/audit.d.ts +2 -0
  13. package/dist/commands/audit.js +59 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +73 -0
  16. package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
  17. package/dist/commands/generate/index.js +393 -0
  18. package/dist/commands/generate/scan.d.ts +41 -0
  19. package/dist/commands/generate/scan.js +256 -0
  20. package/dist/commands/generate/verify.d.ts +14 -0
  21. package/dist/commands/generate/verify.js +122 -0
  22. package/dist/commands/generate/write.d.ts +25 -0
  23. package/dist/commands/generate/write.js +120 -0
  24. package/dist/commands/import.js +4 -1
  25. package/dist/commands/llms-txt.js +6 -4
  26. package/dist/commands/refresh.d.ts +2 -0
  27. package/dist/commands/refresh.js +158 -0
  28. package/dist/commands/review.d.ts +2 -0
  29. package/dist/commands/review.js +110 -0
  30. package/dist/commands/test.js +177 -236
  31. package/dist/commands/watch.js +29 -20
  32. package/dist/config/loader.d.ts +6 -2
  33. package/dist/config/loader.js +39 -3
  34. package/dist/config/types.d.ts +7 -0
  35. package/dist/generator/agents-md.d.ts +25 -0
  36. package/dist/generator/agents-md.js +122 -0
  37. package/dist/generator/generator.js +2 -1
  38. package/dist/generator/index.d.ts +2 -0
  39. package/dist/generator/index.js +2 -0
  40. package/dist/generator/mdx-serializer.d.ts +11 -0
  41. package/dist/generator/mdx-serializer.js +135 -0
  42. package/dist/generator/organizer.d.ts +1 -16
  43. package/dist/generator/organizer.js +0 -38
  44. package/dist/generator/types.d.ts +3 -0
  45. package/dist/generator/writer.js +65 -32
  46. package/dist/github/org-discovery.d.ts +17 -0
  47. package/dist/github/org-discovery.js +93 -0
  48. package/dist/llm/index.d.ts +2 -0
  49. package/dist/llm/index.js +8 -2
  50. package/dist/llm/proxy-client.d.ts +32 -0
  51. package/dist/llm/proxy-client.js +103 -0
  52. package/dist/next-actions/actions.d.ts +2 -0
  53. package/dist/next-actions/actions.js +190 -0
  54. package/dist/next-actions/index.d.ts +6 -0
  55. package/dist/next-actions/index.js +39 -0
  56. package/dist/next-actions/setup.d.ts +2 -0
  57. package/dist/next-actions/setup.js +72 -0
  58. package/dist/next-actions/state.d.ts +7 -0
  59. package/dist/next-actions/state.js +68 -0
  60. package/dist/next-actions/suggest.d.ts +3 -0
  61. package/dist/next-actions/suggest.js +47 -0
  62. package/dist/next-actions/types.d.ts +26 -0
  63. package/dist/refresh/differ.d.ts +9 -0
  64. package/dist/refresh/differ.js +67 -0
  65. package/dist/refresh/index.d.ts +4 -0
  66. package/dist/refresh/index.js +4 -0
  67. package/dist/refresh/manifest.d.ts +18 -0
  68. package/dist/refresh/manifest.js +71 -0
  69. package/dist/refresh/splicer.d.ts +9 -0
  70. package/dist/refresh/splicer.js +50 -0
  71. package/dist/refresh/types.d.ts +37 -0
  72. package/dist/review/index.d.ts +8 -0
  73. package/dist/review/index.js +94 -0
  74. package/dist/review/parser.d.ts +16 -0
  75. package/dist/review/parser.js +95 -0
  76. package/dist/review/types.d.ts +18 -0
  77. package/dist/scanner/csharp.d.ts +0 -4
  78. package/dist/scanner/csharp.js +9 -49
  79. package/dist/scanner/go.d.ts +0 -3
  80. package/dist/scanner/go.js +8 -35
  81. package/dist/scanner/java.d.ts +0 -4
  82. package/dist/scanner/java.js +9 -49
  83. package/dist/scanner/kotlin.d.ts +0 -3
  84. package/dist/scanner/kotlin.js +6 -33
  85. package/dist/scanner/php.d.ts +0 -10
  86. package/dist/scanner/php.js +11 -55
  87. package/dist/scanner/ruby.d.ts +0 -3
  88. package/dist/scanner/ruby.js +8 -38
  89. package/dist/scanner/rust.d.ts +0 -3
  90. package/dist/scanner/rust.js +10 -37
  91. package/dist/scanner/swift.d.ts +0 -3
  92. package/dist/scanner/swift.js +8 -35
  93. package/dist/scanner/types.d.ts +2 -0
  94. package/dist/scanner/utils.d.ts +41 -0
  95. package/dist/scanner/utils.js +97 -0
  96. package/dist/structure/index.d.ts +19 -0
  97. package/dist/structure/index.js +92 -0
  98. package/dist/structure/planner.d.ts +8 -0
  99. package/dist/structure/planner.js +180 -0
  100. package/dist/structure/topology.d.ts +16 -0
  101. package/dist/structure/topology.js +49 -0
  102. package/dist/structure/types.d.ts +26 -0
  103. package/dist/template/docs.json +5 -2
  104. package/dist/template/next.config.mjs +31 -0
  105. package/dist/template/package.json +5 -3
  106. package/dist/template/src/app/layout.tsx +13 -13
  107. package/dist/template/src/app/llms-full.md/route.ts +29 -0
  108. package/dist/template/src/app/llms.txt/route.ts +29 -0
  109. package/dist/template/src/app/md/[...slug]/route.ts +174 -0
  110. package/dist/template/src/app/reference/route.ts +22 -18
  111. package/dist/template/src/app/sitemap.ts +1 -1
  112. package/dist/template/src/components/ai-chat-impl.tsx +206 -0
  113. package/dist/template/src/components/ai-chat.tsx +20 -193
  114. package/dist/template/src/components/mdx/index.tsx +27 -4
  115. package/dist/template/src/lib/fonts.ts +135 -0
  116. package/dist/template/src/middleware.ts +101 -0
  117. package/dist/template/src/styles/globals.css +28 -20
  118. package/dist/testing/comparator.d.ts +7 -0
  119. package/dist/testing/comparator.js +77 -0
  120. package/dist/testing/docker.d.ts +21 -0
  121. package/dist/testing/docker.js +234 -0
  122. package/dist/testing/env.d.ts +16 -0
  123. package/dist/testing/env.js +58 -0
  124. package/dist/testing/extractor.d.ts +9 -0
  125. package/dist/testing/extractor.js +195 -0
  126. package/dist/testing/index.d.ts +6 -0
  127. package/dist/testing/index.js +6 -0
  128. package/dist/testing/runner.d.ts +5 -0
  129. package/dist/testing/runner.js +225 -0
  130. package/dist/testing/types.d.ts +58 -0
  131. package/dist/utils/files.d.ts +0 -8
  132. package/dist/utils/files.js +0 -33
  133. package/package.json +1 -1
  134. package/dist/autofix/autofix.test.js +0 -487
  135. package/dist/commands/generate.js +0 -445
  136. package/dist/generator/generator.test.js +0 -259
  137. package/dist/generator/writer.test.js +0 -411
  138. package/dist/llm/llm.manual-test.js +0 -112
  139. package/dist/llm/llm.mock-test.d.ts +0 -4
  140. package/dist/llm/llm.mock-test.js +0 -79
  141. package/dist/plugins/index.d.ts +0 -47
  142. package/dist/plugins/index.js +0 -181
  143. package/dist/scanner/content-type.test.js +0 -231
  144. package/dist/scanner/integration.test.d.ts +0 -4
  145. package/dist/scanner/integration.test.js +0 -180
  146. package/dist/scanner/scanner.test.js +0 -210
  147. package/dist/scanner/typescript.manual-test.d.ts +0 -1
  148. package/dist/scanner/typescript.manual-test.js +0 -112
  149. package/dist/template/src/app/docs/auth/page.mdx +0 -589
  150. package/dist/template/src/app/docs/autofix/page.mdx +0 -624
  151. package/dist/template/src/app/docs/cli/page.mdx +0 -217
  152. package/dist/template/src/app/docs/config/page.mdx +0 -428
  153. package/dist/template/src/app/docs/configuration/page.mdx +0 -86
  154. package/dist/template/src/app/docs/deployment/page.mdx +0 -112
  155. package/dist/template/src/app/docs/generator/generator.md +0 -504
  156. package/dist/template/src/app/docs/generator/organizer.md +0 -779
  157. package/dist/template/src/app/docs/generator/page.mdx +0 -613
  158. package/dist/template/src/app/docs/github/page.mdx +0 -502
  159. package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
  160. package/dist/template/src/app/docs/llm/index.md +0 -471
  161. package/dist/template/src/app/docs/llm/page.mdx +0 -428
  162. package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
  163. package/dist/template/src/app/docs/pro/page.mdx +0 -121
  164. package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
  165. package/dist/template/src/app/docs/scanner/content-type.md +0 -599
  166. package/dist/template/src/app/docs/scanner/index.md +0 -212
  167. package/dist/template/src/app/docs/scanner/page.mdx +0 -307
  168. package/dist/template/src/app/docs/scanner/python.md +0 -469
  169. package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
  170. package/dist/template/src/app/docs/scanner/rust.md +0 -325
  171. package/dist/template/src/app/docs/scanner/typescript.md +0 -201
  172. package/dist/template/src/app/icon.tsx +0 -29
  173. package/dist/utils/validation.d.ts +0 -1
  174. package/dist/utils/validation.js +0 -12
  175. /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
  176. /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
  177. /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
  178. /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
  179. /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
  180. /package/dist/{scanner/scanner.test.d.ts → testing/types.js} +0 -0
@@ -0,0 +1,256 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { resolveSourceEntries } from '../../config/index.js';
4
+ import { scanDirectory } from '../../scanner/index.js';
5
+ import { discoverOrgRepos, cloneRepoToTemp } from '../../github/org-discovery.js';
6
+ /**
7
+ * Parse source arguments with optional labels.
8
+ * e.g. "./api:API" -> { path: "./api", label: "API" }
9
+ * e.g. "./src" -> { path: "./src" }
10
+ */
11
+ export function parseSourceArgs(args) {
12
+ return args.map(arg => {
13
+ const colonIdx = arg.lastIndexOf(':');
14
+ // Only treat as label separator if colon is not part of a path (e.g. C:\)
15
+ if (colonIdx > 1 && !arg.substring(0, colonIdx).endsWith('\\')) {
16
+ return {
17
+ path: arg.substring(0, colonIdx),
18
+ label: arg.substring(colonIdx + 1),
19
+ };
20
+ }
21
+ return { path: arg };
22
+ });
23
+ }
24
+ /**
25
+ * Read .skryptignore patterns from source directory
26
+ */
27
+ export function readIgnorePatterns(sourcePath) {
28
+ const ignorePath = join(sourcePath, '.skryptignore');
29
+ if (!existsSync(ignorePath))
30
+ return [];
31
+ const content = readFileSync(ignorePath, 'utf-8');
32
+ return content
33
+ .split('\n')
34
+ .map(line => line.trim())
35
+ .filter(line => line && !line.startsWith('#'));
36
+ }
37
+ /**
38
+ * Auto-detect OpenAPI spec file in source directory
39
+ */
40
+ export function findOpenAPISpec(sourcePath) {
41
+ const candidates = [
42
+ 'openapi.json',
43
+ 'openapi.yaml',
44
+ 'openapi.yml',
45
+ 'swagger.json',
46
+ 'swagger.yaml',
47
+ 'swagger.yml',
48
+ 'api.json',
49
+ 'api.yaml',
50
+ 'api.yml',
51
+ ];
52
+ for (const name of candidates) {
53
+ const specPath = join(sourcePath, name);
54
+ if (existsSync(specPath)) {
55
+ return specPath;
56
+ }
57
+ }
58
+ // Check common subdirectories
59
+ const subdirs = ['docs', 'api', 'spec', '.'];
60
+ for (const subdir of subdirs) {
61
+ for (const name of candidates) {
62
+ const specPath = join(sourcePath, subdir, name);
63
+ if (existsSync(specPath)) {
64
+ return specPath;
65
+ }
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Check if element should be excluded based on patterns
72
+ */
73
+ export function shouldExcludeElement(element, patterns) {
74
+ for (const pattern of patterns) {
75
+ // Match by name
76
+ if (pattern.startsWith('name:')) {
77
+ const namePattern = pattern.slice(5);
78
+ if (element.name === namePattern) {
79
+ return true;
80
+ }
81
+ // Only use regex if the pattern contains regex metacharacters
82
+ // Reject patterns with nested quantifiers to prevent catastrophic backtracking (ReDoS)
83
+ if (/[*+?{}()|[\]\\^$.]/.test(namePattern)) {
84
+ if (/(\+|\*|\?)\{|\(\?[^:)]|\(\.[*+].*\)\+|\([^)]*[+*][^)]*\)[+*]/.test(namePattern)) {
85
+ continue; // Skip patterns prone to catastrophic backtracking
86
+ }
87
+ try {
88
+ if (new RegExp(namePattern).test(element.name))
89
+ return true;
90
+ }
91
+ catch {
92
+ // Invalid regex — treat as literal match (already checked above)
93
+ }
94
+ }
95
+ }
96
+ // Match by file path
97
+ else if (pattern.includes('/') || pattern.includes('*')) {
98
+ const filePath = element.filePath;
99
+ if (pattern.includes('**')) {
100
+ const parts = pattern.split('**');
101
+ const prefixMatch = !parts[0] || filePath.includes(parts[0].replace(/^\//, ''));
102
+ const suffixMatch = !parts[1] || filePath.includes(parts[1].replace(/^\//, ''));
103
+ if (prefixMatch && suffixMatch)
104
+ return true;
105
+ }
106
+ else if (filePath.includes(pattern.replace(/\*/g, ''))) {
107
+ return true;
108
+ }
109
+ }
110
+ // Match by exact name
111
+ else if (element.name === pattern) {
112
+ return true;
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+ /**
118
+ * Scan all sources (CLI args, --org, or config), apply filters, return elements.
119
+ */
120
+ export async function scanSources(sources, options, config) {
121
+ // Resolve source entries: CLI args, --org, or config file
122
+ let sourceEntries;
123
+ const tempDirs = [];
124
+ if (options.org) {
125
+ // GitHub org discovery mode
126
+ const token = process.env.GITHUB_TOKEN;
127
+ if (!token) {
128
+ throw new Error('GITHUB_TOKEN environment variable required for --org');
129
+ }
130
+ console.log(`Discovering repos in org "${options.org}"...`);
131
+ const repoWhitelist = options.repos ? options.repos.split(',').map(r => r.trim()) : undefined;
132
+ const repoBlacklist = options.excludeRepos ? options.excludeRepos.split(',').map(r => r.trim()) : undefined;
133
+ let discoveredRepos = await discoverOrgRepos(options.org, token);
134
+ if (repoWhitelist) {
135
+ discoveredRepos = discoveredRepos.filter(r => repoWhitelist.includes(r.name));
136
+ }
137
+ if (repoBlacklist) {
138
+ discoveredRepos = discoveredRepos.filter(r => !repoBlacklist.includes(r.name));
139
+ }
140
+ console.log(` Found ${discoveredRepos.length} repos`);
141
+ sourceEntries = [];
142
+ for (const repo of discoveredRepos) {
143
+ console.log(` Cloning ${repo.full_name}...`);
144
+ const tempDir = await cloneRepoToTemp(repo, token);
145
+ tempDirs.push(tempDir);
146
+ sourceEntries.push({
147
+ path: tempDir,
148
+ label: repo.name,
149
+ include: config.source.include,
150
+ exclude: config.source.exclude,
151
+ });
152
+ }
153
+ }
154
+ else if (sources.length > 1) {
155
+ // Multiple CLI args
156
+ sourceEntries = parseSourceArgs(sources);
157
+ }
158
+ else {
159
+ // Single source or config file sources
160
+ sourceEntries = resolveSourceEntries(config);
161
+ }
162
+ const isMultiSource = sourceEntries.length > 1 || sourceEntries.some(s => s.label);
163
+ // Check all source paths exist
164
+ for (const entry of sourceEntries) {
165
+ const sourcePath = resolve(entry.path);
166
+ if (!existsSync(sourcePath)) {
167
+ throw new Error(`Source directory not found: ${sourcePath}`);
168
+ }
169
+ }
170
+ // Step 1: Scan source code from all sources
171
+ console.log('Step 1: Scanning source code...');
172
+ let allElements = [];
173
+ let totalFiles = 0;
174
+ const allScanErrors = [];
175
+ for (const entry of sourceEntries) {
176
+ const sourcePath = resolve(entry.path);
177
+ if (isMultiSource) {
178
+ console.log(`\n Scanning ${entry.label || entry.path}...`);
179
+ }
180
+ const scanResult = await scanDirectory(sourcePath, {
181
+ include: entry.include || config.source.include,
182
+ exclude: entry.exclude || config.source.exclude,
183
+ onProgress: (current, total, file) => {
184
+ process.stdout.write(`\r [${current}/${total}] ${file.slice(-50).padStart(50)}`);
185
+ }
186
+ });
187
+ console.log('');
188
+ if (scanResult.errors.length > 0) {
189
+ allScanErrors.push(...scanResult.errors);
190
+ }
191
+ totalFiles += scanResult.files.length;
192
+ // Tag elements with source metadata
193
+ for (const file of scanResult.files) {
194
+ for (const el of file.elements) {
195
+ if (entry.label) {
196
+ el.sourceLabel = entry.label;
197
+ }
198
+ el.sourceRoot = sourcePath;
199
+ allElements.push(el);
200
+ }
201
+ }
202
+ }
203
+ if (allScanErrors.length > 0) {
204
+ console.log('\n Scan warnings:');
205
+ allScanErrors.slice(0, 5).forEach(e => console.log(` - ${e}`));
206
+ if (allScanErrors.length > 5) {
207
+ console.log(` ... and ${allScanErrors.length - 5} more`);
208
+ }
209
+ }
210
+ console.log(`\n Found ${allElements.length} API elements in ${totalFiles} files`);
211
+ if (allElements.length === 0) {
212
+ console.log(' No API elements found. Nothing to generate.');
213
+ return { allElements, sourceEntries, primarySourcePath: resolve(sourceEntries[0].path), tempDirs, isMultiSource };
214
+ }
215
+ // Apply privacy filters
216
+ const initialCount = allElements.length;
217
+ // 1. --public-only: filter to exported/public APIs only
218
+ if (options.publicOnly) {
219
+ allElements = allElements.filter(el => el.isExported === true || el.isPublic === true);
220
+ console.log(` --public-only: filtered to ${allElements.length} exported APIs`);
221
+ }
222
+ // 2. Load .skryptignore patterns from all source dirs
223
+ const ignorePatterns = [];
224
+ for (const entry of sourceEntries) {
225
+ ignorePatterns.push(...readIgnorePatterns(resolve(entry.path)));
226
+ }
227
+ if (ignorePatterns.length > 0) {
228
+ console.log(` .skryptignore: loaded ${ignorePatterns.length} patterns`);
229
+ }
230
+ // 3. Combine with --exclude patterns
231
+ const excludePatterns = [...ignorePatterns, ...(options.exclude || [])];
232
+ if (excludePatterns.length > 0) {
233
+ allElements = allElements.filter(el => !shouldExcludeElement(el, excludePatterns));
234
+ if (options.exclude?.length) {
235
+ console.log(` --exclude: applied ${options.exclude.length} additional patterns`);
236
+ }
237
+ }
238
+ if (initialCount !== allElements.length) {
239
+ console.log(` Filtered: ${initialCount} -> ${allElements.length} elements`);
240
+ }
241
+ // Show summary by kind
242
+ const byKind = {};
243
+ for (const el of allElements) {
244
+ byKind[el.kind] = (byKind[el.kind] || 0) + 1;
245
+ }
246
+ const pluralize = (word, count) => {
247
+ if (count === 1)
248
+ return word;
249
+ if (word === 'class')
250
+ return 'classes';
251
+ return word + 's';
252
+ };
253
+ console.log(' ' + Object.entries(byKind).map(([k, v]) => `${v} ${pluralize(k, v)}`).join(', '));
254
+ const primarySourcePath = resolve(sourceEntries[0].path);
255
+ return { allElements, sourceEntries, primarySourcePath, tempDirs, isMultiSource };
256
+ }
@@ -0,0 +1,14 @@
1
+ import { APIElement } from '../../scanner/index.js';
2
+ import { LLMClient } from '../../llm/index.js';
3
+ interface VerifyOptions {
4
+ envFile?: string;
5
+ maxVerifyIterations?: string;
6
+ multiLanguage: boolean;
7
+ externalContext?: Map<string, string>;
8
+ projectContext?: string;
9
+ }
10
+ /**
11
+ * Verify generated code examples by running them, with re-generation retry loop.
12
+ */
13
+ export declare function verifyCodeExamples(outputPath: string, allElements: APIElement[], client: LLMClient, primarySourcePath: string, options: VerifyOptions): Promise<void>;
14
+ export {};
@@ -0,0 +1,122 @@
1
+ import { dirname } from 'path';
2
+ import { resolve } from 'path';
3
+ import { generateForElements, groupDocsByFile, writeDocsToDirectory } from '../../generator/index.js';
4
+ import { extractSnippets, findDocFiles, runLocally, loadEnvFile } from '../../testing/index.js';
5
+ /**
6
+ * Verify generated code examples by running them, with re-generation retry loop.
7
+ */
8
+ export async function verifyCodeExamples(outputPath, allElements, client, primarySourcePath, options) {
9
+ console.log('\nStep 4: Verifying code examples...');
10
+ let verifyEnvVars = {};
11
+ if (options.envFile) {
12
+ try {
13
+ verifyEnvVars = loadEnvFile(resolve(options.envFile));
14
+ console.log(` Loaded ${Object.keys(verifyEnvVars).length} env var(s) from ${options.envFile}`);
15
+ }
16
+ catch {
17
+ console.log(` Warning: Could not load env file: ${options.envFile}`);
18
+ }
19
+ }
20
+ const verifyConfig = {
21
+ timeout: 15000,
22
+ envVars: verifyEnvVars,
23
+ installDeps: true,
24
+ };
25
+ const maxIterations = Math.max(1, parseInt(options.maxVerifyIterations ?? '2', 10) || 2);
26
+ let failedCount = 0;
27
+ let passedCount = 0;
28
+ let skippedCount = 0;
29
+ const genOptions = {
30
+ multiLanguage: options.multiLanguage,
31
+ externalContext: options.externalContext,
32
+ projectContext: options.projectContext,
33
+ onProgress: (_progress) => {
34
+ // Placeholder — overridden below for re-generation
35
+ },
36
+ };
37
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
38
+ const docFiles = findDocFiles(outputPath);
39
+ const allSnippets = docFiles.flatMap(f => extractSnippets(f));
40
+ if (allSnippets.length === 0) {
41
+ console.log(' No code snippets found to verify');
42
+ break;
43
+ }
44
+ if (iteration === 1) {
45
+ console.log(` Found ${allSnippets.length} code snippet(s) to verify`);
46
+ }
47
+ else {
48
+ console.log(`\n Retry ${iteration}/${maxIterations}: re-verifying ${allSnippets.length} snippet(s)...`);
49
+ }
50
+ failedCount = 0;
51
+ passedCount = 0;
52
+ skippedCount = 0;
53
+ const failedSnippets = [];
54
+ const snippetErrors = new Map();
55
+ for (const snippet of allSnippets) {
56
+ const result = await runLocally(snippet, verifyConfig);
57
+ if (result.status === 'pass') {
58
+ passedCount++;
59
+ }
60
+ else if (result.status === 'skip') {
61
+ skippedCount++;
62
+ }
63
+ else {
64
+ failedCount++;
65
+ failedSnippets.push(snippet);
66
+ const errorMsg = result.stderr?.trim().split('\n').slice(0, 5).join('\n') || `Exit code: ${result.exitCode}`;
67
+ snippetErrors.set(snippet.filePath + ':' + snippet.lineNumber, errorMsg);
68
+ console.log(` \x1b[31m✗\x1b[0m ${snippet.filePath}:${snippet.lineNumber} [${snippet.language}]`);
69
+ if (result.stderr) {
70
+ console.log(` ${result.stderr.trim().split('\n')[0]?.slice(0, 80)}`);
71
+ }
72
+ }
73
+ }
74
+ // If all passed or last iteration, stop
75
+ if (failedCount === 0 || iteration === maxIterations) {
76
+ break;
77
+ }
78
+ // Re-generate failing docs by re-running generation for elements whose docs had failing snippets
79
+ console.log(`\n Re-generating ${failedSnippets.length} failing snippet(s)...`);
80
+ const failedFiles = [...new Set(failedSnippets.map(s => s.filePath))];
81
+ for (const failedFile of failedFiles) {
82
+ // Find elements that map to this doc file
83
+ const fileSnippets = failedSnippets.filter(s => s.filePath === failedFile);
84
+ const matchingElements = allElements.filter(el => fileSnippets.some(s => {
85
+ // Match element name in the snippet's surrounding code or filename
86
+ const elNameLower = el.name.toLowerCase();
87
+ return s.code.toLowerCase().includes(elNameLower) ||
88
+ failedFile.toLowerCase().includes(elNameLower);
89
+ }));
90
+ if (matchingElements.length > 0) {
91
+ // Build error context map: element name -> error message
92
+ const previousErrors = new Map();
93
+ for (const snippet of fileSnippets) {
94
+ const errKey = snippet.filePath + ':' + snippet.lineNumber;
95
+ const errMsg = snippetErrors.get(errKey);
96
+ if (errMsg) {
97
+ // Map snippet back to element name
98
+ const matchedEl = matchingElements.find(el => snippet.code.toLowerCase().includes(el.name.toLowerCase()));
99
+ if (matchedEl) {
100
+ previousErrors.set(matchedEl.name, errMsg);
101
+ }
102
+ }
103
+ }
104
+ const reDocs = await generateForElements(matchingElements, client, {
105
+ ...genOptions,
106
+ verify: true,
107
+ previousErrors,
108
+ onProgress: (p) => {
109
+ process.stdout.write(`\r Re-generating: ${p.element} ${p.status}`.padEnd(80));
110
+ },
111
+ });
112
+ console.log('');
113
+ // Re-write the doc file with updated content
114
+ const fileResults = groupDocsByFile(reDocs);
115
+ await writeDocsToDirectory(fileResults, dirname(failedFile), primarySourcePath);
116
+ }
117
+ }
118
+ }
119
+ if (failedCount + passedCount + skippedCount > 0) {
120
+ console.log(`\n Verification: ${passedCount} passed, ${failedCount} failed, ${skippedCount} skipped`);
121
+ }
122
+ }
@@ -0,0 +1,25 @@
1
+ import { APIElement } from '../../scanner/index.js';
2
+ type GeneratedDoc = Awaited<ReturnType<typeof import('../../generator/index.js').generateForElements>>[number];
3
+ interface WriteDocsOptions {
4
+ byTopic?: boolean;
5
+ }
6
+ interface WriteDocsResult {
7
+ filesWritten: number;
8
+ totalDocs: number;
9
+ errorCount: number;
10
+ }
11
+ /**
12
+ * Write generated docs to output directory (byTopic / multiSource / single source).
13
+ */
14
+ export declare function writeDocs(docs: GeneratedDoc[], outputPath: string, primarySourcePath: string, isMultiSource: boolean, options: WriteDocsOptions): Promise<WriteDocsResult>;
15
+ interface WriteAssetsOptions {
16
+ openapi?: string;
17
+ projectName?: string;
18
+ baseUrl?: string;
19
+ agentsMd?: boolean;
20
+ }
21
+ /**
22
+ * Write post-generation assets: OpenAPI spec copy, llms.txt, AGENTS.md, manifest.
23
+ */
24
+ export declare function writeAssets(docs: GeneratedDoc[], allElements: APIElement[], outputPath: string, primarySourcePath: string, configOutputPath: string, filesWritten: number, options: WriteAssetsOptions): Promise<void>;
25
+ export {};
@@ -0,0 +1,120 @@
1
+ import { existsSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { resolve, basename, dirname, join, relative } from 'path';
3
+ import { groupDocsByFile, writeDocsToDirectory, writeDocsByTopic, writeLlmsTxt, generateAgentsMd } from '../../generator/index.js';
4
+ import { writeManifest, buildManifestEntries } from '../../refresh/manifest.js';
5
+ import { findOpenAPISpec } from './scan.js';
6
+ /**
7
+ * Write generated docs to output directory (byTopic / multiSource / single source).
8
+ */
9
+ export async function writeDocs(docs, outputPath, primarySourcePath, isMultiSource, options) {
10
+ let filesWritten;
11
+ let totalDocs;
12
+ if (options.byTopic) {
13
+ console.log(' mode: by-topic (grouped by concept)');
14
+ const result = await writeDocsByTopic(docs, outputPath);
15
+ filesWritten = result.filesWritten;
16
+ totalDocs = result.totalDocs;
17
+ console.log(` topics: ${result.topics.map(t => t.name).join(', ')}`);
18
+ }
19
+ else if (isMultiSource) {
20
+ // Multi-source: write docs namespaced by source label
21
+ filesWritten = 0;
22
+ totalDocs = 0;
23
+ // Group docs by source label
24
+ const bySource = new Map();
25
+ for (const doc of docs) {
26
+ const label = doc.element.sourceLabel || '_default';
27
+ if (!bySource.has(label))
28
+ bySource.set(label, []);
29
+ bySource.get(label).push(doc);
30
+ }
31
+ for (const [label, sourceDocs] of bySource) {
32
+ const fileResults = groupDocsByFile(sourceDocs);
33
+ const sourceOutputDir = label === '_default' ? outputPath : join(outputPath, label.toLowerCase());
34
+ const sourceRoot = sourceDocs[0]?.element.sourceRoot || primarySourcePath;
35
+ const result = await writeDocsToDirectory(fileResults, sourceOutputDir, sourceRoot);
36
+ filesWritten += result.filesWritten;
37
+ totalDocs += result.totalDocs;
38
+ if (label !== '_default') {
39
+ console.log(` ${label}: ${result.filesWritten} files`);
40
+ }
41
+ }
42
+ }
43
+ else {
44
+ // Default: file-based output (single source)
45
+ const fileResults = groupDocsByFile(docs);
46
+ const result = await writeDocsToDirectory(fileResults, outputPath, primarySourcePath);
47
+ filesWritten = result.filesWritten;
48
+ totalDocs = result.totalDocs;
49
+ }
50
+ const errorCount = docs.filter(d => d.error).length;
51
+ return { filesWritten, totalDocs, errorCount };
52
+ }
53
+ /**
54
+ * Write post-generation assets: OpenAPI spec copy, llms.txt, AGENTS.md, manifest.
55
+ */
56
+ export async function writeAssets(docs, allElements, outputPath, primarySourcePath, configOutputPath, filesWritten, options) {
57
+ console.log(`\n Wrote ${filesWritten} documentation files to ${outputPath}`);
58
+ // Copy OpenAPI spec (provided or auto-detected)
59
+ let specPath = options.openapi ? resolve(options.openapi) : null;
60
+ // Auto-detect if not provided
61
+ if (!specPath) {
62
+ const detected = findOpenAPISpec(primarySourcePath);
63
+ if (detected) {
64
+ specPath = detected;
65
+ console.log(`\n Auto-detected OpenAPI spec: ${basename(detected)}`);
66
+ }
67
+ }
68
+ if (specPath) {
69
+ if (existsSync(specPath)) {
70
+ const specFilename = basename(specPath);
71
+ const contentDir = dirname(outputPath);
72
+ const destPath = resolve(contentDir, specFilename);
73
+ mkdirSync(dirname(destPath), { recursive: true });
74
+ copyFileSync(specPath, destPath);
75
+ console.log(` Copied OpenAPI spec: ${specFilename} -> ${destPath}`);
76
+ console.log(' API Playground will be available at /reference');
77
+ }
78
+ else if (options.openapi) {
79
+ console.log(`\n Warning: OpenAPI spec not found: ${specPath}`);
80
+ }
81
+ }
82
+ // Always generate llms.txt for AEO (Answer Engine Optimization)
83
+ await writeLlmsTxt(docs, outputPath, {
84
+ projectName: options.projectName,
85
+ description: `API documentation for ${options.projectName || basename(primarySourcePath)}`
86
+ });
87
+ console.log(`\n Generated llms.txt and llms-full.md for AEO`);
88
+ // Generate AGENTS.md + CLAUDE.md for coding agent integration
89
+ if (options.agentsMd !== false) {
90
+ const projName = options.projectName || basename(primarySourcePath);
91
+ const pages = docs
92
+ .filter(d => !d.error)
93
+ .map(d => ({
94
+ title: d.element.name,
95
+ path: relative(outputPath, join(outputPath, basename(d.element.filePath).replace(/\.[^.]+$/, ''))),
96
+ description: d.markdown?.split(/\.\s/)[0]?.slice(0, 80),
97
+ category: d.element.sourceLabel,
98
+ }));
99
+ const agentsMdContent = generateAgentsMd({
100
+ projectName: projName,
101
+ docsPath: configOutputPath,
102
+ baseUrl: options.baseUrl,
103
+ pages,
104
+ hasLlmsTxt: true,
105
+ });
106
+ // Write to project root (one level up from docs output)
107
+ const projectRoot = resolve(outputPath, '..');
108
+ writeFileSync(join(projectRoot, 'AGENTS.md'), agentsMdContent, 'utf-8');
109
+ writeFileSync(join(projectRoot, 'CLAUDE.md'), agentsMdContent, 'utf-8');
110
+ console.log(` Generated AGENTS.md and CLAUDE.md for coding agent integration`);
111
+ }
112
+ // Write manifest for staleness detection
113
+ try {
114
+ const manifestEntries = buildManifestEntries(allElements, outputPath);
115
+ writeManifest(outputPath, manifestEntries);
116
+ }
117
+ catch {
118
+ // Non-fatal — manifest is optional
119
+ }
120
+ }
@@ -122,10 +122,13 @@ function scaffoldOutput(outputDir, result) {
122
122
  const docsJson = {
123
123
  name: result.name,
124
124
  description: result.description,
125
+ fonts: {
126
+ sans: 'Inter',
127
+ mono: 'JetBrains Mono',
128
+ },
125
129
  theme: {
126
130
  primaryColor: '#3b82f6',
127
131
  accentColor: '#8b5cf6',
128
- font: 'Inter',
129
132
  },
130
133
  navigation: result.navigation.map(group => ({
131
134
  group: group.group,
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs';
3
3
  import { resolve, join, extname, dirname } from 'path';
4
+ import { serializeMdxToMarkdown } from '../generator/mdx-serializer.js';
4
5
  function extractFrontmatter(content) {
5
6
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6
7
  if (!match)
@@ -11,12 +12,13 @@ function extractFrontmatter(content) {
11
12
  return { title, description };
12
13
  }
13
14
  function cleanContent(content) {
14
- return content
15
+ // First pass: use MDX serializer to convert JSX components to markdown
16
+ const serialized = serializeMdxToMarkdown(content);
17
+ return serialized
15
18
  .replace(/^---[\s\S]*?---\n?/, '') // Remove frontmatter
16
- .replace(/<[^>]+>/g, '') // Remove JSX/HTML
17
19
  .replace(/```[\s\S]*?```/g, '[code example]') // Simplify code blocks
20
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, '[image: $1]') // Convert images (before links!)
18
21
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text
19
- .replace(/!\[([^\]]*)\]\([^)]+\)/g, '[image: $1]') // Convert images
20
22
  .replace(/\n{3,}/g, '\n\n') // Normalize newlines
21
23
  .trim();
22
24
  }
@@ -90,7 +92,7 @@ This document provides an overview of the documentation for AI assistants and LL
90
92
  for (const doc of topDocs) {
91
93
  output += `### ${doc.title}
92
94
 
93
- ${doc.content.slice(0, 500)}...
95
+ ${doc.content.slice(0, 500)}${doc.content.length > 500 ? '...' : ''}
94
96
 
95
97
  [Read more](${options.baseUrl}/docs${doc.path})
96
98
 
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const refreshCommand: Command;