style-capture 0.0.1

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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "style-capture",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "bin": {
6
+ "style-capture": "./dist/index.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev": "tsx src/index.ts",
11
+ "build": "tsc -b",
12
+ "check-types": "tsc -b --pretty false",
13
+ "lint": "biome check .",
14
+ "lint:fix": "biome check --write .",
15
+ "test": "echo 'No tests yet'"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^1.1.0",
19
+ "@style-capture/core": "*",
20
+ "clipboardy": "^4.0.0",
21
+ "playwright": "^1.52.0"
22
+ },
23
+ "devDependencies": {
24
+ "@biomejs/biome": "^2.4.5",
25
+ "@types/node": "^24.12.0",
26
+ "lefthook": "^2.1.4",
27
+ "oxfmt": "^0.40.0",
28
+ "oxlint": "^1.55.0",
29
+ "tsx": "^4.20.6",
30
+ "typescript": "~5.9.3",
31
+ "ultracite": "^7.3.0"
32
+ }
33
+ }
package/src/capture.ts ADDED
@@ -0,0 +1,507 @@
1
+ import type { CaptureResult, CaptureSettings } from "@style-capture/core";
2
+ import type { Page } from "playwright";
3
+
4
+ /**
5
+ * Runs the capture pipeline inside a Playwright page context.
6
+ * This mirrors the extension's `buildCapture` from `run-picker.ts`,
7
+ * adapted to run via `page.evaluate()`.
8
+ */
9
+ export async function captureElement(
10
+ page: Page,
11
+ selector: string,
12
+ settings: CaptureSettings
13
+ ): Promise<CaptureResult> {
14
+ const result = await page.evaluate(
15
+ ({ selector, settings }) => {
16
+ const OMITTED_ELEMENT_NAMES = new Set([
17
+ "base",
18
+ "iframe",
19
+ "link",
20
+ "meta",
21
+ "noscript",
22
+ "object",
23
+ "script",
24
+ "style",
25
+ "template",
26
+ ]);
27
+ const OMITTED_ATTRIBUTE_NAMES = new Set(["checked", "selected", "value"]);
28
+ const OMITTED_URL_ATTRIBUTE_NAMES = new Set([
29
+ "action",
30
+ "formaction",
31
+ "href",
32
+ "poster",
33
+ "src",
34
+ "srcdoc",
35
+ "srcset",
36
+ "xlink:href",
37
+ ]);
38
+ const BASELINE_ATTRIBUTE_NAMES = [
39
+ "checked",
40
+ "cols",
41
+ "disabled",
42
+ "multiple",
43
+ "open",
44
+ "rows",
45
+ "selected",
46
+ "size",
47
+ "type",
48
+ "wrap",
49
+ ] as const;
50
+ const INHERITED_PROPERTIES = new Set([
51
+ "color",
52
+ "font-family",
53
+ "font-size",
54
+ "font-style",
55
+ "font-weight",
56
+ "letter-spacing",
57
+ "line-height",
58
+ "list-style-type",
59
+ "text-align",
60
+ "text-decoration-color",
61
+ "text-decoration-line",
62
+ "text-transform",
63
+ "visibility",
64
+ "white-space",
65
+ ]);
66
+ const CURATED_PROPERTIES = [
67
+ "align-items",
68
+ "background-color",
69
+ "background-image",
70
+ "border-bottom-color",
71
+ "border-bottom-left-radius",
72
+ "border-bottom-right-radius",
73
+ "border-bottom-style",
74
+ "border-bottom-width",
75
+ "border-left-color",
76
+ "border-left-style",
77
+ "border-left-width",
78
+ "border-right-color",
79
+ "border-right-style",
80
+ "border-right-width",
81
+ "border-top-color",
82
+ "border-top-left-radius",
83
+ "border-top-right-radius",
84
+ "border-top-style",
85
+ "border-top-width",
86
+ "bottom",
87
+ "box-shadow",
88
+ "color",
89
+ "column-gap",
90
+ "display",
91
+ "flex-basis",
92
+ "flex-direction",
93
+ "flex-grow",
94
+ "flex-shrink",
95
+ "flex-wrap",
96
+ "font-family",
97
+ "font-size",
98
+ "font-style",
99
+ "font-weight",
100
+ "gap",
101
+ "grid-auto-flow",
102
+ "grid-column-end",
103
+ "grid-column-start",
104
+ "grid-row-end",
105
+ "grid-row-start",
106
+ "grid-template-columns",
107
+ "grid-template-rows",
108
+ "height",
109
+ "justify-content",
110
+ "left",
111
+ "letter-spacing",
112
+ "line-height",
113
+ "list-style-type",
114
+ "margin-bottom",
115
+ "margin-left",
116
+ "margin-right",
117
+ "margin-top",
118
+ "max-height",
119
+ "max-width",
120
+ "min-height",
121
+ "min-width",
122
+ "object-fit",
123
+ "object-position",
124
+ "opacity",
125
+ "overflow-x",
126
+ "overflow-y",
127
+ "padding-bottom",
128
+ "padding-left",
129
+ "padding-right",
130
+ "padding-top",
131
+ "position",
132
+ "right",
133
+ "row-gap",
134
+ "text-align",
135
+ "text-decoration-color",
136
+ "text-decoration-line",
137
+ "text-transform",
138
+ "top",
139
+ "transform",
140
+ "transform-origin",
141
+ "visibility",
142
+ "white-space",
143
+ "width",
144
+ "z-index",
145
+ ];
146
+
147
+ const root = document.querySelector(selector);
148
+ if (!root) {
149
+ throw new Error(`No element found for selector: ${selector}`);
150
+ }
151
+
152
+ const elements: Record<
153
+ string,
154
+ {
155
+ attributes: Record<string, string>;
156
+ boundingBox: {
157
+ bottom: number;
158
+ height: number;
159
+ left: number;
160
+ right: number;
161
+ top: number;
162
+ width: number;
163
+ x: number;
164
+ y: number;
165
+ };
166
+ children: string[];
167
+ classList: string[];
168
+ id: string;
169
+ parentId: string | null;
170
+ pseudo: Record<
171
+ string,
172
+ { kind: "before" | "after"; styles: Record<string, string> }
173
+ >;
174
+ selector: string;
175
+ styles: Record<string, string>;
176
+ tagName: string;
177
+ }
178
+ > = {};
179
+ const order: string[] = [];
180
+ const idByElement = new WeakMap<Element, string>();
181
+ let pseudoElementCount = 0;
182
+ let nextId = 0;
183
+
184
+ // Default style cache using hidden iframe
185
+ const defaultStyleFrame = document.createElement("iframe");
186
+ defaultStyleFrame.setAttribute("aria-hidden", "true");
187
+ defaultStyleFrame.tabIndex = -1;
188
+ defaultStyleFrame.style.cssText =
189
+ "position:fixed;top:-9999px;left:-9999px;width:0;height:0;border:0;opacity:0;pointer-events:none";
190
+ document.documentElement.append(defaultStyleFrame);
191
+ const defaultDoc = defaultStyleFrame.contentDocument;
192
+ if (defaultDoc) {
193
+ defaultDoc.open();
194
+ defaultDoc.write("<!doctype html><html><body></body></html>");
195
+ defaultDoc.close();
196
+ }
197
+ const defaultStyleCache = new Map<string, Record<string, string>>();
198
+
199
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors extension capture logic in self-contained page.evaluate
200
+ function getDefaultStyles(element: Element): Record<string, string> {
201
+ const parts = [element.tagName.toLowerCase()];
202
+ for (const attr of BASELINE_ATTRIBUTE_NAMES) {
203
+ if (element.hasAttribute(attr)) {
204
+ parts.push(`${attr}=${element.getAttribute(attr) ?? ""}`);
205
+ }
206
+ }
207
+ const key = parts.join("|");
208
+
209
+ const cached = defaultStyleCache.get(key);
210
+ if (cached) {
211
+ return cached;
212
+ }
213
+
214
+ const frameDoc = defaultStyleFrame.contentDocument;
215
+ const frameWin = defaultStyleFrame.contentWindow;
216
+ if (!(frameDoc && frameWin)) {
217
+ return {};
218
+ }
219
+
220
+ const baseline = frameDoc.createElement(element.tagName.toLowerCase());
221
+ for (const attr of BASELINE_ATTRIBUTE_NAMES) {
222
+ if (element.hasAttribute(attr)) {
223
+ baseline.setAttribute(attr, element.getAttribute(attr) ?? "");
224
+ }
225
+ }
226
+ frameDoc.body.append(baseline);
227
+ const computed = frameWin.getComputedStyle(baseline);
228
+ const defaults: Record<string, string> = {};
229
+ for (const prop of CURATED_PROPERTIES) {
230
+ const val = computed.getPropertyValue(prop).trim();
231
+ if (val) {
232
+ defaults[prop] = val;
233
+ }
234
+ }
235
+ baseline.remove();
236
+ defaultStyleCache.set(key, defaults);
237
+ return defaults;
238
+ }
239
+
240
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors extension capture logic in self-contained page.evaluate
241
+ function snapshotStyles(
242
+ element: Element,
243
+ styles: CSSStyleDeclaration,
244
+ includeAll: boolean,
245
+ parentStyles: Record<string, string> | null
246
+ ): Record<string, string> {
247
+ const output: Record<string, string> = {};
248
+ const properties = includeAll
249
+ ? Array.from(styles)
250
+ : [...CURATED_PROPERTIES];
251
+ const defaultStyles = includeAll ? null : getDefaultStyles(element);
252
+
253
+ for (const property of properties) {
254
+ const value = styles.getPropertyValue(property);
255
+ if (!value) {
256
+ continue;
257
+ }
258
+ const trimmed = value.trim();
259
+ if (!trimmed) {
260
+ continue;
261
+ }
262
+ if (defaultStyles?.[property] === trimmed) {
263
+ continue;
264
+ }
265
+ if (
266
+ parentStyles &&
267
+ INHERITED_PROPERTIES.has(property) &&
268
+ parentStyles[property] === trimmed
269
+ ) {
270
+ continue;
271
+ }
272
+ output[property] = trimmed;
273
+ }
274
+ return output;
275
+ }
276
+
277
+ function snapshotPseudo(
278
+ element: Element,
279
+ kind: "before" | "after",
280
+ includeAll: boolean
281
+ ): { kind: "before" | "after"; styles: Record<string, string> } | null {
282
+ const styles = window.getComputedStyle(element, `::${kind}`);
283
+ const content = styles.getPropertyValue("content").trim();
284
+ const display = styles.getPropertyValue("display").trim();
285
+ const w = styles.getPropertyValue("width").trim();
286
+ const h = styles.getPropertyValue("height").trim();
287
+ const bg = styles.getPropertyValue("background-color").trim();
288
+ const bw = styles.getPropertyValue("border-top-width").trim();
289
+
290
+ if (
291
+ content === "none" &&
292
+ display === "inline" &&
293
+ w === "auto" &&
294
+ h === "auto" &&
295
+ bg === "rgba(0, 0, 0, 0)" &&
296
+ bw === "0px"
297
+ ) {
298
+ return null;
299
+ }
300
+
301
+ return {
302
+ kind,
303
+ styles: snapshotStyles(element, styles, includeAll, null),
304
+ };
305
+ }
306
+
307
+ function buildSelector(element: Element): string {
308
+ if (element.id) {
309
+ return `#${CSS.escape(element.id)}`;
310
+ }
311
+ const segments: string[] = [];
312
+ let current: Element | null = element;
313
+ while (
314
+ current &&
315
+ current.nodeType === Node.ELEMENT_NODE &&
316
+ segments.length < 4
317
+ ) {
318
+ const tag = current.tagName.toLowerCase();
319
+ const classes = Array.from(current.classList)
320
+ .slice(0, 2)
321
+ .map((c) => `.${CSS.escape(c)}`)
322
+ .join("");
323
+ const idx = current.parentElement
324
+ ? Array.from(current.parentElement.children).indexOf(current) + 1
325
+ : 1;
326
+ segments.unshift(`${tag}${classes}:nth-child(${idx})`);
327
+ current = current.parentElement;
328
+ }
329
+ return segments.join(" > ");
330
+ }
331
+
332
+ function shouldOmitAttr(name: string): boolean {
333
+ const n = name.toLowerCase();
334
+ return (
335
+ n.startsWith("on") ||
336
+ n === "nonce" ||
337
+ OMITTED_ATTRIBUTE_NAMES.has(n) ||
338
+ OMITTED_URL_ATTRIBUTE_NAMES.has(n)
339
+ );
340
+ }
341
+
342
+ function getSafeAttributes(el: Element): Record<string, string> {
343
+ const safe: Record<string, string> = {};
344
+ for (const attr of Array.from(el.attributes)) {
345
+ if (!shouldOmitAttr(attr.name)) {
346
+ safe[attr.name] = attr.value;
347
+ }
348
+ }
349
+ return safe;
350
+ }
351
+
352
+ function getBoundingBox(rect: DOMRect) {
353
+ return {
354
+ bottom: rect.bottom,
355
+ height: rect.height,
356
+ left: rect.left,
357
+ right: rect.right,
358
+ top: rect.top,
359
+ width: rect.width,
360
+ x: rect.x,
361
+ y: rect.y,
362
+ };
363
+ }
364
+
365
+ function isHidden(el: Element): boolean {
366
+ const s = window.getComputedStyle(el);
367
+ return s.display === "none" || s.visibility === "hidden";
368
+ }
369
+
370
+ function sanitizeOuterHtml(root: Element): string {
371
+ const clone = root.cloneNode(true);
372
+ if (!(clone instanceof Element)) {
373
+ return "";
374
+ }
375
+
376
+ // Remove omitted elements
377
+ for (const el of Array.from(clone.querySelectorAll("*"))) {
378
+ if (OMITTED_ELEMENT_NAMES.has(el.tagName.toLowerCase())) {
379
+ el.replaceWith(
380
+ el.ownerDocument.createComment(el.tagName.toLowerCase())
381
+ );
382
+ }
383
+ }
384
+
385
+ // Sanitize attributes
386
+ function sanitize(el: Element) {
387
+ for (const attr of Array.from(el.attributes)) {
388
+ if (shouldOmitAttr(attr.name)) {
389
+ el.removeAttribute(attr.name);
390
+ }
391
+ }
392
+ if (el instanceof HTMLTextAreaElement) {
393
+ el.textContent = "";
394
+ }
395
+ }
396
+ sanitize(clone);
397
+ for (const el of clone.querySelectorAll("*")) {
398
+ sanitize(el);
399
+ }
400
+
401
+ return clone.outerHTML;
402
+ }
403
+
404
+ function captureEl(element: Element, parentId: string | null): string {
405
+ const id = `node-${nextId}`;
406
+ nextId += 1;
407
+ idByElement.set(element, id);
408
+ order.push(id);
409
+
410
+ const snapshot = {
411
+ attributes: getSafeAttributes(element),
412
+ boundingBox: getBoundingBox(element.getBoundingClientRect()),
413
+ children: [] as string[],
414
+ classList: Array.from(element.classList),
415
+ id,
416
+ parentId,
417
+ pseudo: {} as Record<
418
+ string,
419
+ { kind: "before" | "after"; styles: Record<string, string> }
420
+ >,
421
+ selector: buildSelector(element),
422
+ styles: snapshotStyles(
423
+ element,
424
+ window.getComputedStyle(element),
425
+ settings.captureMode === "full",
426
+ parentId ? (elements[parentId]?.styles ?? null) : null
427
+ ),
428
+ tagName: element.tagName.toLowerCase(),
429
+ };
430
+
431
+ if (settings.includePseudoElements) {
432
+ const before = snapshotPseudo(
433
+ element,
434
+ "before",
435
+ settings.captureMode === "full"
436
+ );
437
+ const after = snapshotPseudo(
438
+ element,
439
+ "after",
440
+ settings.captureMode === "full"
441
+ );
442
+ if (before) {
443
+ snapshot.pseudo.before = before;
444
+ pseudoElementCount += 1;
445
+ }
446
+ if (after) {
447
+ snapshot.pseudo.after = after;
448
+ pseudoElementCount += 1;
449
+ }
450
+ }
451
+
452
+ elements[id] = snapshot;
453
+ if (parentId && elements[parentId]) {
454
+ elements[parentId].children.push(id);
455
+ }
456
+ return id;
457
+ }
458
+
459
+ const rootElementId = captureEl(root, null);
460
+
461
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
462
+ acceptNode(node) {
463
+ if (
464
+ node instanceof Element &&
465
+ node !== root &&
466
+ !settings.includeHiddenElements &&
467
+ isHidden(node)
468
+ ) {
469
+ return NodeFilter.FILTER_REJECT;
470
+ }
471
+ return NodeFilter.FILTER_ACCEPT;
472
+ },
473
+ });
474
+
475
+ while (walker.nextNode()) {
476
+ const current = walker.currentNode;
477
+ if (!(current instanceof Element)) {
478
+ continue;
479
+ }
480
+ const parentId = current.parentElement
481
+ ? (idByElement.get(current.parentElement) ?? null)
482
+ : null;
483
+ captureEl(current, parentId);
484
+ }
485
+
486
+ // Cleanup
487
+ defaultStyleFrame.remove();
488
+
489
+ return {
490
+ elements,
491
+ metadata: { url: window.location.href, title: document.title },
492
+ order,
493
+ rootElementId,
494
+ rootOuterHtml: sanitizeOuterHtml(root),
495
+ settings,
496
+ summary: {
497
+ elementCount: order.length,
498
+ pseudoElementCount,
499
+ },
500
+ version: 1 as const,
501
+ };
502
+ },
503
+ { selector, settings }
504
+ );
505
+
506
+ return result as CaptureResult;
507
+ }
package/src/index.ts ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ cancel,
5
+ group,
6
+ intro,
7
+ log,
8
+ outro,
9
+ select,
10
+ spinner,
11
+ text,
12
+ } from "@clack/prompts";
13
+ import type { CaptureSettings } from "@style-capture/core";
14
+ import {
15
+ createDefaultSettings,
16
+ formatCaptureForClaudeMarkdown,
17
+ mapCaptureToTailwind,
18
+ } from "@style-capture/core";
19
+ import { chromium } from "playwright";
20
+
21
+ import { captureElement } from "./capture.ts";
22
+
23
+ async function main(): Promise<void> {
24
+ intro("style-capture");
25
+
26
+ const inputs = await group(
27
+ {
28
+ url: () =>
29
+ text({
30
+ message: "URL to capture",
31
+ placeholder: "https://example.com",
32
+ validate: (val) => {
33
+ if (!val) {
34
+ return "URL is required";
35
+ }
36
+ try {
37
+ new URL(val);
38
+ } catch {
39
+ return "Please enter a valid URL";
40
+ }
41
+ },
42
+ }),
43
+ selector: () =>
44
+ text({
45
+ message: "CSS selector for target element",
46
+ placeholder: "main, .hero, #app",
47
+ validate: (val) => {
48
+ if (!val?.trim()) {
49
+ return "Selector is required";
50
+ }
51
+ },
52
+ }),
53
+ mode: () =>
54
+ select({
55
+ message: "Capture mode",
56
+ options: [
57
+ {
58
+ label: "Curated",
59
+ value: "curated" as const,
60
+ hint: "Common visual properties only",
61
+ },
62
+ {
63
+ label: "Full",
64
+ value: "full" as const,
65
+ hint: "All computed styles",
66
+ },
67
+ ],
68
+ }),
69
+ output: () =>
70
+ select({
71
+ message: "Output",
72
+ options: [
73
+ { label: "Clipboard", value: "clipboard" as const },
74
+ { label: "Stdout", value: "stdout" as const },
75
+ ],
76
+ }),
77
+ },
78
+ {
79
+ onCancel: () => {
80
+ cancel("Cancelled.");
81
+ process.exit(0);
82
+ },
83
+ }
84
+ );
85
+
86
+ const settings: CaptureSettings = {
87
+ ...createDefaultSettings(),
88
+ captureMode: inputs.mode,
89
+ };
90
+
91
+ const spin = spinner();
92
+ spin.start("Launching browser");
93
+
94
+ const browser = await chromium.launch({ headless: true });
95
+
96
+ try {
97
+ const page = await browser.newPage();
98
+
99
+ spin.message(`Loading ${inputs.url}`);
100
+ await page.goto(inputs.url, { waitUntil: "networkidle" });
101
+
102
+ spin.message("Verifying selector");
103
+ const found = await page.locator(inputs.selector).count();
104
+ if (found === 0) {
105
+ spin.stop("No element found");
106
+ log.error(
107
+ `Selector "${inputs.selector}" matched 0 elements on ${inputs.url}`
108
+ );
109
+ await browser.close();
110
+ process.exit(1);
111
+ }
112
+
113
+ if (found > 1) {
114
+ log.warn(`Selector matched ${found} elements — capturing the first one`);
115
+ }
116
+
117
+ spin.message(`Capturing ${found} element(s)`);
118
+ const capture = await captureElement(page, inputs.selector, settings);
119
+
120
+ spin.message("Mapping to Tailwind");
121
+ const mapping = mapCaptureToTailwind(capture);
122
+
123
+ spin.message("Formatting");
124
+ const markdown = formatCaptureForClaudeMarkdown(capture, mapping);
125
+
126
+ spin.stop("Capture complete");
127
+
128
+ log.info(
129
+ `${capture.summary.elementCount} elements, ${capture.summary.pseudoElementCount} pseudo-elements`
130
+ );
131
+ log.info(
132
+ `Tailwind: ${mapping.summary.utilityCount} utilities mapped (${mapping.summary.averageConfidence.toFixed(0)}% avg confidence)`
133
+ );
134
+
135
+ if (inputs.output === "clipboard") {
136
+ const { default: clipboardy } = await import("clipboardy");
137
+ await clipboardy.write(markdown);
138
+ log.success("Copied to clipboard");
139
+ } else {
140
+ console.log(`\n${markdown}`);
141
+ }
142
+ } finally {
143
+ await browser.close();
144
+ }
145
+
146
+ outro("Done");
147
+ }
148
+
149
+ main().catch((error) => {
150
+ log.error(String(error));
151
+ process.exit(1);
152
+ });
package/src/run.ts ADDED
@@ -0,0 +1,106 @@
1
+ import type { CaptureSettings } from "@style-capture/core";
2
+ import {
3
+ createDefaultSettings,
4
+ formatCaptureForClaudeMarkdown,
5
+ mapCaptureToTailwind,
6
+ } from "@style-capture/core";
7
+ import { chromium } from "playwright";
8
+
9
+ import { captureElement } from "./capture.ts";
10
+
11
+ export interface RunOptions {
12
+ mode?: "curated" | "full";
13
+ selector: string;
14
+ url: string;
15
+ }
16
+
17
+ /**
18
+ * Non-interactive capture — designed for agent/skill usage.
19
+ * Returns the formatted style_capture prompt as a string.
20
+ */
21
+ export async function run(options: RunOptions): Promise<string> {
22
+ const settings: CaptureSettings = {
23
+ ...createDefaultSettings(),
24
+ captureMode: options.mode ?? "curated",
25
+ };
26
+
27
+ const browser = await chromium.launch({ headless: true });
28
+ try {
29
+ const page = await browser.newPage();
30
+ await page.goto(options.url, { waitUntil: "networkidle" });
31
+
32
+ const count = await page.locator(options.selector).count();
33
+ if (count === 0) {
34
+ throw new Error(
35
+ `Selector "${options.selector}" matched 0 elements on ${options.url}`
36
+ );
37
+ }
38
+
39
+ if (count > 1) {
40
+ process.stderr.write(
41
+ `Warning: selector matched ${count} elements, capturing the first\n`
42
+ );
43
+ }
44
+
45
+ const capture = await captureElement(page, options.selector, settings);
46
+ const mapping = mapCaptureToTailwind(capture);
47
+ const output = formatCaptureForClaudeMarkdown(capture, mapping);
48
+
49
+ process.stderr.write(
50
+ `Captured ${capture.summary.elementCount} elements, ${mapping.summary.utilityCount} Tailwind utilities mapped\n`
51
+ );
52
+
53
+ return output;
54
+ } finally {
55
+ await browser.close();
56
+ }
57
+ }
58
+
59
+ function parseArgs(argv: string[]): RunOptions {
60
+ const args = argv.slice(2);
61
+
62
+ // Support both positional and flag-based args
63
+ let url: string | undefined;
64
+ let selector: string | undefined;
65
+ let mode: "curated" | "full" = "curated";
66
+
67
+ for (let i = 0; i < args.length; i++) {
68
+ const arg = args[i];
69
+ if (arg === "--mode" && args[i + 1]) {
70
+ mode = args[i + 1] as "curated" | "full";
71
+ i++;
72
+ } else if (arg === "--url" && args[i + 1]) {
73
+ url = args[i + 1];
74
+ i++;
75
+ } else if (arg === "--selector" && args[i + 1]) {
76
+ selector = args[i + 1];
77
+ i++;
78
+ } else if (!url) {
79
+ url = arg;
80
+ } else if (!selector) {
81
+ selector = arg;
82
+ }
83
+ }
84
+
85
+ if (!(url && selector)) {
86
+ process.stderr.write(
87
+ "Usage: style-capture <url> <selector> [--mode curated|full]\n"
88
+ );
89
+ process.stderr.write(
90
+ " style-capture --url <url> --selector <selector>\n"
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ return { url, selector, mode };
96
+ }
97
+
98
+ const options = parseArgs(process.argv);
99
+ run(options)
100
+ .then((result) => {
101
+ process.stdout.write(result);
102
+ })
103
+ .catch((error) => {
104
+ process.stderr.write(`Error: ${String(error)}\n`);
105
+ process.exit(1);
106
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "verbatimModuleSyntax": true,
9
+ "moduleDetection": "force",
10
+ "noEmit": true,
11
+ "strict": true,
12
+ "skipLibCheck": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "erasableSyntaxOnly": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "types": ["node"]
18
+ },
19
+ "include": ["src"]
20
+ }