designlang 11.1.0 → 11.3.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/SECURITY.md +46 -0
- package/bin/design-extract.js +23 -0
- package/marketplace/SUBMISSION-PLAYBOOK.md +95 -0
- package/marketplace/chrome-listing.md +45 -0
- package/marketplace/claude-code-skill.md +52 -0
- package/marketplace/cursor-listing.md +55 -0
- package/marketplace/figma-listing.md +50 -0
- package/marketplace/raycast-listing.md +38 -0
- package/marketplace/vscode-listing.md +46 -0
- package/package.json +1 -1
- package/src/chat.js +356 -0
- package/src/clone.js +6 -2
- package/src/crawler.js +18 -8
- package/src/extractors/background-patterns.js +15 -4
- package/src/extractors/material-language.js +4 -2
- package/src/extractors/motion.js +14 -7
- package/src/formatters/design-md.js +319 -0
- package/src/formatters/markdown.js +1 -1
- package/src/formatters/prompt-pack.js +1 -1
- package/src/history.js +2 -4
- package/src/studio.js +15 -5
- package/src/sync.js +14 -18
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// DESIGN.md — agent-native single-file output. Compatible with the
|
|
2
|
+
// 8-canonical-section convention (Overview · Colors · Typography · Layout
|
|
3
|
+
// · Elevation and Depth · Shapes · Components · Do's and Don'ts), plus
|
|
4
|
+
// YAML front matter holding the machine-readable token snapshot.
|
|
5
|
+
//
|
|
6
|
+
// Designed to be a drop-in replacement for design-extractor.com's
|
|
7
|
+
// DESIGN.md output, but driven by the v10/v11 semantic layer (intent,
|
|
8
|
+
// material, voice, anatomy, library detection) rather than just colors.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
|
|
12
|
+
function yamlString(v) {
|
|
13
|
+
if (v == null) return '~';
|
|
14
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
15
|
+
const s = String(v);
|
|
16
|
+
if (s === '' || /[:#&*!?[\]{},|>%@`'"\n]/.test(s) || /^\s|\s$/.test(s)) {
|
|
17
|
+
return JSON.stringify(s);
|
|
18
|
+
}
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function yamlList(arr, indent = ' ') {
|
|
23
|
+
if (!arr || arr.length === 0) return '[]';
|
|
24
|
+
return '\n' + arr.map(v => `${indent}- ${yamlString(v)}`).join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function yamlMap(obj, indent = ' ') {
|
|
28
|
+
const entries = Object.entries(obj || {}).filter(([, v]) => v != null);
|
|
29
|
+
if (entries.length === 0) return '{}';
|
|
30
|
+
return '\n' + entries.map(([k, v]) => `${indent}${k}: ${yamlString(v)}`).join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function topColor(colors, role) {
|
|
34
|
+
return colors?.[role]?.hex || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtPx(v) {
|
|
38
|
+
if (v == null) return null;
|
|
39
|
+
const n = typeof v === 'number' ? v : parseFloat(String(v));
|
|
40
|
+
return Number.isFinite(n) ? `${Math.round(n)}px` : String(v);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function uniq(arr) { return [...new Set(arr.filter(Boolean))]; }
|
|
44
|
+
|
|
45
|
+
function pickHeading(voice, fallback) {
|
|
46
|
+
const s = (voice?.sampleHeadings || []).find(h => h && h.length > 4 && h.length < 80);
|
|
47
|
+
return s || fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ratioLine(v, n) { return `${v} (${n})`; }
|
|
51
|
+
|
|
52
|
+
// ─── Section renderers ─────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function sectionOverview(design) {
|
|
55
|
+
const intent = design.pageIntent?.type || 'landing';
|
|
56
|
+
const intentConf = design.pageIntent?.confidence;
|
|
57
|
+
const material = design.materialLanguage?.label || 'flat';
|
|
58
|
+
const matConf = design.materialLanguage?.confidence;
|
|
59
|
+
const library = design.componentLibrary?.library;
|
|
60
|
+
const libConf = design.componentLibrary?.confidence;
|
|
61
|
+
const order = (design.sectionRoles?.readingOrder || []).join(' → ') || '—';
|
|
62
|
+
const voice = design.voice || {};
|
|
63
|
+
const tone = voice.tone || 'neutral';
|
|
64
|
+
const lede = pickHeading(voice, design.meta?.title || '');
|
|
65
|
+
const url = design.meta?.url || '';
|
|
66
|
+
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(`A **${intent}** page${intentConf ? ` (heuristic confidence ${intentConf})` : ''}, dressed in **${material}** material${matConf ? ` (${matConf})` : ''}.`);
|
|
69
|
+
if (library && library !== 'unknown') {
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push(`Component library appears to be **${library}**${libConf ? ` (${libConf})` : ''}.`);
|
|
72
|
+
}
|
|
73
|
+
if (lede) {
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(`> "${lede}"`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(`The author writes in a **${tone}** voice; headings tend to be **${voice.headingStyle || 'sentence'}** case and **${voice.headingLengthClass || 'balanced'}**.`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(`Reading order detected on the source: \`${order}\`.`);
|
|
81
|
+
if (url) {
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push(`Source: <${url}>.`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sectionColors(design) {
|
|
89
|
+
const c = design.colors || {};
|
|
90
|
+
const lines = ['| role | hex | usage |', '|---|---|---|'];
|
|
91
|
+
const rows = [
|
|
92
|
+
['primary', c.primary?.hex, c.primary?.count],
|
|
93
|
+
['secondary', c.secondary?.hex, c.secondary?.count],
|
|
94
|
+
['accent', c.accent?.hex, c.accent?.count],
|
|
95
|
+
['background', c.backgrounds?.[0], '—'],
|
|
96
|
+
['foreground', c.text?.[0], '—'],
|
|
97
|
+
];
|
|
98
|
+
for (const [role, hex, count] of rows) {
|
|
99
|
+
if (!hex) continue;
|
|
100
|
+
lines.push(`| ${role} | \`${hex}\` | ${count ?? '—'} |`);
|
|
101
|
+
}
|
|
102
|
+
const neutrals = (c.neutrals || []).slice(0, 5);
|
|
103
|
+
if (neutrals.length) {
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push('**Neutrals:** ' + neutrals.map(n => `\`${n.hex}\``).join(' · '));
|
|
106
|
+
}
|
|
107
|
+
if (c.all?.length) {
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(`**Total unique colors detected:** ${c.all.length}.`);
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sectionTypography(design) {
|
|
115
|
+
const t = design.typography || {};
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (t.families?.length) {
|
|
118
|
+
lines.push('**Families**');
|
|
119
|
+
for (const f of t.families.slice(0, 4)) {
|
|
120
|
+
lines.push(`- \`${f.name}\`${f.weights ? ` — weights ${[...new Set(f.weights)].join(', ')}` : ''}${f.count ? ` · ${f.count} uses` : ''}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (t.body?.size) {
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`**Body size:** \`${t.body.size}px\` / line-height \`${t.body.lineHeight ?? '1.5'}\`.`);
|
|
126
|
+
}
|
|
127
|
+
if (t.headings?.length) {
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push('**Heading scale**');
|
|
130
|
+
lines.push('| level | size | weight | line-height |');
|
|
131
|
+
lines.push('|---|---|---|---|');
|
|
132
|
+
for (const [i, h] of t.headings.slice(0, 4).entries()) {
|
|
133
|
+
lines.push(`| h${i + 1} | \`${h.size}px\` | \`${h.weight}\` | \`${h.lineHeight}\` |`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sectionLayout(design) {
|
|
140
|
+
const sp = design.spacing || {};
|
|
141
|
+
const bp = design.breakpoints || [];
|
|
142
|
+
const layout = design.layout || {};
|
|
143
|
+
const lines = [];
|
|
144
|
+
if (sp.base) lines.push(`**Spacing base:** \`${sp.base}px\` increments.`);
|
|
145
|
+
if (sp.scale?.length) lines.push(`**Scale:** ${sp.scale.slice(0, 10).map(s => `\`${(s.value ?? s)}px\``).join(' · ')}`);
|
|
146
|
+
if (layout.gridCount != null || layout.flexCount != null) {
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push(`**Layout primitives:** ${layout.gridCount ?? 0} grid containers · ${layout.flexCount ?? 0} flex containers.`);
|
|
149
|
+
}
|
|
150
|
+
if (bp.length) {
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(`**Breakpoints:** ${bp.map(b => `\`${b}px\``).join(' · ')}`);
|
|
153
|
+
}
|
|
154
|
+
return lines.join('\n') || '_No layout signals captured._';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sectionElevation(design) {
|
|
158
|
+
const sh = design.shadows?.values || [];
|
|
159
|
+
const z = design.zIndex || {};
|
|
160
|
+
const lines = [];
|
|
161
|
+
if (sh.length) {
|
|
162
|
+
lines.push('**Shadow scale**');
|
|
163
|
+
for (const s of sh.slice(0, 6)) {
|
|
164
|
+
lines.push(`- \`${s.label || '?'}\` — \`${s.raw || s.value}\``);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
lines.push('_No discrete shadow tokens detected — flat material._');
|
|
168
|
+
}
|
|
169
|
+
if (z.allValues?.length) {
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push(`**Z-index layers:** ${z.allValues.length}${z.issues?.length ? ` · ⚠ ${z.issues.length} issue(s)` : ''}`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sectionShapes(design) {
|
|
177
|
+
const r = design.borders?.radii || [];
|
|
178
|
+
const lines = [];
|
|
179
|
+
if (r.length) {
|
|
180
|
+
lines.push('**Radius scale**');
|
|
181
|
+
for (const x of r.slice(0, 6)) lines.push(`- \`${x.label || '?'}\` — \`${x.value}px\``);
|
|
182
|
+
} else {
|
|
183
|
+
lines.push('_No discrete radius tokens detected — sharp/brutalist shapes._');
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sectionComponents(design) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
const detected = Object.keys(design.components || {});
|
|
191
|
+
if (detected.length) {
|
|
192
|
+
lines.push(`**Detected patterns:** ${detected.map(c => `\`${c}\``).join(' · ')}`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
}
|
|
195
|
+
const anatomies = design.componentAnatomy || [];
|
|
196
|
+
if (anatomies.length) {
|
|
197
|
+
lines.push('**Anatomy**');
|
|
198
|
+
lines.push('| kind | variants | sizes | instances |');
|
|
199
|
+
lines.push('|---|---|---|---|');
|
|
200
|
+
for (const a of anatomies.slice(0, 8)) {
|
|
201
|
+
const variants = (a.props?.variant || []).join(', ') || '—';
|
|
202
|
+
const sizes = (a.props?.size || []).join(', ') || '—';
|
|
203
|
+
lines.push(`| ${a.kind} | ${variants} | ${sizes} | ${a.totalInstances ?? '—'} |`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!lines.length) lines.push('_No component anatomy extracted._');
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sectionDosDonts(design) {
|
|
211
|
+
const voice = design.voice || {};
|
|
212
|
+
const score = design.score || {};
|
|
213
|
+
const a11y = design.accessibility || {};
|
|
214
|
+
const lines = [];
|
|
215
|
+
|
|
216
|
+
lines.push("**Do's**");
|
|
217
|
+
const dos = [];
|
|
218
|
+
const ctas = (voice.ctaVerbs || []).slice(0, 3).map(v => v.value).filter(Boolean);
|
|
219
|
+
if (ctas.length) dos.push(`Use \`${ctas.join('\`, \`')}\` as the primary verbs in CTAs — these dominate the source.`);
|
|
220
|
+
if (voice.headingStyle) dos.push(`Write headings in **${voice.headingStyle}** case, **${voice.headingLengthClass || 'balanced'}** length.`);
|
|
221
|
+
if (voice.pronoun && voice.pronoun !== 'neutral') dos.push(`Address the reader with the pronoun posture **${voice.pronoun}**.`);
|
|
222
|
+
if (design.materialLanguage?.label) dos.push(`Stay inside the **${design.materialLanguage.label}** material — match shadow and radius habits.`);
|
|
223
|
+
if (!dos.length) dos.push('_No strong directional signals captured._');
|
|
224
|
+
for (const d of dos) lines.push(`- ${d}`);
|
|
225
|
+
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push("**Don'ts**");
|
|
228
|
+
const donts = [];
|
|
229
|
+
if (a11y.failCount > 0) donts.push(`Don't ship copy on the colors flagged in accessibility — ${a11y.failCount} contrast pair(s) fail WCAG AA on the source itself.`);
|
|
230
|
+
if (score.issues?.length) for (const i of score.issues.slice(0, 4)) donts.push(`Don't ${i.toLowerCase().replace(/^./, c => c.toLowerCase())}.`);
|
|
231
|
+
if (!donts.length) donts.push("_No anti-patterns surfaced. Don't invent new tokens — reuse the scale above._");
|
|
232
|
+
for (const d of donts) lines.push(`- ${d}`);
|
|
233
|
+
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── YAML front matter ─────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function frontMatter(design, version) {
|
|
240
|
+
const url = design.meta?.url || '';
|
|
241
|
+
const title = design.meta?.title || '';
|
|
242
|
+
const c = design.colors || {};
|
|
243
|
+
const t = design.typography || {};
|
|
244
|
+
const sp = design.spacing || {};
|
|
245
|
+
const r = design.borders?.radii || [];
|
|
246
|
+
const sh = design.shadows?.values || [];
|
|
247
|
+
|
|
248
|
+
const lines = ['---'];
|
|
249
|
+
lines.push(`site: ${yamlString(title || url)}`);
|
|
250
|
+
if (url) lines.push(`url: ${yamlString(url)}`);
|
|
251
|
+
lines.push(`generated_at: ${yamlString(new Date().toISOString())}`);
|
|
252
|
+
lines.push(`generator: ${yamlString(`designlang@${version}`)}`);
|
|
253
|
+
if (design.pageIntent?.type) lines.push(`intent: ${yamlString(design.pageIntent.type)}`);
|
|
254
|
+
if (design.materialLanguage?.label) lines.push(`material: ${yamlString(design.materialLanguage.label)}`);
|
|
255
|
+
if (design.componentLibrary?.library && design.componentLibrary.library !== 'unknown') {
|
|
256
|
+
lines.push(`library: ${yamlString(design.componentLibrary.library)}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
lines.push('tokens:');
|
|
260
|
+
lines.push(' colors:' + yamlMap({
|
|
261
|
+
primary: topColor(c, 'primary'),
|
|
262
|
+
secondary: topColor(c, 'secondary'),
|
|
263
|
+
accent: topColor(c, 'accent'),
|
|
264
|
+
background: c.backgrounds?.[0],
|
|
265
|
+
foreground: c.text?.[0],
|
|
266
|
+
}, ' '));
|
|
267
|
+
lines.push(' typography:' + yamlMap({
|
|
268
|
+
sans: t.families?.[0]?.name,
|
|
269
|
+
mono: t.families?.find(f => /mono/i.test(f.name))?.name,
|
|
270
|
+
base: t.body?.size,
|
|
271
|
+
}, ' '));
|
|
272
|
+
lines.push(' spacing:' + yamlMap({
|
|
273
|
+
base: sp.base,
|
|
274
|
+
scale: sp.scale?.length ? '[' + sp.scale.slice(0, 10).map(s => (s.value ?? s)).join(', ') + ']' : null,
|
|
275
|
+
}, ' '));
|
|
276
|
+
if (r.length) {
|
|
277
|
+
lines.push(' radii:' + yamlMap(Object.fromEntries(r.slice(0, 6).map(x => [x.label || `r${x.value}`, x.value])), ' '));
|
|
278
|
+
}
|
|
279
|
+
if (sh.length) {
|
|
280
|
+
lines.push(' shadows:' + yamlMap(Object.fromEntries(sh.slice(0, 4).map(x => [x.label || 'shadow', x.raw || x.value])), ' '));
|
|
281
|
+
}
|
|
282
|
+
lines.push('---');
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Main ──────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
let CACHED_VERSION = null;
|
|
289
|
+
function pkgVersion() {
|
|
290
|
+
if (CACHED_VERSION) return CACHED_VERSION;
|
|
291
|
+
try {
|
|
292
|
+
const url = new URL('../../package.json', import.meta.url);
|
|
293
|
+
CACHED_VERSION = JSON.parse(readFileSync(url, 'utf-8')).version;
|
|
294
|
+
} catch { CACHED_VERSION = '0.0.0'; }
|
|
295
|
+
return CACHED_VERSION;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function formatDesignMd(design) {
|
|
299
|
+
const version = pkgVersion();
|
|
300
|
+
const fm = frontMatter(design, version);
|
|
301
|
+
|
|
302
|
+
const sections = [
|
|
303
|
+
['Overview', sectionOverview(design)],
|
|
304
|
+
['Colors', sectionColors(design)],
|
|
305
|
+
['Typography', sectionTypography(design)],
|
|
306
|
+
['Layout', sectionLayout(design)],
|
|
307
|
+
['Elevation and Depth', sectionElevation(design)],
|
|
308
|
+
['Shapes', sectionShapes(design)],
|
|
309
|
+
['Components', sectionComponents(design)],
|
|
310
|
+
["Do's and Don'ts", sectionDosDonts(design)],
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const body = sections.map(([h, b]) => `# ${h}\n\n${b || '_—_'}\n`).join('\n');
|
|
314
|
+
|
|
315
|
+
const sourceUrl = design.meta?.url || '';
|
|
316
|
+
const footer = `\n---\n_Generated by [designlang](https://github.com/Manavarya09/design-extract) v${version} from <${sourceUrl}>._\n_Compatible with the DESIGN.md convention pioneered by [design-extractor.com](https://www.design-extractor.com) — extended with intent, material, voice, anatomy, and library detection._\n`;
|
|
317
|
+
|
|
318
|
+
return `${fm}\n\n${body}${footer}`;
|
|
319
|
+
}
|
|
@@ -733,7 +733,7 @@ export function formatMarkdown(design) {
|
|
|
733
733
|
lines.push('| # | Role | Heading | Confidence |');
|
|
734
734
|
lines.push('|---|------|---------|------------|');
|
|
735
735
|
for (const s of design.sectionRoles.sections.slice(0, 20)) {
|
|
736
|
-
const h = (s.heading || '').replace(/\|/g, '\\|').slice(0, 80);
|
|
736
|
+
const h = (s.heading || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|').slice(0, 80);
|
|
737
737
|
lines.push(`| ${s.index} | ${s.role}${s.subrole ? ` · ${s.subrole}` : ''} | ${h || '—'} | ${s.confidence} |`);
|
|
738
738
|
}
|
|
739
739
|
lines.push('');
|
|
@@ -139,7 +139,7 @@ export function formatCursorPrompt(design) {
|
|
|
139
139
|
` colors: [${b.colors.map(c => `'${c}'`).join(', ')}],`,
|
|
140
140
|
` fonts: [${b.fonts.map(f => `'${f}'`).join(', ')}],`,
|
|
141
141
|
` radii: [${(design.borders?.radii || []).slice(0, 6).map(r => `'${String(r.value ?? r)}'`).join(', ')}],`,
|
|
142
|
-
` shadows: [${(design.shadows?.values || []).slice(0, 3).map(s => `'${String(s.raw ?? s.value ?? s).replace(/'/g, "\\'")}'`).join(', ')}],`,
|
|
142
|
+
` shadows: [${(design.shadows?.values || []).slice(0, 3).map(s => `'${String(s.raw ?? s.value ?? s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`).join(', ')}],`,
|
|
143
143
|
'};',
|
|
144
144
|
'```',
|
|
145
145
|
'',
|
package/src/history.js
CHANGED
|
@@ -19,10 +19,9 @@ export function saveSnapshot(design) {
|
|
|
19
19
|
const hostname = new URL(design.meta.url).hostname.replace(/^www\./, '');
|
|
20
20
|
const file = historyFile(hostname);
|
|
21
21
|
|
|
22
|
+
// Read directly inside try/catch — no existsSync race.
|
|
22
23
|
let history = [];
|
|
23
|
-
|
|
24
|
-
try { history = JSON.parse(readFileSync(file, 'utf-8')); } catch { history = []; }
|
|
25
|
-
}
|
|
24
|
+
try { history = JSON.parse(readFileSync(file, 'utf-8')); } catch { history = []; }
|
|
26
25
|
|
|
27
26
|
// Compact snapshot — only store key metrics, not full data
|
|
28
27
|
const snapshot = {
|
|
@@ -65,7 +64,6 @@ export function getHistory(url) {
|
|
|
65
64
|
ensureDir();
|
|
66
65
|
const hostname = new URL(url).hostname.replace(/^www\./, '');
|
|
67
66
|
const file = historyFile(hostname);
|
|
68
|
-
if (!existsSync(file)) return [];
|
|
69
67
|
try { return JSON.parse(readFileSync(file, 'utf-8')); } catch { return []; }
|
|
70
68
|
}
|
|
71
69
|
|
package/src/studio.js
CHANGED
|
@@ -296,15 +296,25 @@ export async function runStudio(opts) {
|
|
|
296
296
|
// Static passthrough — screenshots, preview.html, etc.
|
|
297
297
|
const safe = pathname.replace(/\.\./g, '').replace(/^\//, '');
|
|
298
298
|
const filePath = join(dir, safe);
|
|
299
|
-
|
|
299
|
+
// Path-traversal guard: must stay inside dir.
|
|
300
|
+
if (!filePath.startsWith(dir)) {
|
|
301
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
302
|
+
res.end('not found');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Race-safe read — single try/catch instead of exists→stat→read chain.
|
|
306
|
+
try {
|
|
307
|
+
if (!statSync(filePath).isFile()) throw new Error('not a file');
|
|
308
|
+
const body = readFileSync(filePath);
|
|
300
309
|
const ext = extname(filePath).toLowerCase();
|
|
301
310
|
res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' });
|
|
302
|
-
res.end(
|
|
311
|
+
res.end(body);
|
|
312
|
+
return;
|
|
313
|
+
} catch {
|
|
314
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
315
|
+
res.end('not found');
|
|
303
316
|
return;
|
|
304
317
|
}
|
|
305
|
-
|
|
306
|
-
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
307
|
-
res.end('not found');
|
|
308
318
|
} catch (e) {
|
|
309
319
|
res.writeHead(500, { 'content-type': 'text/plain' });
|
|
310
320
|
res.end(`error: ${e.message}`);
|
package/src/sync.js
CHANGED
|
@@ -5,9 +5,19 @@ import { formatTokens } from './formatters/tokens.js';
|
|
|
5
5
|
import { formatTailwind } from './formatters/tailwind.js';
|
|
6
6
|
import { formatCssVars } from './formatters/css-vars.js';
|
|
7
7
|
import { saveSnapshot, getHistory } from './history.js';
|
|
8
|
-
import { writeFileSync, readFileSync,
|
|
8
|
+
import { writeFileSync, readFileSync, statSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
|
|
11
|
+
// Race-safe "update only if file exists" — statSync inside try/catch
|
|
12
|
+
// closes the toctou window vs. existsSync→writeFileSync.
|
|
13
|
+
function updateIfExists(path, content) {
|
|
14
|
+
try {
|
|
15
|
+
if (!statSync(path).isFile()) return false;
|
|
16
|
+
writeFileSync(path, content, 'utf-8');
|
|
17
|
+
return true;
|
|
18
|
+
} catch { return false; }
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export async function syncDesign(url, options = {}) {
|
|
12
22
|
const { out = '.', interval = 3600000 } = options; // default 1 hour
|
|
13
23
|
|
|
@@ -42,23 +52,9 @@ export async function syncDesign(url, options = {}) {
|
|
|
42
52
|
// Update local files
|
|
43
53
|
const updates = [];
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
updates.push('design-tokens.json');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const tailwindPath = join(out, 'tailwind.config.js');
|
|
52
|
-
if (existsSync(tailwindPath)) {
|
|
53
|
-
writeFileSync(tailwindPath, formatTailwind(current), 'utf-8');
|
|
54
|
-
updates.push('tailwind.config.js');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const cssPath = join(out, 'variables.css');
|
|
58
|
-
if (existsSync(cssPath)) {
|
|
59
|
-
writeFileSync(cssPath, formatCssVars(current), 'utf-8');
|
|
60
|
-
updates.push('variables.css');
|
|
61
|
-
}
|
|
55
|
+
if (updateIfExists(join(out, 'design-tokens.json'), formatTokens(current))) updates.push('design-tokens.json');
|
|
56
|
+
if (updateIfExists(join(out, 'tailwind.config.js'), formatTailwind(current))) updates.push('tailwind.config.js');
|
|
57
|
+
if (updateIfExists(join(out, 'variables.css'), formatCssVars(current))) updates.push('variables.css');
|
|
62
58
|
|
|
63
59
|
return {
|
|
64
60
|
changes,
|