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,101 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content negotiation middleware for AI agents.
|
|
5
|
+
*
|
|
6
|
+
* When agents send `Accept: text/markdown`, rewrite to the markdown route handler.
|
|
7
|
+
* Also handles `.md` URL suffix for explicit markdown requests.
|
|
8
|
+
* Adds Link + X-Llms-Txt discovery headers to all responses.
|
|
9
|
+
*/
|
|
10
|
+
export function middleware(request: NextRequest) {
|
|
11
|
+
const { pathname } = request.nextUrl
|
|
12
|
+
|
|
13
|
+
// Skip non-doc paths: API routes, static assets, internal Next.js paths
|
|
14
|
+
if (
|
|
15
|
+
pathname.startsWith('/api/') ||
|
|
16
|
+
pathname.startsWith('/_next/') ||
|
|
17
|
+
pathname.startsWith('/reference') ||
|
|
18
|
+
pathname === '/robots.txt' ||
|
|
19
|
+
pathname === '/sitemap.xml' ||
|
|
20
|
+
pathname === '/llms.txt' ||
|
|
21
|
+
pathname === '/llms-full.md' ||
|
|
22
|
+
pathname === '/favicon.ico' ||
|
|
23
|
+
/\.(ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|css|js|map)$/.test(pathname)
|
|
24
|
+
) {
|
|
25
|
+
return addDiscoveryHeaders(NextResponse.next(), request)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle .md URL suffix: /docs/getting-started.md → serve markdown for /docs/getting-started
|
|
29
|
+
if (pathname.endsWith('.md') && pathname.startsWith('/docs/')) {
|
|
30
|
+
const cleanPath = pathname.slice(0, -3) // strip .md
|
|
31
|
+
const slug = cleanPath.replace(/^\/docs\/?/, '')
|
|
32
|
+
if (slug) {
|
|
33
|
+
const url = request.nextUrl.clone()
|
|
34
|
+
url.pathname = `/md/${slug}`
|
|
35
|
+
return addDiscoveryHeaders(NextResponse.rewrite(url), request)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Content negotiation: detect Accept: text/markdown
|
|
40
|
+
const accept = request.headers.get('accept') || ''
|
|
41
|
+
if (prefersMarkdown(accept) && pathname.startsWith('/docs/')) {
|
|
42
|
+
const slug = pathname.replace(/^\/docs\/?/, '')
|
|
43
|
+
if (slug) {
|
|
44
|
+
const url = request.nextUrl.clone()
|
|
45
|
+
url.pathname = `/md/${slug}`
|
|
46
|
+
return addDiscoveryHeaders(NextResponse.rewrite(url), request)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return addDiscoveryHeaders(NextResponse.next(), request)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if the Accept header prefers text/markdown over text/html.
|
|
55
|
+
* Handles quality values: text/markdown;q=1.0, text/html;q=0.9
|
|
56
|
+
*/
|
|
57
|
+
function prefersMarkdown(accept: string): boolean {
|
|
58
|
+
if (!accept) return false
|
|
59
|
+
|
|
60
|
+
const lower = accept.toLowerCase()
|
|
61
|
+
|
|
62
|
+
// Quick check: if text/markdown appears at all
|
|
63
|
+
if (!lower.includes('text/markdown')) return false
|
|
64
|
+
|
|
65
|
+
// Parse quality values
|
|
66
|
+
let mdQuality = 0
|
|
67
|
+
let htmlQuality = 0
|
|
68
|
+
|
|
69
|
+
for (const part of lower.split(',')) {
|
|
70
|
+
const trimmed = part.trim()
|
|
71
|
+
const qMatch = trimmed.match(/;q=([0-9.]+)/)
|
|
72
|
+
const quality = qMatch ? Math.min(parseFloat(qMatch[1]) || 0, 1.0) : 1.0
|
|
73
|
+
|
|
74
|
+
if (trimmed.startsWith('text/markdown')) {
|
|
75
|
+
mdQuality = quality
|
|
76
|
+
} else if (trimmed.startsWith('text/html')) {
|
|
77
|
+
htmlQuality = quality
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return mdQuality > 0 && mdQuality >= htmlQuality
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add agent discovery headers to every response.
|
|
86
|
+
* - Link: points agents to llms.txt
|
|
87
|
+
* - X-Llms-Txt: explicit pointer for agent tooling
|
|
88
|
+
*/
|
|
89
|
+
function addDiscoveryHeaders(response: NextResponse, request: NextRequest): NextResponse {
|
|
90
|
+
const origin = request.nextUrl.origin
|
|
91
|
+
response.headers.set('Link', `<${origin}/llms.txt>; rel="help", <${origin}/llms-full.md>; rel="help"`)
|
|
92
|
+
response.headers.set('X-Llms-Txt', `${origin}/llms.txt`)
|
|
93
|
+
return response
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const config = {
|
|
97
|
+
matcher: [
|
|
98
|
+
// Match all paths except static files
|
|
99
|
+
'/((?!_next/static|_next/image).*)',
|
|
100
|
+
],
|
|
101
|
+
}
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
--color-border: rgba(3, 7, 18, 0.1);
|
|
59
59
|
--color-border-strong: rgba(3, 7, 18, 0.15);
|
|
60
60
|
|
|
61
|
-
/* Code */
|
|
62
|
-
--color-code-bg: #
|
|
61
|
+
/* Code — high-contrast, matching Mintlify/Stripe/Vercel standards */
|
|
62
|
+
--color-code-bg: #fafafa;
|
|
63
63
|
--color-code-border: rgba(3, 7, 18, 0.1);
|
|
64
|
-
--color-code-text: #
|
|
64
|
+
--color-code-text: #1a1a2e;
|
|
65
65
|
|
|
66
66
|
/* Layout */
|
|
67
67
|
--sidebar-width: 264px;
|
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
--color-bg: #0f1117;
|
|
81
81
|
--color-bg-secondary: #1a1d27;
|
|
82
82
|
--color-bg-tertiary: #252833;
|
|
83
|
-
--color-text: #
|
|
84
|
-
--color-text-secondary: #
|
|
83
|
+
--color-text: #e4e5e9;
|
|
84
|
+
--color-text-secondary: #b4b8c1;
|
|
85
85
|
--color-text-tertiary: #6b7280;
|
|
86
86
|
--color-border: rgba(255, 255, 255, 0.07);
|
|
87
87
|
--color-border-strong: rgba(255, 255, 255, 0.12);
|
|
88
|
-
--color-code-bg: #
|
|
89
|
-
--color-code-border: rgba(255, 255, 255, 0.
|
|
90
|
-
--color-code-text: #
|
|
88
|
+
--color-code-bg: #0a0a0c;
|
|
89
|
+
--color-code-border: rgba(255, 255, 255, 0.08);
|
|
90
|
+
--color-code-text: #e8eaed;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -95,14 +95,14 @@
|
|
|
95
95
|
--color-bg: #0f1117;
|
|
96
96
|
--color-bg-secondary: #1a1d27;
|
|
97
97
|
--color-bg-tertiary: #252833;
|
|
98
|
-
--color-text: #
|
|
99
|
-
--color-text-secondary: #
|
|
98
|
+
--color-text: #e4e5e9;
|
|
99
|
+
--color-text-secondary: #b4b8c1;
|
|
100
100
|
--color-text-tertiary: #6b7280;
|
|
101
101
|
--color-border: rgba(255, 255, 255, 0.07);
|
|
102
102
|
--color-border-strong: rgba(255, 255, 255, 0.12);
|
|
103
|
-
--color-code-bg: #
|
|
104
|
-
--color-code-border: rgba(255, 255, 255, 0.
|
|
105
|
-
--color-code-text: #
|
|
103
|
+
--color-code-bg: #0a0a0c;
|
|
104
|
+
--color-code-border: rgba(255, 255, 255, 0.08);
|
|
105
|
+
--color-code-text: #e8eaed;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/* Error page icon — dark mode overrides (avoids Tailwind dark: variant mismatch) */
|
|
@@ -205,33 +205,41 @@ pre {
|
|
|
205
205
|
background-color: var(--color-code-bg) !important;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/* Ensure Shiki syntax tokens render at full opacity — never washed out */
|
|
209
|
+
.shiki code {
|
|
210
|
+
color: var(--color-code-text);
|
|
211
|
+
}
|
|
212
|
+
.shiki .line span {
|
|
213
|
+
opacity: 1 !important;
|
|
214
|
+
}
|
|
215
|
+
|
|
208
216
|
code {
|
|
209
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
217
|
+
font-family: var(--font-mono, ui-monospace), SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
210
218
|
font-size: 0.875em;
|
|
211
219
|
font-variant-ligatures: none;
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
/* Inline code — Mintlify style */
|
|
222
|
+
/* Inline code — high contrast, Mintlify/Stripe style */
|
|
215
223
|
:not(pre) > code {
|
|
216
224
|
background-color: var(--color-gray-100);
|
|
217
225
|
padding: 0.125rem 0.5rem;
|
|
218
226
|
border-radius: 0.375rem;
|
|
219
227
|
font-size: 0.875em;
|
|
220
228
|
line-height: 1.5;
|
|
221
|
-
color: var(--color-gray-
|
|
229
|
+
color: var(--color-gray-800);
|
|
222
230
|
overflow-wrap: break-word;
|
|
223
231
|
word-break: break-word;
|
|
224
232
|
}
|
|
225
233
|
|
|
226
234
|
:root.dark :not(pre) > code {
|
|
227
|
-
background-color: rgba(255, 255, 255, 0.
|
|
228
|
-
color:
|
|
235
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
236
|
+
color: #e0e2e6;
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
@media (prefers-color-scheme: dark) {
|
|
232
240
|
:root:not(.light) :not(pre) > code {
|
|
233
|
-
background-color: rgba(255, 255, 255, 0.
|
|
234
|
-
color:
|
|
241
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
242
|
+
color: #e0e2e6;
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
245
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare actual output against expected output
|
|
3
|
+
*/
|
|
4
|
+
export function compareOutput(actual, expected, mode = 'exact') {
|
|
5
|
+
const normalizedActual = normalize(actual);
|
|
6
|
+
const normalizedExpected = normalize(expected);
|
|
7
|
+
if (mode === 'contains') {
|
|
8
|
+
const match = normalizedActual.includes(normalizedExpected);
|
|
9
|
+
if (match) {
|
|
10
|
+
return { match: true, diff: '' };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
match: false,
|
|
14
|
+
diff: buildDiff(normalizedExpected, normalizedActual, 'contains'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Exact mode
|
|
18
|
+
if (normalizedActual === normalizedExpected) {
|
|
19
|
+
return { match: true, diff: '' };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
match: false,
|
|
23
|
+
diff: buildDiff(normalizedExpected, normalizedActual, 'exact'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Normalize output for comparison: trim whitespace, normalize line endings
|
|
28
|
+
*/
|
|
29
|
+
function normalize(s) {
|
|
30
|
+
return s
|
|
31
|
+
.replace(/\r\n/g, '\n')
|
|
32
|
+
.replace(/\r/g, '\n')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build a unified-style diff for display
|
|
37
|
+
*/
|
|
38
|
+
function buildDiff(expected, actual, mode) {
|
|
39
|
+
const expectedLines = expected.split('\n');
|
|
40
|
+
const actualLines = actual.split('\n');
|
|
41
|
+
const lines = [];
|
|
42
|
+
if (mode === 'contains') {
|
|
43
|
+
lines.push('--- expected (contains) ---');
|
|
44
|
+
lines.push('+++ actual +++');
|
|
45
|
+
lines.push('');
|
|
46
|
+
for (const line of expectedLines) {
|
|
47
|
+
lines.push(`- ${line}`);
|
|
48
|
+
}
|
|
49
|
+
lines.push('---');
|
|
50
|
+
for (const line of actualLines) {
|
|
51
|
+
lines.push(`+ ${line}`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
lines.push('--- expected ---');
|
|
56
|
+
lines.push('+++ actual +++');
|
|
57
|
+
lines.push('');
|
|
58
|
+
const maxLen = Math.max(expectedLines.length, actualLines.length);
|
|
59
|
+
for (let i = 0; i < maxLen; i++) {
|
|
60
|
+
const exp = expectedLines[i];
|
|
61
|
+
const act = actualLines[i];
|
|
62
|
+
if (exp === undefined) {
|
|
63
|
+
lines.push(`+ ${act}`);
|
|
64
|
+
}
|
|
65
|
+
else if (act === undefined) {
|
|
66
|
+
lines.push(`- ${exp}`);
|
|
67
|
+
}
|
|
68
|
+
else if (exp !== act) {
|
|
69
|
+
lines.push(`- ${exp}`);
|
|
70
|
+
lines.push(`+ ${act}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
lines.push(` ${exp}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExtractedSnippet, TestResult, DockerEnvironment, RunnerConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Available Docker environments for multi-env testing
|
|
4
|
+
*/
|
|
5
|
+
export declare const DOCKER_ENVIRONMENTS: DockerEnvironment[];
|
|
6
|
+
/**
|
|
7
|
+
* Check if Docker is available on this machine
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDockerAvailable(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a comma-separated environments string into DockerEnvironment objects
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseEnvironments(envString: string): DockerEnvironment[];
|
|
14
|
+
/**
|
|
15
|
+
* Get compatible Docker environments for a given language
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCompatibleEnvironments(language: string, environments: DockerEnvironment[]): DockerEnvironment[];
|
|
18
|
+
/**
|
|
19
|
+
* Run a snippet in a Docker container
|
|
20
|
+
*/
|
|
21
|
+
export declare function runInDocker(snippet: ExtractedSnippet, environment: DockerEnvironment, config: RunnerConfig): Promise<TestResult>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { spawnSync, spawn } from 'child_process';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { compareOutput } from './comparator.js';
|
|
7
|
+
import { checkRequiredEnv } from './env.js';
|
|
8
|
+
/**
|
|
9
|
+
* Available Docker environments for multi-env testing
|
|
10
|
+
*/
|
|
11
|
+
export const DOCKER_ENVIRONMENTS = [
|
|
12
|
+
{ name: 'node-20', image: 'node:20-slim', command: 'node', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
13
|
+
{ name: 'node-18', image: 'node:18-slim', command: 'node', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
14
|
+
{ name: 'python-3.12', image: 'python:3.12-slim', command: 'python3', languages: ['python', 'py'] },
|
|
15
|
+
{ name: 'python-3.10', image: 'python:3.10-slim', command: 'python3', languages: ['python', 'py'] },
|
|
16
|
+
{ name: 'bun', image: 'oven/bun:latest', command: 'bun', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
17
|
+
{ name: 'deno', image: 'denoland/deno:latest', command: 'deno', languages: ['typescript', 'ts', 'javascript', 'js'] },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Check if Docker is available on this machine
|
|
21
|
+
*/
|
|
22
|
+
export function isDockerAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
const result = spawnSync('docker', ['info'], {
|
|
25
|
+
stdio: 'pipe',
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
});
|
|
28
|
+
return result.status === 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a comma-separated environments string into DockerEnvironment objects
|
|
36
|
+
*/
|
|
37
|
+
export function parseEnvironments(envString) {
|
|
38
|
+
const names = envString.split(',').map(s => s.trim()).filter(Boolean);
|
|
39
|
+
const envs = [];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const env = DOCKER_ENVIRONMENTS.find(e => e.name === name);
|
|
42
|
+
if (env) {
|
|
43
|
+
envs.push(env);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn(` Warning: Unknown environment "${name}". Available: ${DOCKER_ENVIRONMENTS.map(e => e.name).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return envs;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get compatible Docker environments for a given language
|
|
53
|
+
*/
|
|
54
|
+
export function getCompatibleEnvironments(language, environments) {
|
|
55
|
+
return environments.filter(env => env.languages.includes(language));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run a snippet in a Docker container
|
|
59
|
+
*/
|
|
60
|
+
export async function runInDocker(snippet, environment, config) {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const envLabel = `docker:${environment.name}`;
|
|
63
|
+
// Check for skip directive
|
|
64
|
+
if (snippet.skipReason) {
|
|
65
|
+
return {
|
|
66
|
+
snippet,
|
|
67
|
+
status: 'skip',
|
|
68
|
+
stdout: '',
|
|
69
|
+
stderr: `Skipped: ${snippet.skipReason}`,
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
duration: 0,
|
|
72
|
+
environment: envLabel,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Check required env vars
|
|
76
|
+
if (snippet.requiredEnv && snippet.requiredEnv.length > 0) {
|
|
77
|
+
const { ok, missing } = checkRequiredEnv(snippet.requiredEnv, config.envVars);
|
|
78
|
+
if (!ok) {
|
|
79
|
+
return {
|
|
80
|
+
snippet,
|
|
81
|
+
status: 'skip',
|
|
82
|
+
stdout: '',
|
|
83
|
+
stderr: `Missing required env vars: ${missing.join(', ')}`,
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
duration: 0,
|
|
86
|
+
environment: envLabel,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const tempDir = join(tmpdir(), `skrypt-docker-${randomUUID()}`);
|
|
91
|
+
mkdirSync(tempDir, { recursive: true });
|
|
92
|
+
try {
|
|
93
|
+
const isTS = ['typescript', 'ts'].includes(snippet.language);
|
|
94
|
+
const isPy = ['python', 'py'].includes(snippet.language);
|
|
95
|
+
let filename;
|
|
96
|
+
let containerCmd;
|
|
97
|
+
if (isPy) {
|
|
98
|
+
filename = 'test.py';
|
|
99
|
+
containerCmd = ['python3', `/work/${filename}`];
|
|
100
|
+
}
|
|
101
|
+
else if (isTS && environment.name === 'bun') {
|
|
102
|
+
filename = 'test.ts';
|
|
103
|
+
containerCmd = ['bun', 'run', `/work/${filename}`];
|
|
104
|
+
}
|
|
105
|
+
else if (isTS && environment.name === 'deno') {
|
|
106
|
+
filename = 'test.ts';
|
|
107
|
+
containerCmd = ['deno', 'run', '--allow-all', `/work/${filename}`];
|
|
108
|
+
}
|
|
109
|
+
else if (isTS) {
|
|
110
|
+
// Node needs tsx for TypeScript
|
|
111
|
+
filename = 'test.ts';
|
|
112
|
+
containerCmd = ['npx', '-y', 'tsx', `/work/${filename}`];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
filename = 'test.js';
|
|
116
|
+
containerCmd = ['node', `/work/${filename}`];
|
|
117
|
+
}
|
|
118
|
+
writeFileSync(join(tempDir, filename), snippet.code);
|
|
119
|
+
// Build Docker args
|
|
120
|
+
const dockerArgs = ['run', '--rm', '-v', `${tempDir}:/work`, '-w', '/work'];
|
|
121
|
+
// Inject env vars
|
|
122
|
+
for (const [key, value] of Object.entries(config.envVars)) {
|
|
123
|
+
dockerArgs.push('-e', `${key}=${value}`);
|
|
124
|
+
}
|
|
125
|
+
dockerArgs.push(environment.image, ...containerCmd);
|
|
126
|
+
const result = await executeDocker(dockerArgs, config.timeout);
|
|
127
|
+
const duration = Date.now() - startTime;
|
|
128
|
+
// Check output if expected
|
|
129
|
+
if (snippet.expectedOutput !== undefined) {
|
|
130
|
+
const comparison = compareOutput(result.stdout, snippet.expectedOutput, snippet.expectedOutputMode || 'exact');
|
|
131
|
+
if (!comparison.match) {
|
|
132
|
+
return {
|
|
133
|
+
snippet,
|
|
134
|
+
status: 'output_mismatch',
|
|
135
|
+
stdout: result.stdout,
|
|
136
|
+
stderr: result.stderr,
|
|
137
|
+
exitCode: result.exitCode,
|
|
138
|
+
duration,
|
|
139
|
+
environment: envLabel,
|
|
140
|
+
outputMatch: false,
|
|
141
|
+
diff: comparison.diff,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
snippet,
|
|
146
|
+
status: result.exitCode === 0 ? 'pass' : 'fail',
|
|
147
|
+
stdout: result.stdout,
|
|
148
|
+
stderr: result.stderr,
|
|
149
|
+
exitCode: result.exitCode,
|
|
150
|
+
duration,
|
|
151
|
+
environment: envLabel,
|
|
152
|
+
outputMatch: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
snippet,
|
|
157
|
+
status: result.timedOut ? 'timeout' : (result.exitCode === 0 ? 'pass' : 'fail'),
|
|
158
|
+
stdout: result.stdout,
|
|
159
|
+
stderr: result.timedOut
|
|
160
|
+
? `Timeout: execution exceeded ${config.timeout}ms`
|
|
161
|
+
: result.stderr,
|
|
162
|
+
exitCode: result.exitCode,
|
|
163
|
+
duration,
|
|
164
|
+
environment: envLabel,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
return {
|
|
170
|
+
snippet,
|
|
171
|
+
status: 'fail',
|
|
172
|
+
stdout: '',
|
|
173
|
+
stderr: message,
|
|
174
|
+
exitCode: 1,
|
|
175
|
+
duration: Date.now() - startTime,
|
|
176
|
+
environment: envLabel,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
try {
|
|
181
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore cleanup errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function executeDocker(args, timeoutMs) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const proc = spawn('docker', args, {
|
|
191
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
192
|
+
});
|
|
193
|
+
let stdout = '';
|
|
194
|
+
let stderr = '';
|
|
195
|
+
let timedOut = false;
|
|
196
|
+
const MAX_BUFFER = 1024 * 1024; // 1MB cap to prevent OOM
|
|
197
|
+
const timeout = setTimeout(() => {
|
|
198
|
+
timedOut = true;
|
|
199
|
+
proc.kill('SIGKILL');
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
proc.stdout?.on('data', (data) => {
|
|
202
|
+
if (stdout.length < MAX_BUFFER) {
|
|
203
|
+
stdout += data.toString();
|
|
204
|
+
if (stdout.length > MAX_BUFFER)
|
|
205
|
+
stdout = stdout.slice(0, MAX_BUFFER);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
proc.stderr?.on('data', (data) => {
|
|
209
|
+
if (stderr.length < MAX_BUFFER) {
|
|
210
|
+
stderr += data.toString();
|
|
211
|
+
if (stderr.length > MAX_BUFFER)
|
|
212
|
+
stderr = stderr.slice(0, MAX_BUFFER);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
proc.on('close', (code) => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
resolve({
|
|
218
|
+
exitCode: timedOut ? 1 : (code ?? 1),
|
|
219
|
+
stdout,
|
|
220
|
+
stderr,
|
|
221
|
+
timedOut,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
proc.on('error', (err) => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
resolve({
|
|
227
|
+
exitCode: 1,
|
|
228
|
+
stdout,
|
|
229
|
+
stderr: err.message,
|
|
230
|
+
timedOut: false,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load environment variables from a .env file
|
|
3
|
+
*/
|
|
4
|
+
export declare function loadEnvFile(envFilePath: string): Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Build a clean environment for snippet execution.
|
|
7
|
+
* Only passes PATH, HOME, and explicitly injected vars.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildCleanEnv(injectedVars: Record<string, string>): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Check if all required env vars are present
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkRequiredEnv(required: string[], available: Record<string, string>): {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
missing: string[];
|
|
16
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
/**
|
|
3
|
+
* Load environment variables from a .env file
|
|
4
|
+
*/
|
|
5
|
+
export function loadEnvFile(envFilePath) {
|
|
6
|
+
if (!existsSync(envFilePath)) {
|
|
7
|
+
throw new Error(`Env file not found: ${envFilePath}`);
|
|
8
|
+
}
|
|
9
|
+
const content = readFileSync(envFilePath, 'utf-8');
|
|
10
|
+
const vars = {};
|
|
11
|
+
for (const line of content.split('\n')) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
14
|
+
continue;
|
|
15
|
+
const eqIdx = trimmed.indexOf('=');
|
|
16
|
+
if (eqIdx === -1)
|
|
17
|
+
continue;
|
|
18
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
19
|
+
let value = trimmed.substring(eqIdx + 1).trim();
|
|
20
|
+
// Strip surrounding quotes
|
|
21
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
22
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
23
|
+
value = value.slice(1, -1);
|
|
24
|
+
}
|
|
25
|
+
vars[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return vars;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a clean environment for snippet execution.
|
|
31
|
+
* Only passes PATH, HOME, and explicitly injected vars.
|
|
32
|
+
*/
|
|
33
|
+
export function buildCleanEnv(injectedVars) {
|
|
34
|
+
const clean = {
|
|
35
|
+
NODE_NO_WARNINGS: '1',
|
|
36
|
+
};
|
|
37
|
+
// Carry over PATH so runtimes can be found
|
|
38
|
+
if (process.env.PATH) {
|
|
39
|
+
clean.PATH = process.env.PATH;
|
|
40
|
+
}
|
|
41
|
+
// HOME / USERPROFILE for tools that need it
|
|
42
|
+
if (process.env.HOME) {
|
|
43
|
+
clean.HOME = process.env.HOME;
|
|
44
|
+
}
|
|
45
|
+
if (process.env.USERPROFILE) {
|
|
46
|
+
clean.USERPROFILE = process.env.USERPROFILE;
|
|
47
|
+
}
|
|
48
|
+
// Inject user-provided vars (API keys, etc.)
|
|
49
|
+
Object.assign(clean, injectedVars);
|
|
50
|
+
return clean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if all required env vars are present
|
|
54
|
+
*/
|
|
55
|
+
export function checkRequiredEnv(required, available) {
|
|
56
|
+
const missing = required.filter(key => !available[key]);
|
|
57
|
+
return { ok: missing.length === 0, missing };
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExtractedSnippet } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find all .md/.mdx files in a directory recursively
|
|
4
|
+
*/
|
|
5
|
+
export declare function findDocFiles(dir: string): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Extract code blocks from a markdown/MDX file
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractSnippets(filePath: string, languageFilter?: string): ExtractedSnippet[];
|