skrypt-ai 0.1.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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, copyFileSync, mkdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, basename, dirname, join } from 'path';
|
|
4
|
+
import { loadConfig, validateConfig, checkApiKey } from '../config/index.js';
|
|
5
|
+
import { DEFAULT_MODELS } from '../config/types.js';
|
|
6
|
+
import { scanDirectory } from '../scanner/index.js';
|
|
7
|
+
import { createLLMClient } from '../llm/index.js';
|
|
8
|
+
import { generateForElements, groupDocsByFile, writeDocsToDirectory, writeDocsByTopic, writeLlmsTxt } from '../generator/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Read .skryptignore patterns from source directory
|
|
11
|
+
*/
|
|
12
|
+
function readIgnorePatterns(sourcePath) {
|
|
13
|
+
const ignorePath = join(sourcePath, '.skryptignore');
|
|
14
|
+
if (!existsSync(ignorePath))
|
|
15
|
+
return [];
|
|
16
|
+
const content = readFileSync(ignorePath, 'utf-8');
|
|
17
|
+
return content
|
|
18
|
+
.split('\n')
|
|
19
|
+
.map(line => line.trim())
|
|
20
|
+
.filter(line => line && !line.startsWith('#'));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Auto-detect OpenAPI spec file in source directory
|
|
24
|
+
*/
|
|
25
|
+
function findOpenAPISpec(sourcePath) {
|
|
26
|
+
const candidates = [
|
|
27
|
+
'openapi.json',
|
|
28
|
+
'openapi.yaml',
|
|
29
|
+
'openapi.yml',
|
|
30
|
+
'swagger.json',
|
|
31
|
+
'swagger.yaml',
|
|
32
|
+
'swagger.yml',
|
|
33
|
+
'api.json',
|
|
34
|
+
'api.yaml',
|
|
35
|
+
'api.yml',
|
|
36
|
+
];
|
|
37
|
+
for (const name of candidates) {
|
|
38
|
+
const specPath = join(sourcePath, name);
|
|
39
|
+
if (existsSync(specPath)) {
|
|
40
|
+
return specPath;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check common subdirectories
|
|
44
|
+
const subdirs = ['docs', 'api', 'spec', '.'];
|
|
45
|
+
for (const subdir of subdirs) {
|
|
46
|
+
for (const name of candidates) {
|
|
47
|
+
const specPath = join(sourcePath, subdir, name);
|
|
48
|
+
if (existsSync(specPath)) {
|
|
49
|
+
return specPath;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if element should be excluded based on patterns
|
|
57
|
+
*/
|
|
58
|
+
function shouldExcludeElement(element, patterns) {
|
|
59
|
+
for (const pattern of patterns) {
|
|
60
|
+
// Match by name
|
|
61
|
+
if (pattern.startsWith('name:')) {
|
|
62
|
+
const namePattern = pattern.slice(5);
|
|
63
|
+
if (element.name === namePattern || element.name.match(new RegExp(namePattern))) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Match by file path
|
|
68
|
+
else if (pattern.includes('/') || pattern.includes('*')) {
|
|
69
|
+
const filePath = element.filePath;
|
|
70
|
+
if (pattern.includes('**')) {
|
|
71
|
+
const parts = pattern.split('**');
|
|
72
|
+
const prefixMatch = !parts[0] || filePath.includes(parts[0].replace(/^\//, ''));
|
|
73
|
+
const suffixMatch = !parts[1] || filePath.includes(parts[1].replace(/^\//, ''));
|
|
74
|
+
if (prefixMatch && suffixMatch)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
else if (filePath.includes(pattern.replace(/\*/g, ''))) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Match by exact name
|
|
82
|
+
else if (element.name === pattern) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
export const generateCommand = new Command('generate')
|
|
89
|
+
.description('Generate documentation with code examples')
|
|
90
|
+
.argument('<source>', 'Source directory to scan')
|
|
91
|
+
.option('-o, --output <dir>', 'Output directory')
|
|
92
|
+
.option('-c, --config <file>', 'Config file path')
|
|
93
|
+
.option('--provider <name>', 'LLM provider (deepseek, openai, anthropic, google, ollama, openrouter)')
|
|
94
|
+
.option('--model <name>', 'LLM model name')
|
|
95
|
+
.option('--base-url <url>', 'Custom API base URL (for Ollama or proxies)')
|
|
96
|
+
.option('--dry-run', 'Scan only, do not generate docs')
|
|
97
|
+
.option('--multi-lang', 'Generate TypeScript + Python examples')
|
|
98
|
+
.option('--by-topic', 'Organize output by topic instead of file')
|
|
99
|
+
.option('--openapi <file>', 'Include OpenAPI spec file for API Playground')
|
|
100
|
+
.option('--public-only', 'Only document exported/public APIs')
|
|
101
|
+
.option('--exclude <patterns...>', 'Exclude patterns (files, names, or name:pattern)')
|
|
102
|
+
.option('--llms-txt', 'Generate llms.txt for Answer Engine Optimization (AEO)')
|
|
103
|
+
.option('--project-name <name>', 'Project name for llms.txt header')
|
|
104
|
+
.action(async (source, options) => {
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
// Load config (file or defaults)
|
|
107
|
+
const config = loadConfig(options.config);
|
|
108
|
+
// CLI flags override config
|
|
109
|
+
if (source)
|
|
110
|
+
config.source.path = source;
|
|
111
|
+
if (options.output)
|
|
112
|
+
config.output.path = options.output;
|
|
113
|
+
if (options.provider) {
|
|
114
|
+
config.llm.provider = options.provider;
|
|
115
|
+
// Use provider's default model unless explicitly specified
|
|
116
|
+
if (!options.model) {
|
|
117
|
+
config.llm.model = DEFAULT_MODELS[config.llm.provider];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (options.model)
|
|
121
|
+
config.llm.model = options.model;
|
|
122
|
+
if (options.baseUrl)
|
|
123
|
+
config.llm.baseUrl = options.baseUrl;
|
|
124
|
+
// Validate
|
|
125
|
+
const errors = validateConfig(config);
|
|
126
|
+
if (errors.length > 0) {
|
|
127
|
+
console.error('Config errors:');
|
|
128
|
+
errors.forEach(e => console.error(` - ${e}`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
// Check for API key (not needed for Ollama or dry-run)
|
|
132
|
+
if (!options.dryRun) {
|
|
133
|
+
const { ok, envKey } = checkApiKey(config.llm.provider);
|
|
134
|
+
if (!ok && envKey) {
|
|
135
|
+
console.error(`Error: ${envKey} environment variable required for ${config.llm.provider}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log('skrypt generate');
|
|
140
|
+
console.log(` source: ${config.source.path}`);
|
|
141
|
+
console.log(` output: ${config.output.path}`);
|
|
142
|
+
console.log(` provider: ${config.llm.provider}`);
|
|
143
|
+
console.log(` model: ${config.llm.model}`);
|
|
144
|
+
if (config.llm.baseUrl) {
|
|
145
|
+
console.log(` base url: ${config.llm.baseUrl}`);
|
|
146
|
+
}
|
|
147
|
+
console.log('');
|
|
148
|
+
// Check source exists
|
|
149
|
+
const sourcePath = resolve(config.source.path);
|
|
150
|
+
if (!existsSync(sourcePath)) {
|
|
151
|
+
console.error(`Error: Source directory not found: ${sourcePath}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// Step 1: Scan source code
|
|
155
|
+
console.log('Step 1: Scanning source code...');
|
|
156
|
+
const scanResult = await scanDirectory(sourcePath, {
|
|
157
|
+
include: config.source.include,
|
|
158
|
+
exclude: config.source.exclude,
|
|
159
|
+
onProgress: (current, total, file) => {
|
|
160
|
+
process.stdout.write(`\r [${current}/${total}] ${file.slice(-50).padStart(50)}`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
console.log('');
|
|
164
|
+
if (scanResult.errors.length > 0) {
|
|
165
|
+
console.log('\n Scan warnings:');
|
|
166
|
+
scanResult.errors.slice(0, 5).forEach(e => console.log(` - ${e}`));
|
|
167
|
+
if (scanResult.errors.length > 5) {
|
|
168
|
+
console.log(` ... and ${scanResult.errors.length - 5} more`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
console.log(`\n Found ${scanResult.totalElements} API elements in ${scanResult.files.length} files`);
|
|
172
|
+
if (scanResult.totalElements === 0) {
|
|
173
|
+
console.log(' No API elements found. Nothing to generate.');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
// Collect all elements
|
|
177
|
+
let allElements = [];
|
|
178
|
+
for (const file of scanResult.files) {
|
|
179
|
+
for (const el of file.elements) {
|
|
180
|
+
allElements.push(el);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Apply privacy filters
|
|
184
|
+
const initialCount = allElements.length;
|
|
185
|
+
// 1. --public-only: filter to exported/public APIs only
|
|
186
|
+
if (options.publicOnly) {
|
|
187
|
+
allElements = allElements.filter(el => el.isExported === true || el.isPublic === true);
|
|
188
|
+
console.log(` --public-only: filtered to ${allElements.length} exported APIs`);
|
|
189
|
+
}
|
|
190
|
+
// 2. Load .skryptignore patterns
|
|
191
|
+
const ignorePatterns = readIgnorePatterns(sourcePath);
|
|
192
|
+
if (ignorePatterns.length > 0) {
|
|
193
|
+
console.log(` .skryptignore: loaded ${ignorePatterns.length} patterns`);
|
|
194
|
+
}
|
|
195
|
+
// 3. Combine with --exclude patterns
|
|
196
|
+
const excludePatterns = [...ignorePatterns, ...(options.exclude || [])];
|
|
197
|
+
if (excludePatterns.length > 0) {
|
|
198
|
+
allElements = allElements.filter(el => !shouldExcludeElement(el, excludePatterns));
|
|
199
|
+
if (options.exclude?.length) {
|
|
200
|
+
console.log(` --exclude: applied ${options.exclude.length} additional patterns`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (initialCount !== allElements.length) {
|
|
204
|
+
console.log(` Filtered: ${initialCount} -> ${allElements.length} elements`);
|
|
205
|
+
}
|
|
206
|
+
// Show summary by kind
|
|
207
|
+
const byKind = {};
|
|
208
|
+
for (const el of allElements) {
|
|
209
|
+
byKind[el.kind] = (byKind[el.kind] || 0) + 1;
|
|
210
|
+
}
|
|
211
|
+
const pluralize = (word, count) => {
|
|
212
|
+
if (count === 1)
|
|
213
|
+
return word;
|
|
214
|
+
if (word === 'class')
|
|
215
|
+
return 'classes';
|
|
216
|
+
return word + 's';
|
|
217
|
+
};
|
|
218
|
+
console.log(' ' + Object.entries(byKind).map(([k, v]) => `${v} ${pluralize(k, v)}`).join(', '));
|
|
219
|
+
// Dry run - stop here
|
|
220
|
+
if (options.dryRun) {
|
|
221
|
+
console.log('\n[dry run - stopping before generation]');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
// Step 2: Generate docs
|
|
225
|
+
console.log('\nStep 2: Generating documentation...');
|
|
226
|
+
const client = createLLMClient({
|
|
227
|
+
provider: config.llm.provider,
|
|
228
|
+
model: config.llm.model,
|
|
229
|
+
baseUrl: config.llm.baseUrl
|
|
230
|
+
});
|
|
231
|
+
let lastElement = '';
|
|
232
|
+
const multiLanguage = options.multiLang ?? false;
|
|
233
|
+
if (multiLanguage) {
|
|
234
|
+
console.log(' mode: multi-language (TypeScript + Python)');
|
|
235
|
+
}
|
|
236
|
+
const docs = await generateForElements(allElements, client, {
|
|
237
|
+
multiLanguage,
|
|
238
|
+
onProgress: (progress) => {
|
|
239
|
+
if (progress.element !== lastElement) {
|
|
240
|
+
if (lastElement)
|
|
241
|
+
console.log('');
|
|
242
|
+
lastElement = progress.element;
|
|
243
|
+
}
|
|
244
|
+
process.stdout.write(`\r [${progress.current}/${progress.total}] ${progress.element}: ${progress.status}`.padEnd(80));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
console.log('\n');
|
|
248
|
+
// Step 3: Write output
|
|
249
|
+
console.log('Step 3: Writing documentation...');
|
|
250
|
+
const outputPath = resolve(config.output.path);
|
|
251
|
+
let filesWritten;
|
|
252
|
+
let totalDocs;
|
|
253
|
+
if (options.byTopic) {
|
|
254
|
+
console.log(' mode: by-topic (grouped by concept)');
|
|
255
|
+
const result = await writeDocsByTopic(docs, outputPath);
|
|
256
|
+
filesWritten = result.filesWritten;
|
|
257
|
+
totalDocs = result.totalDocs;
|
|
258
|
+
console.log(` topics: ${result.topics.map(t => t.name).join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// Default: file-based output
|
|
262
|
+
const fileResults = groupDocsByFile(docs);
|
|
263
|
+
const result = await writeDocsToDirectory(fileResults, outputPath, sourcePath);
|
|
264
|
+
filesWritten = result.filesWritten;
|
|
265
|
+
totalDocs = result.totalDocs;
|
|
266
|
+
}
|
|
267
|
+
const errorCount = docs.filter(d => d.error).length;
|
|
268
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
269
|
+
console.log(`\n Wrote ${filesWritten} documentation files to ${outputPath}`);
|
|
270
|
+
// Copy OpenAPI spec (provided or auto-detected)
|
|
271
|
+
let specPath = options.openapi ? resolve(options.openapi) : null;
|
|
272
|
+
// Auto-detect if not provided
|
|
273
|
+
if (!specPath) {
|
|
274
|
+
const detected = findOpenAPISpec(sourcePath);
|
|
275
|
+
if (detected) {
|
|
276
|
+
specPath = detected;
|
|
277
|
+
console.log(`\n Auto-detected OpenAPI spec: ${basename(detected)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (specPath) {
|
|
281
|
+
if (existsSync(specPath)) {
|
|
282
|
+
const specFilename = basename(specPath);
|
|
283
|
+
const contentDir = dirname(outputPath);
|
|
284
|
+
const destPath = resolve(contentDir, specFilename);
|
|
285
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
286
|
+
copyFileSync(specPath, destPath);
|
|
287
|
+
console.log(` Copied OpenAPI spec: ${specFilename} -> ${destPath}`);
|
|
288
|
+
console.log(' API Playground will be available at /reference');
|
|
289
|
+
}
|
|
290
|
+
else if (options.openapi) {
|
|
291
|
+
console.log(`\n Warning: OpenAPI spec not found: ${specPath}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Generate llms.txt for AEO (Answer Engine Optimization)
|
|
295
|
+
if (options.llmsTxt) {
|
|
296
|
+
await writeLlmsTxt(docs, outputPath, {
|
|
297
|
+
projectName: options.projectName,
|
|
298
|
+
description: `API documentation for ${options.projectName || basename(sourcePath)}`
|
|
299
|
+
});
|
|
300
|
+
console.log(`\n Generated llms.txt and llms-full.md for AEO`);
|
|
301
|
+
}
|
|
302
|
+
console.log('\n=== Summary ===');
|
|
303
|
+
console.log(` Total elements: ${totalDocs}`);
|
|
304
|
+
console.log(` Generated: ${totalDocs - errorCount}`);
|
|
305
|
+
if (errorCount > 0) {
|
|
306
|
+
console.log(` Errors: ${errorCount}`);
|
|
307
|
+
}
|
|
308
|
+
console.log(` Duration: ${duration}s`);
|
|
309
|
+
console.log(` Output: ${outputPath}`);
|
|
310
|
+
if (errorCount > 0) {
|
|
311
|
+
console.log('\n Elements with errors:');
|
|
312
|
+
docs.filter(d => d.error).slice(0, 10).forEach(d => {
|
|
313
|
+
console.log(` - ${d.element.name}: ${d.error?.slice(0, 50)}`);
|
|
314
|
+
});
|
|
315
|
+
if (errorCount > 10) {
|
|
316
|
+
console.log(` ... and ${errorCount - 10} more`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
console.log('\nDone!');
|
|
320
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
export const initCommand = new Command('init')
|
|
8
|
+
.description('Initialize a new documentation site')
|
|
9
|
+
.argument('[directory]', 'Target directory', '.')
|
|
10
|
+
.option('--name <name>', 'Project name', 'my-docs')
|
|
11
|
+
.action(async (directory, options) => {
|
|
12
|
+
const targetDir = join(process.cwd(), directory);
|
|
13
|
+
console.log('skrypt init');
|
|
14
|
+
console.log(` directory: ${targetDir}`);
|
|
15
|
+
console.log(` name: ${options.name}`);
|
|
16
|
+
console.log('');
|
|
17
|
+
// Check if directory exists and is not empty
|
|
18
|
+
if (existsSync(targetDir)) {
|
|
19
|
+
const files = readdirSync(targetDir);
|
|
20
|
+
if (files.length > 0 && !files.every((f) => f.startsWith('.'))) {
|
|
21
|
+
console.error('Error: Target directory is not empty');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Create target directory
|
|
26
|
+
mkdirSync(targetDir, { recursive: true });
|
|
27
|
+
// Copy template
|
|
28
|
+
const templateDir = join(__dirname, '..', '..', 'template');
|
|
29
|
+
if (!existsSync(templateDir)) {
|
|
30
|
+
console.error('Error: Template not found. Please reinstall skrypt.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log('Creating documentation site...');
|
|
34
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
35
|
+
// Update package.json with project name
|
|
36
|
+
const packageJsonPath = join(targetDir, 'package.json');
|
|
37
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
38
|
+
packageJson.name = options.name;
|
|
39
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
40
|
+
// Update docs.json with project name
|
|
41
|
+
const docsJsonPath = join(targetDir, 'docs.json');
|
|
42
|
+
const docsJson = JSON.parse(readFileSync(docsJsonPath, 'utf-8'));
|
|
43
|
+
docsJson.name = options.name;
|
|
44
|
+
writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2));
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('Done! Next steps:');
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(` cd ${directory === '.' ? '.' : directory}`);
|
|
49
|
+
console.log(' npm install');
|
|
50
|
+
console.log(' npm run dev');
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('Then open http://localhost:3000 to see your docs.');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log('To generate API documentation:');
|
|
55
|
+
console.log(' skrypt generate ./src -o ./content/docs');
|
|
56
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { analyzePRForDocs, postPRComment, postInlineComments } from '../github/pr-comments.js';
|
|
3
|
+
export const reviewPRCommand = new Command('review-pr')
|
|
4
|
+
.description('Review a GitHub PR for documentation issues')
|
|
5
|
+
.argument('<pr-url>', 'GitHub PR URL (e.g., https://github.com/owner/repo/pull/123)')
|
|
6
|
+
.option('--inline', 'Post inline comments instead of a single comment')
|
|
7
|
+
.option('--dry-run', 'Show issues without posting comments')
|
|
8
|
+
.option('--token <token>', 'GitHub token (or use GITHUB_TOKEN env var)')
|
|
9
|
+
.action(async (prUrl, options) => {
|
|
10
|
+
// Parse PR URL
|
|
11
|
+
const urlMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
12
|
+
if (!urlMatch) {
|
|
13
|
+
console.error('Error: Invalid PR URL format');
|
|
14
|
+
console.error('Expected: https://github.com/owner/repo/pull/123');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const owner = urlMatch[1];
|
|
18
|
+
const repo = urlMatch[2];
|
|
19
|
+
const pullNumber = urlMatch[3];
|
|
20
|
+
if (!owner || !repo || !pullNumber) {
|
|
21
|
+
console.error('Error: Could not parse owner/repo/PR number from URL');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
console.log('skrypt review-pr');
|
|
25
|
+
console.log(` repo: ${owner}/${repo}`);
|
|
26
|
+
console.log(` PR: #${pullNumber}`);
|
|
27
|
+
console.log('');
|
|
28
|
+
const token = options.token || process.env.GITHUB_TOKEN;
|
|
29
|
+
if (!token) {
|
|
30
|
+
console.error('Error: GITHUB_TOKEN environment variable or --token required');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Analyze the PR
|
|
34
|
+
console.log('Analyzing PR for documentation issues...');
|
|
35
|
+
const config = {
|
|
36
|
+
owner,
|
|
37
|
+
repo,
|
|
38
|
+
pullNumber: parseInt(pullNumber),
|
|
39
|
+
token,
|
|
40
|
+
};
|
|
41
|
+
let issues;
|
|
42
|
+
try {
|
|
43
|
+
issues = await analyzePRForDocs(config);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
console.error(`Error analyzing PR: ${message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (issues.length === 0) {
|
|
51
|
+
console.log('\n✓ No documentation issues found!');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
console.log(`\nFound ${issues.length} issue(s):`);
|
|
55
|
+
console.log('');
|
|
56
|
+
// Group by severity
|
|
57
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
58
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
59
|
+
const infos = issues.filter(i => i.severity === 'info');
|
|
60
|
+
if (errors.length > 0) {
|
|
61
|
+
console.log(`❌ Errors (${errors.length}):`);
|
|
62
|
+
for (const issue of errors) {
|
|
63
|
+
console.log(` ${issue.file}: ${issue.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (warnings.length > 0) {
|
|
67
|
+
console.log(`⚠️ Warnings (${warnings.length}):`);
|
|
68
|
+
for (const issue of warnings) {
|
|
69
|
+
console.log(` ${issue.file}: ${issue.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (infos.length > 0) {
|
|
73
|
+
console.log(`ℹ️ Suggestions (${infos.length}):`);
|
|
74
|
+
for (const issue of infos) {
|
|
75
|
+
console.log(` ${issue.file}: ${issue.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (options.dryRun) {
|
|
79
|
+
console.log('\n[dry run - no comments posted]');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
// Post comments
|
|
83
|
+
console.log('\nPosting comments to PR...');
|
|
84
|
+
if (options.inline) {
|
|
85
|
+
const inlineIssues = issues.filter(i => i.line);
|
|
86
|
+
const generalIssues = issues.filter(i => !i.line);
|
|
87
|
+
if (inlineIssues.length > 0) {
|
|
88
|
+
const inlineResults = await postInlineComments(config, inlineIssues);
|
|
89
|
+
const success = inlineResults.filter(r => r.success).length;
|
|
90
|
+
console.log(` Posted ${success}/${inlineIssues.length} inline comments`);
|
|
91
|
+
}
|
|
92
|
+
if (generalIssues.length > 0) {
|
|
93
|
+
const result = await postPRComment(config, generalIssues);
|
|
94
|
+
if (result.success) {
|
|
95
|
+
console.log(` Posted summary comment`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error(` Failed to post summary: ${result.error}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const result = await postPRComment(config, issues);
|
|
104
|
+
if (result.success) {
|
|
105
|
+
console.log(`✓ Comment posted successfully`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`✗ Failed to post comment: ${result.error}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log('\nDone!');
|
|
113
|
+
// Exit with error code if there are errors
|
|
114
|
+
if (errors.length > 0) {
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { watch } from 'chokidar';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { loadConfig, validateConfig, checkApiKey } from '../config/index.js';
|
|
6
|
+
import { DEFAULT_MODELS } from '../config/types.js';
|
|
7
|
+
import { scanDirectory } from '../scanner/index.js';
|
|
8
|
+
import { createLLMClient } from '../llm/index.js';
|
|
9
|
+
import { generateForElements, groupDocsByFile, writeDocsToDirectory } from '../generator/index.js';
|
|
10
|
+
export const watchCommand = new Command('watch')
|
|
11
|
+
.description('Watch source files and regenerate docs on changes')
|
|
12
|
+
.argument('<source>', 'Source directory to watch')
|
|
13
|
+
.option('-o, --output <dir>', 'Output directory')
|
|
14
|
+
.option('-c, --config <file>', 'Config file path')
|
|
15
|
+
.option('--provider <name>', 'LLM provider')
|
|
16
|
+
.option('--model <name>', 'LLM model name')
|
|
17
|
+
.option('--debounce <ms>', 'Debounce time in milliseconds', '1000')
|
|
18
|
+
.action(async (source, options) => {
|
|
19
|
+
const config = loadConfig(options.config);
|
|
20
|
+
if (source)
|
|
21
|
+
config.source.path = source;
|
|
22
|
+
if (options.output)
|
|
23
|
+
config.output.path = options.output;
|
|
24
|
+
if (options.provider) {
|
|
25
|
+
config.llm.provider = options.provider;
|
|
26
|
+
if (!options.model) {
|
|
27
|
+
config.llm.model = DEFAULT_MODELS[config.llm.provider];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (options.model)
|
|
31
|
+
config.llm.model = options.model;
|
|
32
|
+
const errors = validateConfig(config);
|
|
33
|
+
if (errors.length > 0) {
|
|
34
|
+
console.error('Config errors:', errors);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const { ok, envKey } = checkApiKey(config.llm.provider);
|
|
38
|
+
if (!ok && envKey) {
|
|
39
|
+
console.error(`Error: ${envKey} required`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const sourcePath = resolve(config.source.path);
|
|
43
|
+
const outputPath = resolve(config.output.path);
|
|
44
|
+
if (!existsSync(sourcePath)) {
|
|
45
|
+
console.error(`Source not found: ${sourcePath}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.log('skrypt watch');
|
|
49
|
+
console.log(` source: ${sourcePath}`);
|
|
50
|
+
console.log(` output: ${outputPath}`);
|
|
51
|
+
console.log(` provider: ${config.llm.provider}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
const client = createLLMClient({
|
|
54
|
+
provider: config.llm.provider,
|
|
55
|
+
model: config.llm.model,
|
|
56
|
+
baseUrl: config.llm.baseUrl
|
|
57
|
+
});
|
|
58
|
+
let generating = false;
|
|
59
|
+
let pendingGenerate = false;
|
|
60
|
+
const debounceMs = parseInt(options.debounce);
|
|
61
|
+
async function generate() {
|
|
62
|
+
if (generating) {
|
|
63
|
+
pendingGenerate = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
generating = true;
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
try {
|
|
69
|
+
console.log('\n[' + new Date().toLocaleTimeString() + '] Regenerating docs...');
|
|
70
|
+
const scanResult = await scanDirectory(sourcePath, {
|
|
71
|
+
include: config.source.include,
|
|
72
|
+
exclude: config.source.exclude,
|
|
73
|
+
});
|
|
74
|
+
if (scanResult.totalElements === 0) {
|
|
75
|
+
console.log(' No elements found');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const allElements = [];
|
|
79
|
+
for (const file of scanResult.files) {
|
|
80
|
+
allElements.push(...file.elements);
|
|
81
|
+
}
|
|
82
|
+
const docs = await generateForElements(allElements, client, {
|
|
83
|
+
onProgress: (p) => {
|
|
84
|
+
process.stdout.write(`\r [${p.current}/${p.total}] ${p.element}`.padEnd(60));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const fileResults = groupDocsByFile(docs);
|
|
88
|
+
await writeDocsToDirectory(fileResults, outputPath, sourcePath);
|
|
89
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
90
|
+
console.log(`\n Done in ${duration}s`);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error('\n Error:', err);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
generating = false;
|
|
97
|
+
if (pendingGenerate) {
|
|
98
|
+
pendingGenerate = false;
|
|
99
|
+
setTimeout(generate, debounceMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Initial generation
|
|
104
|
+
await generate();
|
|
105
|
+
// Watch for changes
|
|
106
|
+
console.log('\nWatching for changes... (Ctrl+C to stop)');
|
|
107
|
+
let debounceTimer = null;
|
|
108
|
+
const watcher = watch(sourcePath, {
|
|
109
|
+
ignored: [
|
|
110
|
+
'**/node_modules/**',
|
|
111
|
+
'**/.git/**',
|
|
112
|
+
'**/dist/**',
|
|
113
|
+
'**/__pycache__/**',
|
|
114
|
+
],
|
|
115
|
+
persistent: true,
|
|
116
|
+
ignoreInitial: true,
|
|
117
|
+
});
|
|
118
|
+
watcher.on('change', (path) => {
|
|
119
|
+
console.log(` Changed: ${path}`);
|
|
120
|
+
if (debounceTimer)
|
|
121
|
+
clearTimeout(debounceTimer);
|
|
122
|
+
debounceTimer = setTimeout(generate, debounceMs);
|
|
123
|
+
});
|
|
124
|
+
watcher.on('add', (path) => {
|
|
125
|
+
console.log(` Added: ${path}`);
|
|
126
|
+
if (debounceTimer)
|
|
127
|
+
clearTimeout(debounceTimer);
|
|
128
|
+
debounceTimer = setTimeout(generate, debounceMs);
|
|
129
|
+
});
|
|
130
|
+
watcher.on('unlink', (path) => {
|
|
131
|
+
console.log(` Removed: ${path}`);
|
|
132
|
+
if (debounceTimer)
|
|
133
|
+
clearTimeout(debounceTimer);
|
|
134
|
+
debounceTimer = setTimeout(generate, debounceMs);
|
|
135
|
+
});
|
|
136
|
+
// Keep process alive
|
|
137
|
+
process.on('SIGINT', () => {
|
|
138
|
+
console.log('\nStopping watch...');
|
|
139
|
+
watcher.close();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Config, LLMProvider } from './types.js';
|
|
2
|
+
export declare function findConfigFile(dir: string): string | null;
|
|
3
|
+
export declare function loadConfig(configPath?: string): Config;
|
|
4
|
+
export declare function validateConfig(config: Config): string[];
|
|
5
|
+
export declare function getRequiredEnvKey(provider: LLMProvider): string | null;
|
|
6
|
+
export declare function checkApiKey(provider: LLMProvider): {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
envKey: string | null;
|
|
9
|
+
};
|