skrypt-ai 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/auth/index.js +6 -4
  11. package/dist/cli.js +12 -2
  12. package/dist/commands/audit.d.ts +2 -0
  13. package/dist/commands/audit.js +59 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +73 -0
  16. package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
  17. package/dist/commands/generate/index.js +393 -0
  18. package/dist/commands/generate/scan.d.ts +41 -0
  19. package/dist/commands/generate/scan.js +256 -0
  20. package/dist/commands/generate/verify.d.ts +14 -0
  21. package/dist/commands/generate/verify.js +122 -0
  22. package/dist/commands/generate/write.d.ts +25 -0
  23. package/dist/commands/generate/write.js +120 -0
  24. package/dist/commands/import.js +4 -1
  25. package/dist/commands/llms-txt.js +6 -4
  26. package/dist/commands/refresh.d.ts +2 -0
  27. package/dist/commands/refresh.js +158 -0
  28. package/dist/commands/review.d.ts +2 -0
  29. package/dist/commands/review.js +110 -0
  30. package/dist/commands/test.js +177 -236
  31. package/dist/commands/watch.js +29 -20
  32. package/dist/config/loader.d.ts +6 -2
  33. package/dist/config/loader.js +39 -3
  34. package/dist/config/types.d.ts +7 -0
  35. package/dist/generator/agents-md.d.ts +25 -0
  36. package/dist/generator/agents-md.js +122 -0
  37. package/dist/generator/generator.js +2 -1
  38. package/dist/generator/index.d.ts +2 -0
  39. package/dist/generator/index.js +2 -0
  40. package/dist/generator/mdx-serializer.d.ts +11 -0
  41. package/dist/generator/mdx-serializer.js +135 -0
  42. package/dist/generator/organizer.d.ts +1 -16
  43. package/dist/generator/organizer.js +0 -38
  44. package/dist/generator/types.d.ts +3 -0
  45. package/dist/generator/writer.js +65 -32
  46. package/dist/github/org-discovery.d.ts +17 -0
  47. package/dist/github/org-discovery.js +93 -0
  48. package/dist/llm/index.d.ts +2 -0
  49. package/dist/llm/index.js +8 -2
  50. package/dist/llm/proxy-client.d.ts +32 -0
  51. package/dist/llm/proxy-client.js +103 -0
  52. package/dist/next-actions/actions.d.ts +2 -0
  53. package/dist/next-actions/actions.js +190 -0
  54. package/dist/next-actions/index.d.ts +6 -0
  55. package/dist/next-actions/index.js +39 -0
  56. package/dist/next-actions/setup.d.ts +2 -0
  57. package/dist/next-actions/setup.js +72 -0
  58. package/dist/next-actions/state.d.ts +7 -0
  59. package/dist/next-actions/state.js +68 -0
  60. package/dist/next-actions/suggest.d.ts +3 -0
  61. package/dist/next-actions/suggest.js +47 -0
  62. package/dist/next-actions/types.d.ts +26 -0
  63. package/dist/refresh/differ.d.ts +9 -0
  64. package/dist/refresh/differ.js +67 -0
  65. package/dist/refresh/index.d.ts +4 -0
  66. package/dist/refresh/index.js +4 -0
  67. package/dist/refresh/manifest.d.ts +18 -0
  68. package/dist/refresh/manifest.js +71 -0
  69. package/dist/refresh/splicer.d.ts +9 -0
  70. package/dist/refresh/splicer.js +50 -0
  71. package/dist/refresh/types.d.ts +37 -0
  72. package/dist/review/index.d.ts +8 -0
  73. package/dist/review/index.js +94 -0
  74. package/dist/review/parser.d.ts +16 -0
  75. package/dist/review/parser.js +95 -0
  76. package/dist/review/types.d.ts +18 -0
  77. package/dist/scanner/csharp.d.ts +0 -4
  78. package/dist/scanner/csharp.js +9 -49
  79. package/dist/scanner/go.d.ts +0 -3
  80. package/dist/scanner/go.js +8 -35
  81. package/dist/scanner/java.d.ts +0 -4
  82. package/dist/scanner/java.js +9 -49
  83. package/dist/scanner/kotlin.d.ts +0 -3
  84. package/dist/scanner/kotlin.js +6 -33
  85. package/dist/scanner/php.d.ts +0 -10
  86. package/dist/scanner/php.js +11 -55
  87. package/dist/scanner/ruby.d.ts +0 -3
  88. package/dist/scanner/ruby.js +8 -38
  89. package/dist/scanner/rust.d.ts +0 -3
  90. package/dist/scanner/rust.js +10 -37
  91. package/dist/scanner/swift.d.ts +0 -3
  92. package/dist/scanner/swift.js +8 -35
  93. package/dist/scanner/types.d.ts +2 -0
  94. package/dist/scanner/utils.d.ts +41 -0
  95. package/dist/scanner/utils.js +97 -0
  96. package/dist/structure/index.d.ts +19 -0
  97. package/dist/structure/index.js +92 -0
  98. package/dist/structure/planner.d.ts +8 -0
  99. package/dist/structure/planner.js +180 -0
  100. package/dist/structure/topology.d.ts +16 -0
  101. package/dist/structure/topology.js +49 -0
  102. package/dist/structure/types.d.ts +26 -0
  103. package/dist/template/docs.json +5 -2
  104. package/dist/template/next.config.mjs +31 -0
  105. package/dist/template/package.json +5 -3
  106. package/dist/template/src/app/layout.tsx +13 -13
  107. package/dist/template/src/app/llms-full.md/route.ts +29 -0
  108. package/dist/template/src/app/llms.txt/route.ts +29 -0
  109. package/dist/template/src/app/md/[...slug]/route.ts +174 -0
  110. package/dist/template/src/app/reference/route.ts +22 -18
  111. package/dist/template/src/app/sitemap.ts +1 -1
  112. package/dist/template/src/components/ai-chat-impl.tsx +206 -0
  113. package/dist/template/src/components/ai-chat.tsx +20 -193
  114. package/dist/template/src/components/mdx/index.tsx +27 -4
  115. package/dist/template/src/lib/fonts.ts +135 -0
  116. package/dist/template/src/middleware.ts +101 -0
  117. package/dist/template/src/styles/globals.css +28 -20
  118. package/dist/testing/comparator.d.ts +7 -0
  119. package/dist/testing/comparator.js +77 -0
  120. package/dist/testing/docker.d.ts +21 -0
  121. package/dist/testing/docker.js +234 -0
  122. package/dist/testing/env.d.ts +16 -0
  123. package/dist/testing/env.js +58 -0
  124. package/dist/testing/extractor.d.ts +9 -0
  125. package/dist/testing/extractor.js +195 -0
  126. package/dist/testing/index.d.ts +6 -0
  127. package/dist/testing/index.js +6 -0
  128. package/dist/testing/runner.d.ts +5 -0
  129. package/dist/testing/runner.js +225 -0
  130. package/dist/testing/types.d.ts +58 -0
  131. package/dist/utils/files.d.ts +0 -8
  132. package/dist/utils/files.js +0 -33
  133. package/package.json +1 -1
  134. package/dist/autofix/autofix.test.js +0 -487
  135. package/dist/commands/generate.js +0 -445
  136. package/dist/generator/generator.test.js +0 -259
  137. package/dist/generator/writer.test.js +0 -411
  138. package/dist/llm/llm.manual-test.js +0 -112
  139. package/dist/llm/llm.mock-test.d.ts +0 -4
  140. package/dist/llm/llm.mock-test.js +0 -79
  141. package/dist/plugins/index.d.ts +0 -47
  142. package/dist/plugins/index.js +0 -181
  143. package/dist/scanner/content-type.test.js +0 -231
  144. package/dist/scanner/integration.test.d.ts +0 -4
  145. package/dist/scanner/integration.test.js +0 -180
  146. package/dist/scanner/scanner.test.js +0 -210
  147. package/dist/scanner/typescript.manual-test.d.ts +0 -1
  148. package/dist/scanner/typescript.manual-test.js +0 -112
  149. package/dist/template/src/app/docs/auth/page.mdx +0 -589
  150. package/dist/template/src/app/docs/autofix/page.mdx +0 -624
  151. package/dist/template/src/app/docs/cli/page.mdx +0 -217
  152. package/dist/template/src/app/docs/config/page.mdx +0 -428
  153. package/dist/template/src/app/docs/configuration/page.mdx +0 -86
  154. package/dist/template/src/app/docs/deployment/page.mdx +0 -112
  155. package/dist/template/src/app/docs/generator/generator.md +0 -504
  156. package/dist/template/src/app/docs/generator/organizer.md +0 -779
  157. package/dist/template/src/app/docs/generator/page.mdx +0 -613
  158. package/dist/template/src/app/docs/github/page.mdx +0 -502
  159. package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
  160. package/dist/template/src/app/docs/llm/index.md +0 -471
  161. package/dist/template/src/app/docs/llm/page.mdx +0 -428
  162. package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
  163. package/dist/template/src/app/docs/pro/page.mdx +0 -121
  164. package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
  165. package/dist/template/src/app/docs/scanner/content-type.md +0 -599
  166. package/dist/template/src/app/docs/scanner/index.md +0 -212
  167. package/dist/template/src/app/docs/scanner/page.mdx +0 -307
  168. package/dist/template/src/app/docs/scanner/python.md +0 -469
  169. package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
  170. package/dist/template/src/app/docs/scanner/rust.md +0 -325
  171. package/dist/template/src/app/docs/scanner/typescript.md +0 -201
  172. package/dist/template/src/app/icon.tsx +0 -29
  173. package/dist/utils/validation.d.ts +0 -1
  174. package/dist/utils/validation.js +0 -12
  175. /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
  176. /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
  177. /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
  178. /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
  179. /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
  180. /package/dist/{scanner/scanner.test.d.ts → testing/types.js} +0 -0
@@ -0,0 +1,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: #ffffff;
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: #1f2937;
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: #e0e2e6;
84
- --color-text-secondary: #b0b4bc;
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: #0b0c0e;
89
- --color-code-border: rgba(255, 255, 255, 0.1);
90
- --color-code-text: #e0e2e6;
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: #e0e2e6;
99
- --color-text-secondary: #b0b4bc;
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: #0b0c0e;
104
- --color-code-border: rgba(255, 255, 255, 0.1);
105
- --color-code-text: #e0e2e6;
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-600);
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.05);
228
- color: var(--color-gray-200);
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.05);
234
- color: var(--color-gray-200);
241
+ background-color: rgba(255, 255, 255, 0.08);
242
+ color: #e0e2e6;
235
243
  }
236
244
  }
237
245
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Compare actual output against expected output
3
+ */
4
+ export declare function compareOutput(actual: string, expected: string, mode?: 'exact' | 'contains'): {
5
+ match: boolean;
6
+ diff: string;
7
+ };
@@ -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[];