skrypt-ai 0.5.0 → 0.6.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/auth/index.js +8 -1
- package/dist/autofix/index.d.ts +0 -4
- package/dist/autofix/index.js +0 -21
- package/dist/capture/browser.d.ts +11 -0
- package/dist/capture/browser.js +173 -0
- package/dist/capture/diff.d.ts +23 -0
- package/dist/capture/diff.js +52 -0
- package/dist/capture/index.d.ts +23 -0
- package/dist/capture/index.js +210 -0
- package/dist/capture/naming.d.ts +17 -0
- package/dist/capture/naming.js +45 -0
- package/dist/capture/parser.d.ts +15 -0
- package/dist/capture/parser.js +80 -0
- package/dist/capture/types.d.ts +57 -0
- package/dist/capture/types.js +1 -0
- package/dist/cli.js +4 -0
- package/dist/commands/autofix.js +136 -120
- package/dist/commands/cron.js +58 -47
- package/dist/commands/deploy.js +123 -102
- package/dist/commands/generate.js +88 -6
- package/dist/commands/heal.d.ts +10 -0
- package/dist/commands/heal.js +201 -0
- package/dist/commands/i18n.js +146 -111
- package/dist/commands/lint.js +50 -44
- package/dist/commands/llms-txt.js +59 -49
- package/dist/commands/login.js +61 -43
- package/dist/commands/mcp.js +6 -0
- package/dist/commands/monitor.js +13 -8
- package/dist/commands/qa.d.ts +2 -0
- package/dist/commands/qa.js +43 -0
- package/dist/commands/review-pr.js +108 -102
- package/dist/commands/sdk.js +128 -122
- package/dist/commands/security.js +86 -80
- package/dist/commands/test.js +91 -92
- package/dist/commands/version.js +104 -75
- package/dist/commands/watch.js +130 -114
- package/dist/config/types.js +2 -2
- package/dist/context-hub/index.d.ts +23 -0
- package/dist/context-hub/index.js +179 -0
- package/dist/context-hub/mappings.d.ts +8 -0
- package/dist/context-hub/mappings.js +55 -0
- package/dist/context-hub/types.d.ts +33 -0
- package/dist/context-hub/types.js +1 -0
- package/dist/generator/generator.js +39 -6
- package/dist/generator/types.d.ts +7 -0
- package/dist/generator/writer.d.ts +3 -1
- package/dist/generator/writer.js +24 -4
- package/dist/llm/anthropic-client.d.ts +1 -0
- package/dist/llm/anthropic-client.js +3 -1
- package/dist/llm/index.d.ts +6 -4
- package/dist/llm/index.js +76 -261
- package/dist/llm/openai-client.d.ts +1 -0
- package/dist/llm/openai-client.js +7 -2
- package/dist/qa/checks.d.ts +10 -0
- package/dist/qa/checks.js +492 -0
- package/dist/qa/fixes.d.ts +30 -0
- package/dist/qa/fixes.js +277 -0
- package/dist/qa/index.d.ts +29 -0
- package/dist/qa/index.js +187 -0
- package/dist/qa/types.d.ts +24 -0
- package/dist/qa/types.js +1 -0
- package/dist/scanner/csharp.d.ts +23 -0
- package/dist/scanner/csharp.js +421 -0
- package/dist/scanner/index.js +16 -2
- package/dist/scanner/java.d.ts +39 -0
- package/dist/scanner/java.js +318 -0
- package/dist/scanner/kotlin.d.ts +23 -0
- package/dist/scanner/kotlin.js +389 -0
- package/dist/scanner/php.d.ts +57 -0
- package/dist/scanner/php.js +351 -0
- package/dist/scanner/ruby.d.ts +36 -0
- package/dist/scanner/ruby.js +431 -0
- package/dist/scanner/swift.d.ts +25 -0
- package/dist/scanner/swift.js +392 -0
- package/dist/scanner/types.d.ts +1 -1
- package/dist/template/content/docs/_navigation.json +46 -0
- package/dist/template/content/docs/_sidebars.json +684 -0
- package/dist/template/content/docs/core.md +4544 -0
- package/dist/template/content/docs/index.mdx +89 -0
- package/dist/template/content/docs/integrations.md +1158 -0
- package/dist/template/content/docs/llms-full.md +403 -0
- package/dist/template/content/docs/llms.txt +4588 -0
- package/dist/template/content/docs/other.md +10379 -0
- package/dist/template/content/docs/tools.md +746 -0
- package/dist/template/content/docs/types.md +531 -0
- package/dist/template/docs.json +13 -11
- package/dist/template/mdx-components.tsx +27 -2
- package/dist/template/package.json +6 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +84 -6
- package/dist/template/src/app/api/chat/route.ts +83 -128
- package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
- package/dist/template/src/app/docs/llms-full.md +151 -4
- package/dist/template/src/app/docs/llms.txt +2464 -847
- package/dist/template/src/app/docs/page.mdx +48 -38
- package/dist/template/src/app/layout.tsx +3 -1
- package/dist/template/src/app/page.tsx +22 -8
- package/dist/template/src/components/ai-chat.tsx +73 -64
- package/dist/template/src/components/breadcrumbs.tsx +21 -23
- package/dist/template/src/components/copy-button.tsx +13 -9
- package/dist/template/src/components/copy-page-button.tsx +54 -0
- package/dist/template/src/components/docs-layout.tsx +37 -25
- package/dist/template/src/components/header.tsx +51 -10
- package/dist/template/src/components/mdx/card.tsx +17 -3
- package/dist/template/src/components/mdx/code-block.tsx +13 -9
- package/dist/template/src/components/mdx/code-group.tsx +13 -8
- package/dist/template/src/components/mdx/heading.tsx +15 -2
- package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
- package/dist/template/src/components/mdx/index.tsx +2 -0
- package/dist/template/src/components/mdx/mermaid.tsx +110 -0
- package/dist/template/src/components/mdx/screenshot.tsx +150 -0
- package/dist/template/src/components/scroll-to-hash.tsx +48 -0
- package/dist/template/src/components/sidebar.tsx +12 -18
- package/dist/template/src/components/table-of-contents.tsx +9 -0
- package/dist/template/src/lib/highlight.ts +3 -88
- package/dist/template/src/lib/navigation.ts +159 -0
- package/dist/template/src/styles/globals.css +17 -6
- package/dist/utils/validation.d.ts +0 -3
- package/dist/utils/validation.js +0 -26
- package/package.json +3 -2
package/dist/auth/index.js
CHANGED
|
@@ -121,7 +121,14 @@ export async function checkPlan(apiKey) {
|
|
|
121
121
|
if (typeof data.valid !== 'boolean' || typeof data.email !== 'string') {
|
|
122
122
|
return { valid: false, plan: 'free', email: '', error: 'Invalid API response' };
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
const plan = data.plan === 'pro' ? 'pro' : 'free';
|
|
125
|
+
return {
|
|
126
|
+
valid: data.valid,
|
|
127
|
+
plan,
|
|
128
|
+
email: data.email,
|
|
129
|
+
expiresAt: typeof data.expiresAt === 'string' ? data.expiresAt : undefined,
|
|
130
|
+
error: typeof data.error === 'string' ? data.error : undefined,
|
|
131
|
+
};
|
|
125
132
|
}
|
|
126
133
|
catch {
|
|
127
134
|
return { valid: false, plan: 'free', email: '', error: 'Failed to connect to API' };
|
package/dist/autofix/index.d.ts
CHANGED
|
@@ -32,10 +32,6 @@ export interface ValidationResult {
|
|
|
32
32
|
* Auto-fix a code example using LLM
|
|
33
33
|
*/
|
|
34
34
|
export declare function autoFixExample(example: CodeExample, client: LLMClient, options?: AutoFixOptions): Promise<FixResult>;
|
|
35
|
-
/**
|
|
36
|
-
* Batch auto-fix multiple examples
|
|
37
|
-
*/
|
|
38
|
-
export declare function autoFixBatch(examples: CodeExample[], client: LLMClient, options?: AutoFixOptions): Promise<Map<number, FixResult>>;
|
|
39
35
|
/**
|
|
40
36
|
* Create a TypeScript validator using tsc
|
|
41
37
|
*/
|
package/dist/autofix/index.js
CHANGED
|
@@ -152,27 +152,6 @@ function extractCode(response, language) {
|
|
|
152
152
|
return null;
|
|
153
153
|
return joined;
|
|
154
154
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Batch auto-fix multiple examples
|
|
157
|
-
*/
|
|
158
|
-
export async function autoFixBatch(examples, client, options = {}) {
|
|
159
|
-
const results = new Map();
|
|
160
|
-
for (let i = 0; i < examples.length; i++) {
|
|
161
|
-
const example = examples[i];
|
|
162
|
-
if (!example)
|
|
163
|
-
continue;
|
|
164
|
-
console.log(` [${i + 1}/${examples.length}] Fixing ${example.language} example...`);
|
|
165
|
-
const result = await autoFixExample(example, client, options);
|
|
166
|
-
results.set(i, result);
|
|
167
|
-
if (result.success) {
|
|
168
|
-
console.log(` ✓ Fixed in ${result.iterations} iteration(s)`);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
console.log(` ✗ Could not fix after ${result.iterations} iteration(s)`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return results;
|
|
175
|
-
}
|
|
176
155
|
/**
|
|
177
156
|
* Create a TypeScript validator using tsc
|
|
178
157
|
*/
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright wrapper for screenshot capture.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imports Playwright (optional dependency).
|
|
5
|
+
* Only allows localhost URLs by default for security.
|
|
6
|
+
*/
|
|
7
|
+
import type { ScreenshotDirective, ScreenshotResult, CaptureOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Capture screenshots for a batch of directives.
|
|
10
|
+
*/
|
|
11
|
+
export declare function captureScreenshots(directives: ScreenshotDirective[], options: CaptureOptions): Promise<ScreenshotResult[]>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright wrapper for screenshot capture.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imports Playwright (optional dependency).
|
|
5
|
+
* Only allows localhost URLs by default for security.
|
|
6
|
+
*/
|
|
7
|
+
import { screenshotFilename, darkVariantFilename } from './naming.js';
|
|
8
|
+
import { computeHash } from './diff.js';
|
|
9
|
+
const ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
10
|
+
/**
|
|
11
|
+
* Dynamically import Playwright. Exits with clear instructions if not installed.
|
|
12
|
+
*/
|
|
13
|
+
async function ensurePlaywright() {
|
|
14
|
+
try {
|
|
15
|
+
// Dynamic import — playwright is optional
|
|
16
|
+
const mod = await Function('return import("playwright")')();
|
|
17
|
+
return mod;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('Playwright is required for screenshot capture. Install it: npx playwright install chromium');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a URL is on an allowed host (localhost by default).
|
|
25
|
+
*/
|
|
26
|
+
function validateUrl(url) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
return { valid: ALLOWED_HOSTS.has(parsed.hostname), host: parsed.hostname };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { valid: false, host: '' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Capture screenshots for a batch of directives.
|
|
37
|
+
*/
|
|
38
|
+
export async function captureScreenshots(directives, options) {
|
|
39
|
+
const pw = await ensurePlaywright();
|
|
40
|
+
const results = [];
|
|
41
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
42
|
+
try {
|
|
43
|
+
// Process with concurrency limit
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (let i = 0; i < directives.length; i += options.concurrency) {
|
|
46
|
+
chunks.push(directives.slice(i, i + options.concurrency));
|
|
47
|
+
}
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
const chunkResults = await Promise.all(chunk.map(directive => captureOne(browser, directive, options, pw)));
|
|
50
|
+
results.push(...chunkResults);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await browser.close();
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Capture a single screenshot.
|
|
60
|
+
*/
|
|
61
|
+
async function captureOne(browser, directive, options, pw) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
const filename = screenshotFilename(directive.url, directive.selector);
|
|
64
|
+
// Security check
|
|
65
|
+
const { valid, host } = validateUrl(directive.url);
|
|
66
|
+
if (!valid) {
|
|
67
|
+
return {
|
|
68
|
+
directive,
|
|
69
|
+
filename,
|
|
70
|
+
status: 'failed',
|
|
71
|
+
error: `URL host "${host}" not allowed. Only localhost, 127.0.0.1, and ::1 are permitted.`,
|
|
72
|
+
duration: Date.now() - start,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Build context options
|
|
76
|
+
const contextOpts = {
|
|
77
|
+
viewport: options.viewport,
|
|
78
|
+
};
|
|
79
|
+
// Device emulation
|
|
80
|
+
if (options.device) {
|
|
81
|
+
const device = pw.devices[options.device];
|
|
82
|
+
if (device) {
|
|
83
|
+
contextOpts.viewport = device.viewport;
|
|
84
|
+
contextOpts.userAgent = device.userAgent;
|
|
85
|
+
contextOpts.deviceScaleFactor = device.deviceScaleFactor;
|
|
86
|
+
contextOpts.isMobile = device.isMobile;
|
|
87
|
+
contextOpts.hasTouch = device.hasTouch;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const context = await browser.newContext(contextOpts);
|
|
91
|
+
const page = await context.newPage();
|
|
92
|
+
try {
|
|
93
|
+
// Navigate
|
|
94
|
+
await page.goto(directive.url, {
|
|
95
|
+
waitUntil: 'networkidle',
|
|
96
|
+
timeout: options.timeout,
|
|
97
|
+
});
|
|
98
|
+
// Wait for settle time
|
|
99
|
+
await page.waitForTimeout(options.wait);
|
|
100
|
+
// Capture light mode screenshot
|
|
101
|
+
let buffer;
|
|
102
|
+
if (directive.selector) {
|
|
103
|
+
const locator = page.locator(directive.selector);
|
|
104
|
+
const visible = await locator.isVisible();
|
|
105
|
+
if (!visible) {
|
|
106
|
+
return {
|
|
107
|
+
directive,
|
|
108
|
+
filename,
|
|
109
|
+
status: 'failed',
|
|
110
|
+
error: `Selector "${directive.selector}" not found or not visible on ${directive.url}`,
|
|
111
|
+
duration: Date.now() - start,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
buffer = await locator.screenshot({ type: 'png' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
buffer = await page.screenshot({ fullPage: true, type: 'png' });
|
|
118
|
+
}
|
|
119
|
+
const hash = computeHash(buffer);
|
|
120
|
+
const result = {
|
|
121
|
+
directive,
|
|
122
|
+
filename,
|
|
123
|
+
status: 'captured',
|
|
124
|
+
hash,
|
|
125
|
+
duration: Date.now() - start,
|
|
126
|
+
};
|
|
127
|
+
// Dark mode capture
|
|
128
|
+
if (options.dark) {
|
|
129
|
+
try {
|
|
130
|
+
await page.emulateMedia({ colorScheme: 'dark' });
|
|
131
|
+
await page.evaluate(() => {
|
|
132
|
+
document.documentElement.classList.add('dark');
|
|
133
|
+
});
|
|
134
|
+
await page.waitForTimeout(500);
|
|
135
|
+
let darkBuffer;
|
|
136
|
+
if (directive.selector) {
|
|
137
|
+
darkBuffer = await page.locator(directive.selector).screenshot({ type: 'png' });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
darkBuffer = await page.screenshot({ fullPage: true, type: 'png' });
|
|
141
|
+
}
|
|
142
|
+
result.darkFilename = darkVariantFilename(filename);
|
|
143
|
+
result._buffers = {
|
|
144
|
+
light: buffer,
|
|
145
|
+
dark: darkBuffer,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Dark mode capture failed — continue with light only
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Store light buffer for the orchestrator
|
|
153
|
+
if (!result._buffers) {
|
|
154
|
+
;
|
|
155
|
+
result._buffer = buffer;
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
161
|
+
return {
|
|
162
|
+
directive,
|
|
163
|
+
filename,
|
|
164
|
+
status: 'failed',
|
|
165
|
+
error: message,
|
|
166
|
+
duration: Date.now() - start,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await page.close();
|
|
171
|
+
await context.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based change detection for screenshots.
|
|
3
|
+
*
|
|
4
|
+
* Stores SHA-256 hashes in .skrypt/screenshot-hashes.json (project root).
|
|
5
|
+
* Used with --diff flag to skip re-capturing unchanged screenshots.
|
|
6
|
+
*/
|
|
7
|
+
export type HashStore = Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Compute SHA-256 hash of a buffer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeHash(buffer: Buffer): string;
|
|
12
|
+
/**
|
|
13
|
+
* Load the hash store from .skrypt/screenshot-hashes.json in the project directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadHashStore(projectDir: string): HashStore;
|
|
16
|
+
/**
|
|
17
|
+
* Save the hash store to .skrypt/screenshot-hashes.json in the project directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function saveHashStore(projectDir: string, store: HashStore): void;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a screenshot has changed based on its hash.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasChanged(filename: string, newHash: string, store: HashStore): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based change detection for screenshots.
|
|
3
|
+
*
|
|
4
|
+
* Stores SHA-256 hashes in .skrypt/screenshot-hashes.json (project root).
|
|
5
|
+
* Used with --diff flag to skip re-capturing unchanged screenshots.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
const HASH_FILE = 'screenshot-hashes.json';
|
|
11
|
+
const STORE_DIR = '.skrypt';
|
|
12
|
+
/**
|
|
13
|
+
* Compute SHA-256 hash of a buffer.
|
|
14
|
+
*/
|
|
15
|
+
export function computeHash(buffer) {
|
|
16
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load the hash store from .skrypt/screenshot-hashes.json in the project directory.
|
|
20
|
+
*/
|
|
21
|
+
export function loadHashStore(projectDir) {
|
|
22
|
+
const storePath = join(projectDir, STORE_DIR, HASH_FILE);
|
|
23
|
+
if (!existsSync(storePath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(storePath, 'utf-8');
|
|
28
|
+
const parsed = JSON.parse(content);
|
|
29
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Save the hash store to .skrypt/screenshot-hashes.json in the project directory.
|
|
40
|
+
*/
|
|
41
|
+
export function saveHashStore(projectDir, store) {
|
|
42
|
+
const storeDir = join(projectDir, STORE_DIR);
|
|
43
|
+
mkdirSync(storeDir, { recursive: true });
|
|
44
|
+
const storePath = join(storeDir, HASH_FILE);
|
|
45
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a screenshot has changed based on its hash.
|
|
49
|
+
*/
|
|
50
|
+
export function hasChanged(filename, newHash, store) {
|
|
51
|
+
return store[filename] !== newHash;
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot capture orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates MDX parsing, browser capture, diff detection, and file output.
|
|
5
|
+
*/
|
|
6
|
+
import type { CaptureOptions, CaptureReport } from './types.js';
|
|
7
|
+
export type { CaptureOptions, CaptureReport } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Run the screenshot capture pipeline.
|
|
10
|
+
*
|
|
11
|
+
* 1. Find all MDX files in docsDir
|
|
12
|
+
* 2. Parse <Screenshot> directives from each
|
|
13
|
+
* 3. Validate base URL reachability
|
|
14
|
+
* 4. Capture screenshots via headless browser
|
|
15
|
+
* 5. Compare hashes for --diff mode
|
|
16
|
+
* 6. Write changed files to outputDir
|
|
17
|
+
* 7. Write manifest and hash store
|
|
18
|
+
*/
|
|
19
|
+
export declare function runCapture(docsDir: string, options: CaptureOptions): Promise<CaptureReport>;
|
|
20
|
+
/**
|
|
21
|
+
* Print a capture report to stdout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function printCaptureReport(report: CaptureReport): void;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot capture orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates MDX parsing, browser capture, diff detection, and file output.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join, resolve } from 'path';
|
|
8
|
+
import { findMdxFiles } from '../utils/files.js';
|
|
9
|
+
import { parseScreenshotDirectives } from './parser.js';
|
|
10
|
+
import { captureScreenshots } from './browser.js';
|
|
11
|
+
import { loadHashStore, saveHashStore, computeHash, hasChanged } from './diff.js';
|
|
12
|
+
/**
|
|
13
|
+
* Run the screenshot capture pipeline.
|
|
14
|
+
*
|
|
15
|
+
* 1. Find all MDX files in docsDir
|
|
16
|
+
* 2. Parse <Screenshot> directives from each
|
|
17
|
+
* 3. Validate base URL reachability
|
|
18
|
+
* 4. Capture screenshots via headless browser
|
|
19
|
+
* 5. Compare hashes for --diff mode
|
|
20
|
+
* 6. Write changed files to outputDir
|
|
21
|
+
* 7. Write manifest and hash store
|
|
22
|
+
*/
|
|
23
|
+
export async function runCapture(docsDir, options) {
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
const resolvedDocsDir = resolve(docsDir);
|
|
26
|
+
const resolvedOutputDir = resolve(options.outputDir);
|
|
27
|
+
// 1. Find MDX files
|
|
28
|
+
const mdxFiles = findMdxFiles(resolvedDocsDir);
|
|
29
|
+
// 2. Parse directives
|
|
30
|
+
const allDirectives = mdxFiles.flatMap(filePath => {
|
|
31
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
32
|
+
return parseScreenshotDirectives(content, filePath, options.baseUrl);
|
|
33
|
+
});
|
|
34
|
+
if (allDirectives.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
directives: 0,
|
|
37
|
+
captured: 0,
|
|
38
|
+
unchanged: 0,
|
|
39
|
+
failed: 0,
|
|
40
|
+
duration: Date.now() - startTime,
|
|
41
|
+
results: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// 3. Dry run
|
|
45
|
+
if (options.dryRun) {
|
|
46
|
+
console.log(`\n Found ${allDirectives.length} screenshot directive(s):`);
|
|
47
|
+
for (const d of allDirectives) {
|
|
48
|
+
const sel = d.selector ? ` (${d.selector})` : ' (full page)';
|
|
49
|
+
console.log(` ${d.url}${sel} → ${d.alt}`);
|
|
50
|
+
}
|
|
51
|
+
console.log('\n [dry run — no files written]\n');
|
|
52
|
+
return {
|
|
53
|
+
directives: allDirectives.length,
|
|
54
|
+
captured: 0,
|
|
55
|
+
unchanged: 0,
|
|
56
|
+
failed: 0,
|
|
57
|
+
duration: Date.now() - startTime,
|
|
58
|
+
results: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// 4. Validate base URL reachability
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(options.baseUrl, {
|
|
64
|
+
method: 'HEAD',
|
|
65
|
+
signal: AbortSignal.timeout(5000),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
console.error(`\n Base URL ${options.baseUrl} returned ${response.status}`);
|
|
69
|
+
console.error(' Make sure your dev server is running.\n');
|
|
70
|
+
return {
|
|
71
|
+
directives: allDirectives.length,
|
|
72
|
+
captured: 0,
|
|
73
|
+
unchanged: 0,
|
|
74
|
+
failed: allDirectives.length,
|
|
75
|
+
duration: Date.now() - startTime,
|
|
76
|
+
results: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
console.error(`\n Cannot reach ${options.baseUrl}`);
|
|
82
|
+
console.error(' Start your dev server first, then run heal again.\n');
|
|
83
|
+
return {
|
|
84
|
+
directives: allDirectives.length,
|
|
85
|
+
captured: 0,
|
|
86
|
+
unchanged: 0,
|
|
87
|
+
failed: allDirectives.length,
|
|
88
|
+
duration: Date.now() - startTime,
|
|
89
|
+
results: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// 5. Load hash store for diff mode
|
|
93
|
+
const projectDir = resolvedDocsDir;
|
|
94
|
+
const hashStore = options.diff ? loadHashStore(projectDir) : {};
|
|
95
|
+
// 6. Capture all screenshots
|
|
96
|
+
console.log(`\n Capturing ${allDirectives.length} screenshot(s)...`);
|
|
97
|
+
const results = await captureScreenshots(allDirectives, options);
|
|
98
|
+
// 7. Process results — write files, check diffs
|
|
99
|
+
mkdirSync(resolvedOutputDir, { recursive: true });
|
|
100
|
+
let captured = 0;
|
|
101
|
+
let unchanged = 0;
|
|
102
|
+
let failed = 0;
|
|
103
|
+
const newHashStore = { ...hashStore };
|
|
104
|
+
const manifestEntries = {};
|
|
105
|
+
for (const result of results) {
|
|
106
|
+
if (result.status === 'failed') {
|
|
107
|
+
failed++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const lightBuffer = getBuffer(result, 'light');
|
|
111
|
+
if (!lightBuffer) {
|
|
112
|
+
failed++;
|
|
113
|
+
result.status = 'failed';
|
|
114
|
+
result.error = 'No screenshot data captured';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const hash = computeHash(lightBuffer);
|
|
118
|
+
// Diff check
|
|
119
|
+
if (options.diff && !hasChanged(result.filename, hash, hashStore)) {
|
|
120
|
+
unchanged++;
|
|
121
|
+
result.status = 'unchanged';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Write light screenshot
|
|
125
|
+
const outputPath = join(resolvedOutputDir, result.filename);
|
|
126
|
+
writeFileSync(outputPath, lightBuffer);
|
|
127
|
+
newHashStore[result.filename] = hash;
|
|
128
|
+
captured++;
|
|
129
|
+
// Write dark variant if present
|
|
130
|
+
if (result.darkFilename) {
|
|
131
|
+
const darkBuffer = getBuffer(result, 'dark');
|
|
132
|
+
if (darkBuffer) {
|
|
133
|
+
const darkPath = join(resolvedOutputDir, result.darkFilename);
|
|
134
|
+
writeFileSync(darkPath, darkBuffer);
|
|
135
|
+
newHashStore[result.darkFilename] = computeHash(darkBuffer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Build manifest entry
|
|
139
|
+
manifestEntries[result.filename] = {
|
|
140
|
+
url: result.directive.url,
|
|
141
|
+
selector: result.directive.selector,
|
|
142
|
+
filename: result.filename,
|
|
143
|
+
darkFilename: result.darkFilename,
|
|
144
|
+
capturedAt: new Date().toISOString(),
|
|
145
|
+
hash,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// 8. Write manifest
|
|
149
|
+
const manifest = {
|
|
150
|
+
version: 1,
|
|
151
|
+
capturedAt: new Date().toISOString(),
|
|
152
|
+
baseUrl: options.baseUrl,
|
|
153
|
+
viewport: options.viewport,
|
|
154
|
+
screenshots: manifestEntries,
|
|
155
|
+
};
|
|
156
|
+
writeFileSync(join(resolvedOutputDir, '_manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
|
|
157
|
+
// 9. Save hash store
|
|
158
|
+
if (options.diff) {
|
|
159
|
+
saveHashStore(projectDir, newHashStore);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
directives: allDirectives.length,
|
|
163
|
+
captured,
|
|
164
|
+
unchanged,
|
|
165
|
+
failed,
|
|
166
|
+
duration: Date.now() - startTime,
|
|
167
|
+
results,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract buffer from result (stored as internal _buffer or _buffers property).
|
|
172
|
+
*/
|
|
173
|
+
function getBuffer(result, variant) {
|
|
174
|
+
const r = result;
|
|
175
|
+
if (r._buffers) {
|
|
176
|
+
return variant === 'light' ? r._buffers.light : r._buffers.dark;
|
|
177
|
+
}
|
|
178
|
+
if (variant === 'light' && r._buffer) {
|
|
179
|
+
return r._buffer;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Print a capture report to stdout.
|
|
185
|
+
*/
|
|
186
|
+
export function printCaptureReport(report) {
|
|
187
|
+
if (report.directives === 0) {
|
|
188
|
+
console.log('\n No <Screenshot> directives found in docs.\n');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (report.captured > 0)
|
|
193
|
+
parts.push(`${report.captured} captured`);
|
|
194
|
+
if (report.unchanged > 0)
|
|
195
|
+
parts.push(`${report.unchanged} unchanged`);
|
|
196
|
+
if (report.failed > 0)
|
|
197
|
+
parts.push(`${report.failed} failed`);
|
|
198
|
+
console.log(`\n Screenshots: ${parts.join(', ')} (${report.duration}ms)`);
|
|
199
|
+
// Show failures
|
|
200
|
+
const failures = report.results.filter(r => r.status === 'failed');
|
|
201
|
+
for (const f of failures) {
|
|
202
|
+
const loc = f.directive.selector
|
|
203
|
+
? `${f.directive.url} (${f.directive.selector})`
|
|
204
|
+
: f.directive.url;
|
|
205
|
+
console.log(` ✗ ${loc}: ${f.error}`);
|
|
206
|
+
}
|
|
207
|
+
if (report.captured > 0) {
|
|
208
|
+
console.log('');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic screenshot filename generation.
|
|
3
|
+
*
|
|
4
|
+
* Derives stable, human-readable filenames from URL + optional CSS selector.
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* /dashboard + .billing-panel → dashboard--billing-panel.png
|
|
8
|
+
* /settings/account + #profile → settings-account--profile.png
|
|
9
|
+
* /dashboard (no selector) → dashboard.png
|
|
10
|
+
*/
|
|
11
|
+
export declare function screenshotFilename(url: string, selector?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Dark mode variant filename.
|
|
14
|
+
* dashboard.png → dashboard-dark.png
|
|
15
|
+
* dashboard--panel.png → dashboard--panel-dark.png
|
|
16
|
+
*/
|
|
17
|
+
export declare function darkVariantFilename(filename: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic screenshot filename generation.
|
|
3
|
+
*
|
|
4
|
+
* Derives stable, human-readable filenames from URL + optional CSS selector.
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* /dashboard + .billing-panel → dashboard--billing-panel.png
|
|
8
|
+
* /settings/account + #profile → settings-account--profile.png
|
|
9
|
+
* /dashboard (no selector) → dashboard.png
|
|
10
|
+
*/
|
|
11
|
+
export function screenshotFilename(url, selector) {
|
|
12
|
+
// Extract pathname from full URL or use as-is if just a path
|
|
13
|
+
let pathname;
|
|
14
|
+
try {
|
|
15
|
+
pathname = new URL(url).pathname;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
pathname = url;
|
|
19
|
+
}
|
|
20
|
+
// Strip leading /
|
|
21
|
+
let slug = pathname.replace(/^\/+/, '');
|
|
22
|
+
// Replace / with -
|
|
23
|
+
slug = slug.replace(/\//g, '-');
|
|
24
|
+
// If empty (root path), use "index"
|
|
25
|
+
if (!slug)
|
|
26
|
+
slug = 'index';
|
|
27
|
+
// Sanitize: only keep alphanumeric and hyphens
|
|
28
|
+
slug = slug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
29
|
+
if (selector) {
|
|
30
|
+
// Strip CSS selector prefix (. or #)
|
|
31
|
+
let selectorSlug = selector.replace(/^[.#]/, '');
|
|
32
|
+
// Sanitize selector the same way
|
|
33
|
+
selectorSlug = selectorSlug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
34
|
+
slug = `${slug}--${selectorSlug}`;
|
|
35
|
+
}
|
|
36
|
+
return `${slug}.png`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Dark mode variant filename.
|
|
40
|
+
* dashboard.png → dashboard-dark.png
|
|
41
|
+
* dashboard--panel.png → dashboard--panel-dark.png
|
|
42
|
+
*/
|
|
43
|
+
export function darkVariantFilename(filename) {
|
|
44
|
+
return filename.replace(/\.png$/, '-dark.png');
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract <Screenshot> directives from MDX files.
|
|
3
|
+
*
|
|
4
|
+
* Scans MDX content for <Screenshot> tags outside code blocks.
|
|
5
|
+
* Uses the same code-block-aware scanning pattern as src/qa/checks.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { ScreenshotDirective } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse all <Screenshot> directives from MDX content.
|
|
10
|
+
*
|
|
11
|
+
* Supports both self-closing (<Screenshot ... />) and open/close tags.
|
|
12
|
+
* Extracts url, selector, alt, caption props via regex.
|
|
13
|
+
* Resolves relative URLs against baseUrl.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseScreenshotDirectives(content: string, filePath: string, baseUrl: string): ScreenshotDirective[];
|