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 { mkdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join, basename, relative } from 'path';
|
|
3
|
+
import { formatAsMarkdown } from './generator.js';
|
|
4
|
+
import { organizeByTopic, detectCrossReferences, getCrossRefsForElement } from './organizer.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate llms.txt file (Answer Engine Optimization)
|
|
7
|
+
* Format follows https://llmstxt.org convention
|
|
8
|
+
*/
|
|
9
|
+
export async function writeLlmsTxt(docs, outputDir, options = {}) {
|
|
10
|
+
const projectName = options.projectName || 'API';
|
|
11
|
+
const description = options.description || 'API documentation generated by skrypt';
|
|
12
|
+
let content = `# ${projectName}\n\n`;
|
|
13
|
+
content += `> ${description}\n\n`;
|
|
14
|
+
// Group by file for organization
|
|
15
|
+
const byFile = new Map();
|
|
16
|
+
for (const doc of docs) {
|
|
17
|
+
const file = basename(doc.element.filePath).replace(/\.[^.]+$/, '');
|
|
18
|
+
if (!byFile.has(file)) {
|
|
19
|
+
byFile.set(file, []);
|
|
20
|
+
}
|
|
21
|
+
byFile.get(file).push(doc);
|
|
22
|
+
}
|
|
23
|
+
// Summary section
|
|
24
|
+
content += `## Overview\n\n`;
|
|
25
|
+
content += `This project contains ${docs.length} documented API elements across ${byFile.size} modules.\n\n`;
|
|
26
|
+
// Quick reference
|
|
27
|
+
content += `## Quick Reference\n\n`;
|
|
28
|
+
for (const [file, fileDocs] of byFile) {
|
|
29
|
+
content += `### ${file}\n\n`;
|
|
30
|
+
for (const doc of fileDocs) {
|
|
31
|
+
content += `- \`${doc.element.name}\`: ${doc.element.kind}`;
|
|
32
|
+
if (doc.markdown) {
|
|
33
|
+
// Extract first sentence as summary
|
|
34
|
+
const firstSentence = doc.markdown.split(/\.\s/)[0]?.slice(0, 100);
|
|
35
|
+
if (firstSentence) {
|
|
36
|
+
content += ` - ${firstSentence}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
content += '\n';
|
|
40
|
+
}
|
|
41
|
+
content += '\n';
|
|
42
|
+
}
|
|
43
|
+
// Detailed API section
|
|
44
|
+
content += `## API Details\n\n`;
|
|
45
|
+
for (const doc of docs) {
|
|
46
|
+
content += `### ${doc.element.name}\n\n`;
|
|
47
|
+
content += `**Type:** ${doc.element.kind}\n`;
|
|
48
|
+
content += `**File:** ${doc.element.filePath}\n\n`;
|
|
49
|
+
content += `\`\`\`\n${doc.element.signature}\n\`\`\`\n\n`;
|
|
50
|
+
if (doc.markdown) {
|
|
51
|
+
content += doc.markdown.slice(0, 500) + '\n\n';
|
|
52
|
+
}
|
|
53
|
+
if (doc.codeExample) {
|
|
54
|
+
content += `**Example:**\n\`\`\`${doc.codeLanguage}\n${doc.codeExample.slice(0, 300)}\n\`\`\`\n\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Write both llms.txt and llms-full.md
|
|
58
|
+
await writeFile(join(outputDir, 'llms.txt'), content, 'utf-8');
|
|
59
|
+
// Also write a condensed version (just signatures)
|
|
60
|
+
let condensed = `# ${projectName} API\n\n`;
|
|
61
|
+
condensed += `> ${description}\n\n`;
|
|
62
|
+
for (const doc of docs) {
|
|
63
|
+
condensed += `## ${doc.element.name}\n`;
|
|
64
|
+
condensed += `${doc.element.signature}\n\n`;
|
|
65
|
+
}
|
|
66
|
+
await writeFile(join(outputDir, 'llms-full.md'), condensed, 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Write generated docs to output directory
|
|
70
|
+
*/
|
|
71
|
+
export async function writeDocsToDirectory(results, outputDir, sourceDir) {
|
|
72
|
+
let filesWritten = 0;
|
|
73
|
+
let totalDocs = 0;
|
|
74
|
+
// Create output directory
|
|
75
|
+
await mkdir(outputDir, { recursive: true });
|
|
76
|
+
for (const result of results) {
|
|
77
|
+
if (result.docs.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
// Calculate relative path from source
|
|
80
|
+
const relPath = relative(sourceDir, result.filePath);
|
|
81
|
+
// Validate path traversal - prevent escaping output directory
|
|
82
|
+
if (relPath.startsWith('..') || relPath.startsWith('/')) {
|
|
83
|
+
console.warn(`Skipping file outside source directory: ${result.filePath}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const docFileName = relPath.replace(/\.[^.]+$/, '.md');
|
|
87
|
+
const outputPath = join(outputDir, docFileName);
|
|
88
|
+
// Create subdirectories if needed
|
|
89
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
90
|
+
// Generate markdown content
|
|
91
|
+
const title = basename(result.filePath, '.py')
|
|
92
|
+
.replace(/^./, c => c.toUpperCase())
|
|
93
|
+
.replace(/_/g, ' ');
|
|
94
|
+
const content = formatAsMarkdown(result.docs, title);
|
|
95
|
+
// Write file
|
|
96
|
+
await writeFile(outputPath, content, 'utf-8');
|
|
97
|
+
filesWritten++;
|
|
98
|
+
totalDocs += result.docs.length;
|
|
99
|
+
}
|
|
100
|
+
// Write index file
|
|
101
|
+
await writeIndexFile(results, outputDir, sourceDir);
|
|
102
|
+
return { filesWritten, totalDocs };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Write an index file linking to all generated docs
|
|
106
|
+
*/
|
|
107
|
+
async function writeIndexFile(results, outputDir, sourceDir) {
|
|
108
|
+
let content = '# API Documentation\n\n';
|
|
109
|
+
content += 'Generated by [skrypt](https://github.com/debgotwired/skrypt)\n\n';
|
|
110
|
+
// Summary
|
|
111
|
+
const totalElements = results.reduce((sum, r) => sum + r.docs.length, 0);
|
|
112
|
+
content += '## Summary\n\n';
|
|
113
|
+
content += `- **Total elements:** ${totalElements}\n\n`;
|
|
114
|
+
// File index
|
|
115
|
+
content += '## Files\n\n';
|
|
116
|
+
for (const result of results) {
|
|
117
|
+
if (result.docs.length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
const relPath = relative(sourceDir, result.filePath);
|
|
120
|
+
const docFileName = relPath.replace(/\.[^.]+$/, '.md');
|
|
121
|
+
content += `- [${relPath}](./${docFileName})`;
|
|
122
|
+
content += ` (${result.docs.length} elements)\n`;
|
|
123
|
+
}
|
|
124
|
+
await writeFile(join(outputDir, 'README.md'), content, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Group generated docs by file
|
|
128
|
+
*/
|
|
129
|
+
export function groupDocsByFile(docs) {
|
|
130
|
+
const byFile = new Map();
|
|
131
|
+
for (const doc of docs) {
|
|
132
|
+
const file = doc.element.filePath;
|
|
133
|
+
if (!byFile.has(file)) {
|
|
134
|
+
byFile.set(file, []);
|
|
135
|
+
}
|
|
136
|
+
byFile.get(file).push(doc);
|
|
137
|
+
}
|
|
138
|
+
return Array.from(byFile.entries()).map(([filePath, fileDocs]) => ({
|
|
139
|
+
filePath,
|
|
140
|
+
docs: fileDocs
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
// Topic-organized output
|
|
144
|
+
/**
|
|
145
|
+
* Write docs organized by topic
|
|
146
|
+
*/
|
|
147
|
+
export async function writeDocsByTopic(docs, outputDir) {
|
|
148
|
+
let filesWritten = 0;
|
|
149
|
+
// Organize docs by topic
|
|
150
|
+
const topics = organizeByTopic(docs);
|
|
151
|
+
const crossRefs = detectCrossReferences(docs);
|
|
152
|
+
// Create output directory
|
|
153
|
+
await mkdir(outputDir, { recursive: true });
|
|
154
|
+
// Write each topic as a separate file
|
|
155
|
+
for (const topic of topics) {
|
|
156
|
+
const topicPath = join(outputDir, `${topic.id}.md`);
|
|
157
|
+
const content = formatTopicMarkdown(topic, crossRefs);
|
|
158
|
+
await writeFile(topicPath, content, 'utf-8');
|
|
159
|
+
filesWritten++;
|
|
160
|
+
}
|
|
161
|
+
// Write navigation index
|
|
162
|
+
await writeTopicIndexFile(topics, outputDir);
|
|
163
|
+
// Write sidebar config
|
|
164
|
+
await writeSidebarConfig(topics, outputDir);
|
|
165
|
+
return { filesWritten, totalDocs: docs.length, topics };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Format a topic as markdown with cross-references
|
|
169
|
+
*/
|
|
170
|
+
function formatTopicMarkdown(topic, allCrossRefs) {
|
|
171
|
+
// MDX frontmatter (standard across doc platforms)
|
|
172
|
+
let content = `---
|
|
173
|
+
title: "${topic.name}"
|
|
174
|
+
description: "${topic.description || `API reference for ${topic.name}`}"
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
`;
|
|
178
|
+
// Table of contents
|
|
179
|
+
content += '<CardGroup cols={2}>\n';
|
|
180
|
+
for (const doc of topic.docs) {
|
|
181
|
+
const anchor = slugify(doc.element.name);
|
|
182
|
+
const icon = doc.element.kind === 'class' ? 'cube' : doc.element.kind === 'method' ? 'code' : 'function';
|
|
183
|
+
content += ` <Card title="${doc.element.name}" icon="${icon}" href="#${anchor}">\n`;
|
|
184
|
+
content += ` ${doc.element.kind}\n`;
|
|
185
|
+
content += ` </Card>\n`;
|
|
186
|
+
}
|
|
187
|
+
content += '</CardGroup>\n\n';
|
|
188
|
+
// Each element
|
|
189
|
+
for (const doc of topic.docs) {
|
|
190
|
+
content += formatElementWithCrossRefs(doc, allCrossRefs);
|
|
191
|
+
}
|
|
192
|
+
return content;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Format a single element with cross-references
|
|
196
|
+
*/
|
|
197
|
+
function formatElementWithCrossRefs(doc, allCrossRefs) {
|
|
198
|
+
const { element } = doc;
|
|
199
|
+
const anchor = slugify(element.name);
|
|
200
|
+
let section = `<a id="${anchor}"></a>\n\n`;
|
|
201
|
+
section += `## \`${element.name}\`\n\n`;
|
|
202
|
+
section += `\`\`\`${doc.codeLanguage}\n${element.signature}\n\`\`\`\n\n`;
|
|
203
|
+
if (doc.markdown) {
|
|
204
|
+
section += doc.markdown + '\n\n';
|
|
205
|
+
}
|
|
206
|
+
// Multi-language examples (MDX CodeGroup)
|
|
207
|
+
const hasMultiLang = doc.typescriptExample && doc.pythonExample;
|
|
208
|
+
if (hasMultiLang) {
|
|
209
|
+
section += '### Examples\n\n';
|
|
210
|
+
section += '<CodeGroup>\n\n';
|
|
211
|
+
section += `\`\`\`typescript example.ts\n${doc.typescriptExample}\n\`\`\`\n\n`;
|
|
212
|
+
section += `\`\`\`python example.py\n${doc.pythonExample}\n\`\`\`\n\n`;
|
|
213
|
+
section += '</CodeGroup>\n\n';
|
|
214
|
+
}
|
|
215
|
+
else if (doc.codeExample) {
|
|
216
|
+
section += '### Example\n\n';
|
|
217
|
+
section += `\`\`\`${doc.codeLanguage} example.${doc.codeLanguage === 'typescript' ? 'ts' : doc.codeLanguage === 'python' ? 'py' : 'js'}\n${doc.codeExample}\n\`\`\`\n\n`;
|
|
218
|
+
}
|
|
219
|
+
// Cross-references
|
|
220
|
+
const refs = getCrossRefsForElement(element.name, allCrossRefs);
|
|
221
|
+
if (refs.length > 0) {
|
|
222
|
+
section += '### Related\n\n';
|
|
223
|
+
section += '<CardGroup cols={3}>\n';
|
|
224
|
+
const uses = refs.filter(r => r.relationship === 'uses');
|
|
225
|
+
const usedBy = refs.filter(r => r.relationship === 'used-by');
|
|
226
|
+
const related = refs.filter(r => r.relationship === 'related' || r.relationship === 'see-also');
|
|
227
|
+
for (const ref of [...uses, ...usedBy, ...related].slice(0, 6)) {
|
|
228
|
+
const label = ref.relationship === 'uses' ? 'Uses' : ref.relationship === 'used-by' ? 'Used by' : 'Related';
|
|
229
|
+
section += ` <Card title="${ref.toElement}" icon="link" href="#${slugify(ref.toElement)}">\n`;
|
|
230
|
+
section += ` ${label}\n`;
|
|
231
|
+
section += ` </Card>\n`;
|
|
232
|
+
}
|
|
233
|
+
section += '</CardGroup>\n\n';
|
|
234
|
+
}
|
|
235
|
+
section += '---\n\n';
|
|
236
|
+
return section;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Write topic-based index file
|
|
240
|
+
*/
|
|
241
|
+
async function writeTopicIndexFile(topics, outputDir) {
|
|
242
|
+
// Summary stats
|
|
243
|
+
const totalElements = topics.reduce((sum, t) => sum + t.docs.length, 0);
|
|
244
|
+
// MDX frontmatter
|
|
245
|
+
let content = `---
|
|
246
|
+
title: "API Reference"
|
|
247
|
+
description: "Complete API documentation with code examples"
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
`;
|
|
251
|
+
content += `<Info>\n`;
|
|
252
|
+
content += ` **${totalElements}** documented elements\n`;
|
|
253
|
+
content += `</Info>\n\n`;
|
|
254
|
+
// Topics as CardGroup
|
|
255
|
+
content += '<CardGroup cols={2}>\n';
|
|
256
|
+
for (const topic of topics) {
|
|
257
|
+
const icon = topic.id === 'memory' ? 'database' : topic.id === 'tools' ? 'wrench' : topic.id === 'core' ? 'cube' : 'folder';
|
|
258
|
+
content += ` <Card title="${topic.name}" icon="${icon}" href="${topic.id}">\n`;
|
|
259
|
+
content += ` ${topic.description || ''} (${topic.docs.length} items)\n`;
|
|
260
|
+
content += ` </Card>\n`;
|
|
261
|
+
}
|
|
262
|
+
content += '</CardGroup>\n\n';
|
|
263
|
+
// Quick reference table per topic
|
|
264
|
+
for (const topic of topics) {
|
|
265
|
+
content += `## ${topic.name}\n\n`;
|
|
266
|
+
if (topic.description) {
|
|
267
|
+
content += `${topic.description}\n\n`;
|
|
268
|
+
}
|
|
269
|
+
content += '| Element | Type |\n';
|
|
270
|
+
content += '|---------|------|\n';
|
|
271
|
+
for (const doc of topic.docs.slice(0, 5)) {
|
|
272
|
+
content += `| [\`${doc.element.name}\`](${topic.id}#${slugify(doc.element.name)}) | ${doc.element.kind} |\n`;
|
|
273
|
+
}
|
|
274
|
+
if (topic.docs.length > 5) {
|
|
275
|
+
content += `| *...and ${topic.docs.length - 5} more* | |\n`;
|
|
276
|
+
}
|
|
277
|
+
content += '\n';
|
|
278
|
+
}
|
|
279
|
+
await writeFile(join(outputDir, 'index.mdx'), content, 'utf-8');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Write sidebar configuration for doc platforms
|
|
283
|
+
*/
|
|
284
|
+
async function writeSidebarConfig(topics, outputDir) {
|
|
285
|
+
// Navigation config (compatible with multiple doc platforms)
|
|
286
|
+
const navGroups = topics.map(topic => ({
|
|
287
|
+
group: topic.name,
|
|
288
|
+
pages: [`api-reference/${topic.id}`]
|
|
289
|
+
}));
|
|
290
|
+
const navConfig = {
|
|
291
|
+
navigation: [
|
|
292
|
+
{ group: 'API Reference', pages: navGroups.flatMap(g => g.pages) }
|
|
293
|
+
],
|
|
294
|
+
// Full navigation with nested pages
|
|
295
|
+
groups: navGroups
|
|
296
|
+
};
|
|
297
|
+
await writeFile(join(outputDir, '_navigation.json'), JSON.stringify(navConfig, null, 2), 'utf-8');
|
|
298
|
+
// Sidebar config (Docusaurus-compatible)
|
|
299
|
+
const docusaurusSidebar = {
|
|
300
|
+
apiSidebar: topics.map(topic => ({
|
|
301
|
+
type: 'category',
|
|
302
|
+
label: topic.name,
|
|
303
|
+
items: topic.docs.map(doc => ({
|
|
304
|
+
type: 'doc',
|
|
305
|
+
id: `${topic.id}/${slugify(doc.element.name)}`,
|
|
306
|
+
label: doc.element.name
|
|
307
|
+
}))
|
|
308
|
+
}))
|
|
309
|
+
};
|
|
310
|
+
await writeFile(join(outputDir, '_sidebars.json'), JSON.stringify(docusaurusSidebar, null, 2), 'utf-8');
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Convert string to URL-safe slug
|
|
314
|
+
*/
|
|
315
|
+
function slugify(str) {
|
|
316
|
+
return str
|
|
317
|
+
.toLowerCase()
|
|
318
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
319
|
+
.replace(/^-|-$/g, '');
|
|
320
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub PR Comments Integration
|
|
3
|
+
*
|
|
4
|
+
* Posts documentation suggestions and issues as PR comments.
|
|
5
|
+
* Requires: GITHUB_TOKEN environment variable
|
|
6
|
+
*/
|
|
7
|
+
export interface PRCommentConfig {
|
|
8
|
+
owner: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
pullNumber: number;
|
|
11
|
+
token?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DocumentationIssue {
|
|
14
|
+
type: 'missing' | 'outdated' | 'broken_example' | 'suggestion';
|
|
15
|
+
severity: 'error' | 'warning' | 'info';
|
|
16
|
+
file: string;
|
|
17
|
+
line?: number;
|
|
18
|
+
message: string;
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CommentResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
commentId?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Post a review comment on a PR
|
|
29
|
+
*/
|
|
30
|
+
export declare function postPRComment(config: PRCommentConfig, issues: DocumentationIssue[]): Promise<CommentResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Post inline review comments on specific lines
|
|
33
|
+
*/
|
|
34
|
+
export declare function postInlineComments(config: PRCommentConfig, issues: DocumentationIssue[]): Promise<CommentResult[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Analyze a PR for documentation issues
|
|
37
|
+
*/
|
|
38
|
+
export declare function analyzePRForDocs(config: PRCommentConfig, _options?: {
|
|
39
|
+
checkExamples?: boolean;
|
|
40
|
+
}): Promise<DocumentationIssue[]>;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub PR Comments Integration
|
|
3
|
+
*
|
|
4
|
+
* Posts documentation suggestions and issues as PR comments.
|
|
5
|
+
* Requires: GITHUB_TOKEN environment variable
|
|
6
|
+
*/
|
|
7
|
+
const GITHUB_API = 'https://api.github.com';
|
|
8
|
+
/**
|
|
9
|
+
* Post a review comment on a PR
|
|
10
|
+
*/
|
|
11
|
+
export async function postPRComment(config, issues) {
|
|
12
|
+
const token = config.token || process.env.GITHUB_TOKEN;
|
|
13
|
+
if (!token) {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: 'GITHUB_TOKEN environment variable required',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (issues.length === 0) {
|
|
20
|
+
return { success: true };
|
|
21
|
+
}
|
|
22
|
+
// Build comment body
|
|
23
|
+
const body = buildCommentBody(issues);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/issues/${config.pullNumber}/comments`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
Accept: 'application/vnd.github.v3+json',
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'User-Agent': 'skrypt-cli',
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ body }),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const error = await response.text();
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: `GitHub API error: ${response.status} ${error}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
commentId: data.id,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: message,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Post inline review comments on specific lines
|
|
58
|
+
*/
|
|
59
|
+
export async function postInlineComments(config, issues) {
|
|
60
|
+
const token = config.token || process.env.GITHUB_TOKEN;
|
|
61
|
+
if (!token) {
|
|
62
|
+
return [{
|
|
63
|
+
success: false,
|
|
64
|
+
error: 'GITHUB_TOKEN environment variable required',
|
|
65
|
+
}];
|
|
66
|
+
}
|
|
67
|
+
// First, get the PR diff to find commit SHA
|
|
68
|
+
const prResponse = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}`, {
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
Accept: 'application/vnd.github.v3+json',
|
|
72
|
+
'User-Agent': 'skrypt-cli',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (!prResponse.ok) {
|
|
76
|
+
return [{
|
|
77
|
+
success: false,
|
|
78
|
+
error: 'Could not fetch PR details',
|
|
79
|
+
}];
|
|
80
|
+
}
|
|
81
|
+
const pr = await prResponse.json();
|
|
82
|
+
const commitId = pr.head.sha;
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const issue of issues) {
|
|
85
|
+
if (!issue.line)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}/comments`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${token}`,
|
|
92
|
+
Accept: 'application/vnd.github.v3+json',
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'User-Agent': 'skrypt-cli',
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
body: formatInlineComment(issue),
|
|
98
|
+
commit_id: commitId,
|
|
99
|
+
path: issue.file,
|
|
100
|
+
line: issue.line,
|
|
101
|
+
side: 'RIGHT',
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
if (response.ok) {
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
results.push({ success: true, commentId: data.id });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const error = await response.text();
|
|
110
|
+
results.push({
|
|
111
|
+
success: false,
|
|
112
|
+
error: `Line ${issue.line}: ${error}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
results.push({
|
|
118
|
+
success: false,
|
|
119
|
+
error: err instanceof Error ? err.message : String(err),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the main PR comment body
|
|
127
|
+
*/
|
|
128
|
+
function buildCommentBody(issues) {
|
|
129
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
130
|
+
const warningCount = issues.filter(i => i.severity === 'warning').length;
|
|
131
|
+
const infoCount = issues.filter(i => i.severity === 'info').length;
|
|
132
|
+
let body = `## 📚 Skrypt Review\n\n`;
|
|
133
|
+
if (errorCount > 0) {
|
|
134
|
+
body += `❌ **${errorCount} error${errorCount > 1 ? 's' : ''}**`;
|
|
135
|
+
}
|
|
136
|
+
if (warningCount > 0) {
|
|
137
|
+
body += ` ⚠️ **${warningCount} warning${warningCount > 1 ? 's' : ''}**`;
|
|
138
|
+
}
|
|
139
|
+
if (infoCount > 0) {
|
|
140
|
+
body += ` ℹ️ **${infoCount} suggestion${infoCount > 1 ? 's' : ''}**`;
|
|
141
|
+
}
|
|
142
|
+
body += '\n\n---\n\n';
|
|
143
|
+
// Group by type
|
|
144
|
+
const byType = new Map();
|
|
145
|
+
for (const issue of issues) {
|
|
146
|
+
const existing = byType.get(issue.type) || [];
|
|
147
|
+
existing.push(issue);
|
|
148
|
+
byType.set(issue.type, existing);
|
|
149
|
+
}
|
|
150
|
+
const typeLabels = {
|
|
151
|
+
missing: '📝 Missing Documentation',
|
|
152
|
+
outdated: '🔄 Outdated Documentation',
|
|
153
|
+
broken_example: '🐛 Broken Code Examples',
|
|
154
|
+
suggestion: '💡 Suggestions',
|
|
155
|
+
};
|
|
156
|
+
for (const [type, typeIssues] of byType) {
|
|
157
|
+
body += `### ${typeLabels[type] || type}\n\n`;
|
|
158
|
+
for (const issue of typeIssues) {
|
|
159
|
+
const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
160
|
+
body += `${icon} **${issue.file}**`;
|
|
161
|
+
if (issue.line) {
|
|
162
|
+
body += ` (line ${issue.line})`;
|
|
163
|
+
}
|
|
164
|
+
body += `\n`;
|
|
165
|
+
body += `> ${issue.message}\n`;
|
|
166
|
+
if (issue.suggestion) {
|
|
167
|
+
body += `\n**Suggestion:** ${issue.suggestion}\n`;
|
|
168
|
+
}
|
|
169
|
+
if (issue.code) {
|
|
170
|
+
body += `\n\`\`\`diff\n${issue.code}\n\`\`\`\n`;
|
|
171
|
+
}
|
|
172
|
+
body += '\n';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
body += '\n---\n';
|
|
176
|
+
body += '*Generated by [skrypt](https://skrypt.sh)*';
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Format an inline review comment
|
|
181
|
+
*/
|
|
182
|
+
function formatInlineComment(issue) {
|
|
183
|
+
const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : '💡';
|
|
184
|
+
let comment = `${icon} **${issue.type.replace('_', ' ')}:** ${issue.message}`;
|
|
185
|
+
if (issue.suggestion) {
|
|
186
|
+
comment += `\n\n**Suggestion:** ${issue.suggestion}`;
|
|
187
|
+
}
|
|
188
|
+
if (issue.code) {
|
|
189
|
+
comment += `\n\n\`\`\`suggestion\n${issue.code}\n\`\`\``;
|
|
190
|
+
}
|
|
191
|
+
return comment;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Analyze a PR for documentation issues
|
|
195
|
+
*/
|
|
196
|
+
export async function analyzePRForDocs(config, _options = {}) {
|
|
197
|
+
const token = config.token || process.env.GITHUB_TOKEN;
|
|
198
|
+
if (!token) {
|
|
199
|
+
throw new Error('GITHUB_TOKEN required');
|
|
200
|
+
}
|
|
201
|
+
// Get PR files
|
|
202
|
+
const response = await fetch(`${GITHUB_API}/repos/${config.owner}/${config.repo}/pulls/${config.pullNumber}/files`, {
|
|
203
|
+
headers: {
|
|
204
|
+
Authorization: `Bearer ${token}`,
|
|
205
|
+
Accept: 'application/vnd.github.v3+json',
|
|
206
|
+
'User-Agent': 'skrypt-cli',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new Error('Could not fetch PR files');
|
|
211
|
+
}
|
|
212
|
+
const files = await response.json();
|
|
213
|
+
const issues = [];
|
|
214
|
+
// Analyze each changed file
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
// Check for source code files that might need doc updates
|
|
217
|
+
if (/\.(ts|js|py|go|rs)$/.test(file.filename)) {
|
|
218
|
+
// Check if there's a corresponding doc file
|
|
219
|
+
const docFile = file.filename
|
|
220
|
+
.replace(/\.(ts|js|py|go|rs)$/, '.md')
|
|
221
|
+
.replace(/^src\//, 'docs/');
|
|
222
|
+
const hasDocChanges = files.some((f) => f.filename === docFile);
|
|
223
|
+
if (!hasDocChanges && file.status === 'modified') {
|
|
224
|
+
// Check if the diff contains new exports/functions
|
|
225
|
+
const patch = file.patch || '';
|
|
226
|
+
const hasNewExports = /^\+.*export\s+(function|const|class|interface)/m.test(patch);
|
|
227
|
+
const hasNewPubFn = /^\+.*pub\s+fn/m.test(patch); // Rust
|
|
228
|
+
const hasNewDefPy = /^\+.*def\s+[a-z]/m.test(patch); // Python
|
|
229
|
+
if (hasNewExports || hasNewPubFn || hasNewDefPy) {
|
|
230
|
+
issues.push({
|
|
231
|
+
type: 'missing',
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
file: file.filename,
|
|
234
|
+
message: 'New API added but documentation may not be updated',
|
|
235
|
+
suggestion: `Run \`skrypt generate\` to update documentation for ${file.filename}`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Check markdown files for potential issues
|
|
241
|
+
if (/\.mdx?$/.test(file.filename)) {
|
|
242
|
+
const patch = file.patch || '';
|
|
243
|
+
// Check for broken code blocks
|
|
244
|
+
const codeBlockIssues = checkCodeBlocks(patch, file.filename);
|
|
245
|
+
issues.push(...codeBlockIssues);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return issues;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check code blocks in markdown for potential issues
|
|
252
|
+
*/
|
|
253
|
+
function checkCodeBlocks(patch, filename) {
|
|
254
|
+
const issues = [];
|
|
255
|
+
// Find code blocks being added
|
|
256
|
+
const addedLines = patch
|
|
257
|
+
.split('\n')
|
|
258
|
+
.filter(line => line.startsWith('+'))
|
|
259
|
+
.map(line => line.slice(1))
|
|
260
|
+
.join('\n');
|
|
261
|
+
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
262
|
+
let match;
|
|
263
|
+
while ((match = codeBlockRegex.exec(addedLines)) !== null) {
|
|
264
|
+
const language = match[1] || 'text';
|
|
265
|
+
const code = match[2];
|
|
266
|
+
if (!code)
|
|
267
|
+
continue;
|
|
268
|
+
// Check for common issues
|
|
269
|
+
if (code.includes('TODO') || code.includes('FIXME')) {
|
|
270
|
+
issues.push({
|
|
271
|
+
type: 'broken_example',
|
|
272
|
+
severity: 'warning',
|
|
273
|
+
file: filename,
|
|
274
|
+
message: 'Code example contains TODO/FIXME marker',
|
|
275
|
+
suggestion: 'Complete the code example before merging',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (code.includes('...') && !code.includes('...args')) {
|
|
279
|
+
issues.push({
|
|
280
|
+
type: 'broken_example',
|
|
281
|
+
severity: 'info',
|
|
282
|
+
file: filename,
|
|
283
|
+
message: 'Code example contains ellipsis (...) placeholder',
|
|
284
|
+
suggestion: 'Consider providing complete, runnable code',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// Check for obviously broken syntax
|
|
288
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
289
|
+
try {
|
|
290
|
+
new Function(code);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
294
|
+
// Only flag if it's clearly meant to be runnable
|
|
295
|
+
if (!code.includes('// ...') && code.length > 20) {
|
|
296
|
+
issues.push({
|
|
297
|
+
type: 'broken_example',
|
|
298
|
+
severity: 'error',
|
|
299
|
+
file: filename,
|
|
300
|
+
message: `JavaScript/TypeScript syntax error: ${errMsg.split('\n')[0]}`,
|
|
301
|
+
suggestion: 'Fix the code example or mark it as pseudo-code',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return issues;
|
|
308
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LLMClient, LLMClientConfig, CompletionRequest, CompletionResponse } from './types.js';
|
|
2
|
+
import { LLMProvider } from '../config/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Anthropic Claude client
|
|
5
|
+
*/
|
|
6
|
+
export declare class AnthropicClient implements LLMClient {
|
|
7
|
+
provider: LLMProvider;
|
|
8
|
+
private client;
|
|
9
|
+
private model;
|
|
10
|
+
private maxRetries;
|
|
11
|
+
constructor(config: LLMClientConfig);
|
|
12
|
+
isConfigured(): boolean;
|
|
13
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
14
|
+
private mapStopReason;
|
|
15
|
+
private handleError;
|
|
16
|
+
}
|