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.
- 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/auth/index.js +6 -4
- package/dist/cli.js +12 -2
- 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/{generate.d.ts → generate/index.d.ts} +1 -0
- package/dist/commands/generate/index.js +393 -0
- package/dist/commands/generate/scan.d.ts +41 -0
- package/dist/commands/generate/scan.js +256 -0
- package/dist/commands/generate/verify.d.ts +14 -0
- package/dist/commands/generate/verify.js +122 -0
- package/dist/commands/generate/write.d.ts +25 -0
- package/dist/commands/generate/write.js +120 -0
- package/dist/commands/import.js +4 -1
- package/dist/commands/llms-txt.js +6 -4
- package/dist/commands/refresh.d.ts +2 -0
- package/dist/commands/refresh.js +158 -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 -2
- package/dist/config/loader.js +39 -3
- package/dist/config/types.d.ts +7 -0
- package/dist/generator/agents-md.d.ts +25 -0
- package/dist/generator/agents-md.js +122 -0
- package/dist/generator/generator.js +2 -1
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/mdx-serializer.d.ts +11 -0
- package/dist/generator/mdx-serializer.js +135 -0
- package/dist/generator/organizer.d.ts +1 -16
- package/dist/generator/organizer.js +0 -38
- package/dist/generator/types.d.ts +3 -0
- package/dist/generator/writer.js +65 -32
- 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/llm/proxy-client.d.ts +32 -0
- package/dist/llm/proxy-client.js +103 -0
- 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/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/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/scanner/csharp.d.ts +0 -4
- package/dist/scanner/csharp.js +9 -49
- package/dist/scanner/go.d.ts +0 -3
- package/dist/scanner/go.js +8 -35
- package/dist/scanner/java.d.ts +0 -4
- package/dist/scanner/java.js +9 -49
- package/dist/scanner/kotlin.d.ts +0 -3
- package/dist/scanner/kotlin.js +6 -33
- package/dist/scanner/php.d.ts +0 -10
- package/dist/scanner/php.js +11 -55
- package/dist/scanner/ruby.d.ts +0 -3
- package/dist/scanner/ruby.js +8 -38
- package/dist/scanner/rust.d.ts +0 -3
- package/dist/scanner/rust.js +10 -37
- package/dist/scanner/swift.d.ts +0 -3
- package/dist/scanner/swift.js +8 -35
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/utils.d.ts +41 -0
- package/dist/scanner/utils.js +97 -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/template/docs.json +5 -2
- package/dist/template/next.config.mjs +31 -0
- package/dist/template/package.json +5 -3
- package/dist/template/src/app/layout.tsx +13 -13
- package/dist/template/src/app/llms-full.md/route.ts +29 -0
- package/dist/template/src/app/llms.txt/route.ts +29 -0
- package/dist/template/src/app/md/[...slug]/route.ts +174 -0
- package/dist/template/src/app/reference/route.ts +22 -18
- package/dist/template/src/app/sitemap.ts +1 -1
- package/dist/template/src/components/ai-chat-impl.tsx +206 -0
- package/dist/template/src/components/ai-chat.tsx +20 -193
- package/dist/template/src/components/mdx/index.tsx +27 -4
- package/dist/template/src/lib/fonts.ts +135 -0
- package/dist/template/src/middleware.ts +101 -0
- package/dist/template/src/styles/globals.css +28 -20
- 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/utils/files.d.ts +0 -8
- package/dist/utils/files.js +0 -33
- package/package.json +1 -1
- package/dist/autofix/autofix.test.js +0 -487
- package/dist/commands/generate.js +0 -445
- package/dist/generator/generator.test.js +0 -259
- package/dist/generator/writer.test.js +0 -411
- package/dist/llm/llm.manual-test.js +0 -112
- package/dist/llm/llm.mock-test.d.ts +0 -4
- package/dist/llm/llm.mock-test.js +0 -79
- package/dist/plugins/index.d.ts +0 -47
- package/dist/plugins/index.js +0 -181
- package/dist/scanner/content-type.test.js +0 -231
- package/dist/scanner/integration.test.d.ts +0 -4
- package/dist/scanner/integration.test.js +0 -180
- package/dist/scanner/scanner.test.js +0 -210
- package/dist/scanner/typescript.manual-test.d.ts +0 -1
- package/dist/scanner/typescript.manual-test.js +0 -112
- package/dist/template/src/app/docs/auth/page.mdx +0 -589
- package/dist/template/src/app/docs/autofix/page.mdx +0 -624
- package/dist/template/src/app/docs/cli/page.mdx +0 -217
- package/dist/template/src/app/docs/config/page.mdx +0 -428
- package/dist/template/src/app/docs/configuration/page.mdx +0 -86
- package/dist/template/src/app/docs/deployment/page.mdx +0 -112
- package/dist/template/src/app/docs/generator/generator.md +0 -504
- package/dist/template/src/app/docs/generator/organizer.md +0 -779
- package/dist/template/src/app/docs/generator/page.mdx +0 -613
- package/dist/template/src/app/docs/github/page.mdx +0 -502
- package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
- package/dist/template/src/app/docs/llm/index.md +0 -471
- package/dist/template/src/app/docs/llm/page.mdx +0 -428
- package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
- package/dist/template/src/app/docs/pro/page.mdx +0 -121
- package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
- package/dist/template/src/app/docs/scanner/content-type.md +0 -599
- package/dist/template/src/app/docs/scanner/index.md +0 -212
- package/dist/template/src/app/docs/scanner/page.mdx +0 -307
- package/dist/template/src/app/docs/scanner/python.md +0 -469
- package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
- package/dist/template/src/app/docs/scanner/rust.md +0 -325
- package/dist/template/src/app/docs/scanner/typescript.md +0 -201
- package/dist/template/src/app/icon.tsx +0 -29
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -12
- /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
- /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
- /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
- /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
- /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
- /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
|
+
}
|
package/dist/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|