slides-grab 1.0.0 → 1.1.2

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.
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, readdir, rm, access } from 'node:fs/promises';
4
+ import { constants as fsConstants } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { homedir } from 'node:os';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const packageRoot = resolve(__dirname, '..');
12
+ const sourceRoot = join(packageRoot, 'skills');
13
+
14
+ function parseArgs(argv) {
15
+ const opts = {
16
+ force: false,
17
+ dryRun: false,
18
+ };
19
+
20
+ for (const arg of argv) {
21
+ if (arg === '--force') {
22
+ opts.force = true;
23
+ continue;
24
+ }
25
+ if (arg === '--dry-run') {
26
+ opts.dryRun = true;
27
+ continue;
28
+ }
29
+ if (arg === '-h' || arg === '--help') {
30
+ opts.help = true;
31
+ continue;
32
+ }
33
+ throw new Error(`Unknown option: ${arg}`);
34
+ }
35
+
36
+ return opts;
37
+ }
38
+
39
+ async function pathExists(path) {
40
+ try {
41
+ await access(path, fsConstants.F_OK);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function usage() {
49
+ process.stdout.write('Usage: node scripts/install-codex-skills.js [--force] [--dry-run]\n');
50
+ }
51
+
52
+ async function listSkillDirs() {
53
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
54
+ const names = [];
55
+
56
+ for (const entry of entries) {
57
+ if (!entry.isDirectory()) continue;
58
+ const skillPath = join(sourceRoot, entry.name, 'SKILL.md');
59
+ if (await pathExists(skillPath)) {
60
+ names.push(entry.name);
61
+ }
62
+ }
63
+
64
+ return names.sort((a, b) => a.localeCompare(b));
65
+ }
66
+
67
+ async function main() {
68
+ const opts = parseArgs(process.argv.slice(2));
69
+ if (opts.help) {
70
+ usage();
71
+ return;
72
+ }
73
+
74
+ const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
75
+ const targetRoot = join(codexHome, 'skills');
76
+
77
+ const skillDirs = await listSkillDirs();
78
+ if (skillDirs.length === 0) {
79
+ process.stdout.write('[codex-skills] No skills found under ./skills\n');
80
+ return;
81
+ }
82
+
83
+ process.stdout.write(`[codex-skills] Source: ${sourceRoot}\n`);
84
+ process.stdout.write(`[codex-skills] Target: ${targetRoot}\n`);
85
+
86
+ if (opts.dryRun) {
87
+ for (const name of skillDirs) {
88
+ process.stdout.write(`[dry-run] would install: ${name}\n`);
89
+ }
90
+ return;
91
+ }
92
+
93
+ await mkdir(targetRoot, { recursive: true });
94
+
95
+ for (const name of skillDirs) {
96
+ const src = join(sourceRoot, name);
97
+ const dest = join(targetRoot, name);
98
+
99
+ const exists = await pathExists(dest);
100
+ if (exists && !opts.force) {
101
+ process.stdout.write(`[skip] ${name} already exists (use --force to overwrite)\n`);
102
+ continue;
103
+ }
104
+
105
+ if (exists && opts.force) {
106
+ await rm(dest, { recursive: true, force: true });
107
+ }
108
+
109
+ await cp(src, dest, { recursive: true });
110
+ process.stdout.write(`[install] ${name}\n`);
111
+ }
112
+
113
+ process.stdout.write('[codex-skills] Done. Restart Codex to pick up new skills.\n');
114
+ }
115
+
116
+ main().catch((error) => {
117
+ process.stderr.write(`[codex-skills] ${error.message}\n`);
118
+ process.exit(1);
119
+ });
@@ -1,340 +1,167 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readdir } from 'node:fs/promises';
4
- import { join, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
6
5
  import { chromium } from 'playwright';
7
6
 
8
- const FRAME_PT = { width: 720, height: 405 };
9
- const PT_TO_PX = 96 / 72;
10
- const FRAME_PX = {
11
- width: FRAME_PT.width * PT_TO_PX,
12
- height: FRAME_PT.height * PT_TO_PX
7
+ import {
8
+ DEFAULT_SLIDES_DIR,
9
+ DEFAULT_VALIDATE_FORMAT,
10
+ getValidateUsage,
11
+ parseValidateCliArgs,
12
+ } from '../src/validation/cli.js';
13
+ import {
14
+ createValidationFailure,
15
+ createValidationResult,
16
+ ensureSlidesPassValidation,
17
+ findSlideFiles,
18
+ formatValidationFailureForExport,
19
+ scanSlides,
20
+ selectSlideFiles,
21
+ } from '../src/validation/core.js';
22
+
23
+ export {
24
+ DEFAULT_SLIDES_DIR,
25
+ ensureSlidesPassValidation,
26
+ findSlideFiles,
27
+ formatValidationFailureForExport,
28
+ parseValidateCliArgs as parseCliArgs,
13
29
  };
14
- const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
15
- const TEXT_SELECTOR = 'p,h1,h2,h3,h4,h5,h6,li';
16
- const TOLERANCE_PX = 0.5;
17
- const DEFAULT_SLIDES_DIR = 'slides';
18
30
 
19
- function toSlideOrder(fileName) {
20
- const match = fileName.match(/\d+/);
21
- return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
22
- }
23
-
24
- function sortSlideFiles(a, b) {
25
- const orderA = toSlideOrder(a);
26
- const orderB = toSlideOrder(b);
27
- if (orderA !== orderB) return orderA - orderB;
28
- return a.localeCompare(b);
29
- }
30
-
31
- function buildIssue(code, message, payload = {}) {
32
- return { code, message, ...payload };
33
- }
34
-
35
- function summarizeSlides(slides) {
36
- const summary = {
37
- totalSlides: slides.length,
38
- passedSlides: 0,
39
- failedSlides: 0,
40
- criticalIssues: 0,
41
- warnings: 0
42
- };
31
+ export function flattenValidationResult(result) {
32
+ const diagnostics = [];
33
+
34
+ for (const slide of result.slides || []) {
35
+ for (const issue of slide.critical || []) {
36
+ diagnostics.push({
37
+ slide: slide.slide,
38
+ severity: 'error',
39
+ code: issue.code,
40
+ message: issue.message,
41
+ location: issue.element || issue.parent || undefined,
42
+ related: Array.isArray(issue.elements) ? issue.elements : undefined,
43
+ source: issue.source,
44
+ assetPath: issue.assetPath,
45
+ detail: issue.detail,
46
+ metrics: issue.metrics,
47
+ bbox: issue.bbox
48
+ ? {
49
+ x: issue.bbox.x,
50
+ y: issue.bbox.y,
51
+ width: issue.bbox.width,
52
+ height: issue.bbox.height,
53
+ }
54
+ : undefined,
55
+ intersection: issue.intersection,
56
+ });
57
+ }
43
58
 
44
- for (const slide of slides) {
45
- if (slide.status === 'pass') {
46
- summary.passedSlides += 1;
47
- } else {
48
- summary.failedSlides += 1;
59
+ for (const issue of slide.warning || []) {
60
+ diagnostics.push({
61
+ slide: slide.slide,
62
+ severity: 'warning',
63
+ code: issue.code,
64
+ message: issue.message,
65
+ location: issue.element || issue.parent || undefined,
66
+ related: Array.isArray(issue.elements) ? issue.elements : undefined,
67
+ source: issue.source,
68
+ assetPath: issue.assetPath,
69
+ detail: issue.detail,
70
+ metrics: issue.metrics,
71
+ bbox: issue.bbox
72
+ ? {
73
+ x: issue.bbox.x,
74
+ y: issue.bbox.y,
75
+ width: issue.bbox.width,
76
+ height: issue.bbox.height,
77
+ }
78
+ : undefined,
79
+ intersection: issue.intersection,
80
+ });
49
81
  }
50
- summary.criticalIssues += slide.summary.criticalCount;
51
- summary.warnings += slide.summary.warningCount;
52
82
  }
53
83
 
54
- return summary;
55
- }
56
-
57
- function printUsage() {
58
- process.stdout.write(
59
- [
60
- 'Usage: node scripts/validate-slides.js [options]',
61
- '',
62
- 'Options:',
63
- ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
64
- ' -h, --help Show this help message',
65
- ].join('\n'),
66
- );
67
- process.stdout.write('\n');
84
+ return {
85
+ schemaVersion: 1,
86
+ generatedAt: result.generatedAt,
87
+ summary: {
88
+ totalSlides: result.summary?.totalSlides ?? 0,
89
+ passedSlides: result.summary?.passedSlides ?? 0,
90
+ failedSlides: result.summary?.failedSlides ?? 0,
91
+ errors: result.summary?.criticalIssues ?? 0,
92
+ warnings: result.summary?.warnings ?? 0,
93
+ },
94
+ diagnostics,
95
+ ...(result.error ? { error: result.error } : {}),
96
+ };
68
97
  }
69
98
 
70
- function readOptionValue(args, index, optionName) {
71
- const next = args[index + 1];
72
- if (!next || next.startsWith('-')) {
73
- throw new Error(`Missing value for ${optionName}.`);
99
+ function formatDiagnosticLine(diagnostic) {
100
+ const target = diagnostic.location
101
+ || (diagnostic.related && diagnostic.related.length > 0 ? diagnostic.related.join(' <> ') : '')
102
+ || diagnostic.source
103
+ || '';
104
+
105
+ const extra = [];
106
+ if (diagnostic.source) extra.push(`source=${diagnostic.source}`);
107
+ if (diagnostic.assetPath) extra.push(`assetPath=${diagnostic.assetPath}`);
108
+ if (diagnostic.metrics) {
109
+ extra.push(
110
+ Object.entries(diagnostic.metrics)
111
+ .map(([key, value]) => `${key}=${value}`)
112
+ .join(' '),
113
+ );
74
114
  }
75
- return next;
76
- }
77
-
78
- function parseCliArgs(args) {
79
- const options = {
80
- slidesDir: DEFAULT_SLIDES_DIR,
81
- help: false,
82
- };
83
-
84
- for (let i = 0; i < args.length; i += 1) {
85
- const arg = args[i];
86
-
87
- if (arg === '-h' || arg === '--help') {
88
- options.help = true;
89
- continue;
90
- }
91
-
92
- if (arg === '--slides-dir') {
93
- options.slidesDir = readOptionValue(args, i, '--slides-dir');
94
- i += 1;
95
- continue;
96
- }
97
-
98
- if (arg.startsWith('--slides-dir=')) {
99
- options.slidesDir = arg.slice('--slides-dir='.length);
100
- continue;
101
- }
102
-
103
- throw new Error(`Unknown option: ${arg}`);
115
+ if (diagnostic.bbox) {
116
+ extra.push(`bbox=${diagnostic.bbox.x},${diagnostic.bbox.y} ${diagnostic.bbox.width}x${diagnostic.bbox.height}`);
104
117
  }
105
-
106
- if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
107
- throw new Error('--slides-dir must be a non-empty string.');
118
+ if (diagnostic.intersection) {
119
+ extra.push(
120
+ `intersection=${diagnostic.intersection.x},${diagnostic.intersection.y} ${diagnostic.intersection.width}x${diagnostic.intersection.height}`,
121
+ );
108
122
  }
123
+ if (diagnostic.detail) extra.push(`detail=${diagnostic.detail}`);
109
124
 
110
- options.slidesDir = options.slidesDir.trim();
111
- return options;
112
- }
113
-
114
- async function findSlideFiles(slidesDir) {
115
- const entries = await readdir(slidesDir, { withFileTypes: true });
116
- return entries
117
- .filter((entry) => entry.isFile() && SLIDE_FILE_PATTERN.test(entry.name))
118
- .map((entry) => entry.name)
119
- .sort(sortSlideFiles);
125
+ const targetSuffix = target ? ` ${target}` : '';
126
+ const extraSuffix = extra.length > 0 ? ` (${extra.join('; ')})` : '';
127
+ return `${diagnostic.slide}:${diagnostic.severity}[${diagnostic.code}]${targetSuffix} - ${diagnostic.message}${extraSuffix}`;
120
128
  }
121
129
 
122
- async function inspectSlide(page, fileName, slidesDir) {
123
- const slidePath = join(slidesDir, fileName);
124
- const slideUrl = pathToFileURL(slidePath).href;
125
-
126
- await page.goto(slideUrl, { waitUntil: 'load' });
127
- await page.evaluate(async () => {
128
- if (document.fonts?.ready) {
129
- await document.fonts.ready;
130
- }
131
- });
132
-
133
- const inspection = await page.evaluate(
134
- ({ framePx, textSelector, tolerancePx }) => {
135
- const skipTags = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'TITLE', 'NOSCRIPT']);
136
- const critical = [];
137
- const warning = [];
138
- const seenOverlaps = new Set();
139
-
140
- const round = (value) => Number(value.toFixed(2));
141
-
142
- const normalizeRect = (rect) => {
143
- const left = rect.left ?? rect.x ?? 0;
144
- const top = rect.top ?? rect.y ?? 0;
145
- const width = rect.width ?? (rect.right - left) ?? 0;
146
- const height = rect.height ?? (rect.bottom - top) ?? 0;
147
- const right = rect.right ?? (left + width);
148
- const bottom = rect.bottom ?? (top + height);
149
- return {
150
- x: round(left),
151
- y: round(top),
152
- width: round(width),
153
- height: round(height),
154
- left: round(left),
155
- top: round(top),
156
- right: round(right),
157
- bottom: round(bottom)
158
- };
159
- };
160
-
161
- const elementPath = (element) => {
162
- if (!element || element.nodeType !== Node.ELEMENT_NODE) return '';
163
- if (element === document.body) return 'body';
164
-
165
- const parts = [];
166
- let current = element;
167
-
168
- while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
169
- let part = current.tagName.toLowerCase();
170
- if (current.id) {
171
- part += `#${current.id}`;
172
- parts.unshift(part);
173
- break;
174
- }
175
-
176
- const classNames = Array.from(current.classList).slice(0, 2);
177
- if (classNames.length > 0) {
178
- part += `.${classNames.join('.')}`;
179
- }
180
-
181
- if (current.parentElement) {
182
- const siblingsOfSameTag = Array.from(current.parentElement.children)
183
- .filter((sibling) => sibling.tagName === current.tagName);
184
- if (siblingsOfSameTag.length > 1) {
185
- const index = siblingsOfSameTag.indexOf(current);
186
- part += `:nth-of-type(${index + 1})`;
187
- }
188
- }
189
-
190
- parts.unshift(part);
191
- current = current.parentElement;
192
- }
193
-
194
- return `body > ${parts.join(' > ')}`;
195
- };
196
-
197
- const isVisible = (element) => {
198
- if (skipTags.has(element.tagName)) return false;
199
- const style = window.getComputedStyle(element);
200
- if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
201
- return false;
202
- }
203
- const rect = element.getBoundingClientRect();
204
- return rect.width > 0 && rect.height > 0;
205
- };
206
-
207
- const bodyRect = document.body.getBoundingClientRect();
208
- const frameRect = {
209
- left: bodyRect.left,
210
- top: bodyRect.top,
211
- right: bodyRect.left + (bodyRect.width || framePx.width),
212
- bottom: bodyRect.top + (bodyRect.height || framePx.height),
213
- width: bodyRect.width || framePx.width,
214
- height: bodyRect.height || framePx.height
215
- };
216
-
217
- const allVisibleElements = Array.from(document.body.querySelectorAll('*')).filter(isVisible);
218
- const visibleSet = new Set(allVisibleElements);
219
-
220
- for (const element of allVisibleElements) {
221
- const rect = element.getBoundingClientRect();
222
- const outsideFrame = (
223
- rect.left < frameRect.left - tolerancePx ||
224
- rect.top < frameRect.top - tolerancePx ||
225
- rect.right > frameRect.right + tolerancePx ||
226
- rect.bottom > frameRect.bottom + tolerancePx
227
- );
228
-
229
- if (outsideFrame) {
230
- critical.push({
231
- code: 'overflow-outside-frame',
232
- message: 'Element exceeds the 720pt x 405pt slide frame.',
233
- element: elementPath(element),
234
- bbox: normalizeRect(rect),
235
- frame: normalizeRect(frameRect)
236
- });
237
- }
238
- }
239
-
240
- const textElements = Array.from(document.querySelectorAll(textSelector));
241
- for (const element of textElements) {
242
- if (!isVisible(element)) continue;
243
- const content = (element.textContent || '').trim();
244
- if (!content) continue;
245
-
246
- const clipped = element.scrollHeight > element.clientHeight;
247
- if (!clipped) continue;
248
-
249
- critical.push({
250
- code: 'text-clipped',
251
- message: 'Text element is clipped because scrollHeight is larger than clientHeight.',
252
- element: elementPath(element),
253
- metrics: {
254
- scrollHeight: element.scrollHeight,
255
- clientHeight: element.clientHeight
256
- },
257
- bbox: normalizeRect(element.getBoundingClientRect())
258
- });
259
- }
260
-
261
- const parents = [document.body, ...allVisibleElements];
262
- for (const parent of parents) {
263
- const children = Array.from(parent.children).filter((child) => visibleSet.has(child));
264
- if (children.length < 2) continue;
265
-
266
- for (let i = 0; i < children.length; i += 1) {
267
- for (let j = i + 1; j < children.length; j += 1) {
268
- const first = children[i];
269
- const second = children[j];
270
-
271
- const rectA = first.getBoundingClientRect();
272
- const rectB = second.getBoundingClientRect();
273
-
274
- const overlapWidth = Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left);
275
- const overlapHeight = Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top);
276
-
277
- if (overlapWidth <= tolerancePx || overlapHeight <= tolerancePx) continue;
278
-
279
- const firstPath = elementPath(first);
280
- const secondPath = elementPath(second);
281
- const overlapKey = [firstPath, secondPath].sort().join('::');
282
-
283
- if (seenOverlaps.has(overlapKey)) continue;
284
- seenOverlaps.add(overlapKey);
130
+ export function formatValidationResult(result, format = DEFAULT_VALIDATE_FORMAT) {
131
+ if (format === 'json-full') {
132
+ return `${JSON.stringify(result, null, 2)}\n`;
133
+ }
285
134
 
286
- warning.push({
287
- code: 'sibling-overlap',
288
- message: 'Sibling elements overlap in their bounding boxes.',
289
- parent: elementPath(parent),
290
- elements: [firstPath, secondPath],
291
- intersection: {
292
- x: round(Math.max(rectA.left, rectB.left)),
293
- y: round(Math.max(rectA.top, rectB.top)),
294
- width: round(overlapWidth),
295
- height: round(overlapHeight)
296
- },
297
- boxes: [normalizeRect(rectA), normalizeRect(rectB)]
298
- });
299
- }
300
- }
301
- }
135
+ const flattened = flattenValidationResult(result);
136
+ if (format === 'json') {
137
+ return `${JSON.stringify(flattened, null, 2)}\n`;
138
+ }
302
139
 
303
- return {
304
- critical,
305
- warning
306
- };
307
- },
308
- {
309
- framePx: FRAME_PX,
310
- textSelector: TEXT_SELECTOR,
311
- tolerancePx: TOLERANCE_PX
312
- }
140
+ const lines = flattened.diagnostics.map(formatDiagnosticLine);
141
+ if (flattened.error) {
142
+ lines.push(`validation:error[validation-failed] - ${flattened.error}`);
143
+ }
144
+ lines.push(
145
+ `summary: ${flattened.summary.totalSlides} slide(s) checked, ${flattened.summary.passedSlides} passed, ${flattened.summary.failedSlides} failed, ${flattened.summary.errors} error(s), ${flattened.summary.warnings} warning(s)`,
313
146
  );
314
-
315
- const summary = {
316
- criticalCount: inspection.critical.length,
317
- warningCount: inspection.warning.length
318
- };
319
-
320
- return {
321
- slide: fileName,
322
- status: summary.criticalCount > 0 ? 'fail' : 'pass',
323
- critical: inspection.critical,
324
- warning: inspection.warning,
325
- summary
326
- };
147
+ return `${lines.join('\n')}\n`;
327
148
  }
328
149
 
329
- async function main() {
330
- const options = parseCliArgs(process.argv.slice(2));
331
- if (options.help) {
332
- printUsage();
333
- return;
150
+ function peekValidateFormat(args = []) {
151
+ for (let i = 0; i < args.length; i += 1) {
152
+ const arg = args[i];
153
+ if (arg === '--format') {
154
+ return args[i + 1] || DEFAULT_VALIDATE_FORMAT;
155
+ }
156
+ if (arg.startsWith('--format=')) {
157
+ return arg.slice('--format='.length) || DEFAULT_VALIDATE_FORMAT;
158
+ }
334
159
  }
160
+ return DEFAULT_VALIDATE_FORMAT;
161
+ }
335
162
 
336
- const slidesDir = resolve(process.cwd(), options.slidesDir);
337
- const slideFiles = await findSlideFiles(slidesDir);
163
+ export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
164
+ const slideFiles = selectSlideFiles(await findSlideFiles(slidesDir), selectedSlides, slidesDir);
338
165
  if (slideFiles.length === 0) {
339
166
  throw new Error(`No slide-*.html files found in: ${slidesDir}`);
340
167
  }
@@ -343,74 +170,35 @@ async function main() {
343
170
  const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
344
171
  const page = await context.newPage();
345
172
 
346
- const slides = [];
347
173
  try {
348
- for (const slideFile of slideFiles) {
349
- try {
350
- const result = await inspectSlide(page, slideFile, slidesDir);
351
- slides.push(result);
352
- } catch (error) {
353
- slides.push({
354
- slide: slideFile,
355
- status: 'fail',
356
- critical: [
357
- buildIssue(
358
- 'slide-validation-error',
359
- 'Slide validation failed before checks could complete.',
360
- { detail: error instanceof Error ? error.message : String(error) }
361
- )
362
- ],
363
- warning: [],
364
- summary: {
365
- criticalCount: 1,
366
- warningCount: 0
367
- }
368
- });
369
- }
370
- }
174
+ const slides = await scanSlides(page, slidesDir, slideFiles);
175
+ return createValidationResult(slides);
371
176
  } finally {
372
177
  await browser.close();
373
178
  }
179
+ }
374
180
 
375
- const result = {
376
- generatedAt: new Date().toISOString(),
377
- frame: {
378
- widthPt: FRAME_PT.width,
379
- heightPt: FRAME_PT.height,
380
- widthPx: FRAME_PX.width,
381
- heightPx: FRAME_PX.height
382
- },
383
- slides,
384
- summary: summarizeSlides(slides)
385
- };
386
-
387
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
181
+ export async function main(args = process.argv.slice(2)) {
182
+ const options = parseValidateCliArgs(args);
183
+ if (options.help) {
184
+ process.stdout.write(`${getValidateUsage()}\n`);
185
+ return;
186
+ }
388
187
 
188
+ const slidesDir = resolve(process.cwd(), options.slidesDir);
189
+ const result = await validateSlides(slidesDir, { selectedSlides: options.slides });
190
+ process.stdout.write(formatValidationResult(result, options.format));
389
191
  if (result.summary.failedSlides > 0) {
390
192
  process.exitCode = 1;
391
193
  }
392
194
  }
393
195
 
394
- main().catch((error) => {
395
- const failure = {
396
- generatedAt: new Date().toISOString(),
397
- frame: {
398
- widthPt: FRAME_PT.width,
399
- heightPt: FRAME_PT.height,
400
- widthPx: FRAME_PX.width,
401
- heightPx: FRAME_PX.height
402
- },
403
- slides: [],
404
- summary: {
405
- totalSlides: 0,
406
- passedSlides: 0,
407
- failedSlides: 0,
408
- criticalIssues: 1,
409
- warnings: 0
410
- },
411
- error: error instanceof Error ? error.message : String(error)
412
- };
196
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
413
197
 
414
- process.stdout.write(`${JSON.stringify(failure, null, 2)}\n`);
415
- process.exit(1);
416
- });
198
+ if (isMain) {
199
+ main().catch((error) => {
200
+ const failure = createValidationFailure(error);
201
+ process.stdout.write(formatValidationResult(failure, peekValidateFormat(process.argv.slice(2))));
202
+ process.exit(1);
203
+ });
204
+ }