@stayicon/drift-guard 0.1.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/LICENSE +21 -0
- package/README.md +201 -0
- package/bin/drift-guard.mjs +2 -0
- package/dist/chunk-27T45SVD.js +627 -0
- package/dist/chunk-27T45SVD.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +203 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
cssFiles: [
|
|
4
|
+
"src/**/*.css",
|
|
5
|
+
"app/**/*.css",
|
|
6
|
+
"styles/**/*.css",
|
|
7
|
+
"**/*.module.css",
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
htmlFiles: [
|
|
11
|
+
"**/*.html",
|
|
12
|
+
"!node_modules/**",
|
|
13
|
+
"!dist/**",
|
|
14
|
+
"!build/**"
|
|
15
|
+
],
|
|
16
|
+
threshold: 10,
|
|
17
|
+
trackCategories: ["color", "font", "spacing", "shadow", "radius", "layout"],
|
|
18
|
+
ignore: [
|
|
19
|
+
"node_modules/**",
|
|
20
|
+
"dist/**",
|
|
21
|
+
"build/**",
|
|
22
|
+
".next/**",
|
|
23
|
+
"coverage/**"
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/core/snapshot.ts
|
|
28
|
+
import fs from "fs";
|
|
29
|
+
import path from "path";
|
|
30
|
+
import fg from "fast-glob";
|
|
31
|
+
|
|
32
|
+
// src/parsers/css-parser.ts
|
|
33
|
+
import * as csstree from "css-tree";
|
|
34
|
+
var CATEGORY_MAP = {
|
|
35
|
+
// Colors
|
|
36
|
+
"color": "color",
|
|
37
|
+
"background-color": "color",
|
|
38
|
+
"background": "color",
|
|
39
|
+
"border-color": "color",
|
|
40
|
+
"outline-color": "color",
|
|
41
|
+
"fill": "color",
|
|
42
|
+
"stroke": "color",
|
|
43
|
+
"--color": "color",
|
|
44
|
+
// Fonts
|
|
45
|
+
"font-family": "font",
|
|
46
|
+
"font-size": "font",
|
|
47
|
+
"font-weight": "font",
|
|
48
|
+
"line-height": "font",
|
|
49
|
+
"letter-spacing": "font",
|
|
50
|
+
// Spacing
|
|
51
|
+
"margin": "spacing",
|
|
52
|
+
"margin-top": "spacing",
|
|
53
|
+
"margin-right": "spacing",
|
|
54
|
+
"margin-bottom": "spacing",
|
|
55
|
+
"margin-left": "spacing",
|
|
56
|
+
"padding": "spacing",
|
|
57
|
+
"padding-top": "spacing",
|
|
58
|
+
"padding-right": "spacing",
|
|
59
|
+
"padding-bottom": "spacing",
|
|
60
|
+
"padding-left": "spacing",
|
|
61
|
+
"gap": "spacing",
|
|
62
|
+
"row-gap": "spacing",
|
|
63
|
+
"column-gap": "spacing",
|
|
64
|
+
// Shadows
|
|
65
|
+
"box-shadow": "shadow",
|
|
66
|
+
"text-shadow": "shadow",
|
|
67
|
+
// Radius
|
|
68
|
+
"border-radius": "radius",
|
|
69
|
+
"border-top-left-radius": "radius",
|
|
70
|
+
"border-top-right-radius": "radius",
|
|
71
|
+
"border-bottom-left-radius": "radius",
|
|
72
|
+
"border-bottom-right-radius": "radius",
|
|
73
|
+
// Layout
|
|
74
|
+
"display": "layout",
|
|
75
|
+
"flex-direction": "layout",
|
|
76
|
+
"justify-content": "layout",
|
|
77
|
+
"align-items": "layout",
|
|
78
|
+
"grid-template-columns": "layout",
|
|
79
|
+
"grid-template-rows": "layout",
|
|
80
|
+
"position": "layout"
|
|
81
|
+
};
|
|
82
|
+
function getCategory(property) {
|
|
83
|
+
if (CATEGORY_MAP[property]) {
|
|
84
|
+
return CATEGORY_MAP[property];
|
|
85
|
+
}
|
|
86
|
+
if (property.startsWith("--")) {
|
|
87
|
+
const lower = property.toLowerCase();
|
|
88
|
+
if (lower.includes("color") || lower.includes("bg") || lower.includes("text")) return "color";
|
|
89
|
+
if (lower.includes("font") || lower.includes("size") || lower.includes("weight")) return "font";
|
|
90
|
+
if (lower.includes("spacing") || lower.includes("margin") || lower.includes("padding") || lower.includes("gap")) return "spacing";
|
|
91
|
+
if (lower.includes("shadow")) return "shadow";
|
|
92
|
+
if (lower.includes("radius") || lower.includes("rounded")) return "radius";
|
|
93
|
+
return "other";
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function parseCss(cssContent, filePath) {
|
|
98
|
+
const tokens = [];
|
|
99
|
+
try {
|
|
100
|
+
const ast = csstree.parse(cssContent, {
|
|
101
|
+
filename: filePath,
|
|
102
|
+
positions: true
|
|
103
|
+
});
|
|
104
|
+
csstree.walk(ast, {
|
|
105
|
+
visit: "Declaration",
|
|
106
|
+
enter(node) {
|
|
107
|
+
const property = node.property;
|
|
108
|
+
const category = getCategory(property);
|
|
109
|
+
if (!category) return;
|
|
110
|
+
const value = csstree.generate(node.value);
|
|
111
|
+
if (!value || ["inherit", "initial", "unset", "revert"].includes(value)) return;
|
|
112
|
+
let selector = ":root";
|
|
113
|
+
let parent = this.atrule ?? this.rule;
|
|
114
|
+
if (parent && parent.type === "Rule" && parent.prelude) {
|
|
115
|
+
selector = csstree.generate(parent.prelude);
|
|
116
|
+
}
|
|
117
|
+
tokens.push({
|
|
118
|
+
category,
|
|
119
|
+
property,
|
|
120
|
+
value: value.trim(),
|
|
121
|
+
selector,
|
|
122
|
+
file: filePath,
|
|
123
|
+
line: node.loc?.start?.line
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(`Warning: Failed to parse CSS in ${filePath}: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
return tokens;
|
|
131
|
+
}
|
|
132
|
+
function extractCssVariables(cssContent, filePath) {
|
|
133
|
+
const tokens = [];
|
|
134
|
+
const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = varRegex.exec(cssContent)) !== null) {
|
|
137
|
+
const property = `--${match[1]}`;
|
|
138
|
+
const value = match[2].trim();
|
|
139
|
+
const category = getCategory(property) ?? "other";
|
|
140
|
+
tokens.push({
|
|
141
|
+
category,
|
|
142
|
+
property,
|
|
143
|
+
value,
|
|
144
|
+
selector: ":root",
|
|
145
|
+
file: filePath,
|
|
146
|
+
line: cssContent.substring(0, match.index).split("\n").length
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return tokens;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/parsers/html-parser.ts
|
|
153
|
+
import * as cheerio from "cheerio";
|
|
154
|
+
var TRACKED_PROPERTIES = {
|
|
155
|
+
"color": "color",
|
|
156
|
+
"background-color": "color",
|
|
157
|
+
"background": "color",
|
|
158
|
+
"border-color": "color",
|
|
159
|
+
"font-family": "font",
|
|
160
|
+
"font-size": "font",
|
|
161
|
+
"font-weight": "font",
|
|
162
|
+
"line-height": "font",
|
|
163
|
+
"margin": "spacing",
|
|
164
|
+
"margin-top": "spacing",
|
|
165
|
+
"margin-right": "spacing",
|
|
166
|
+
"margin-bottom": "spacing",
|
|
167
|
+
"margin-left": "spacing",
|
|
168
|
+
"padding": "spacing",
|
|
169
|
+
"padding-top": "spacing",
|
|
170
|
+
"padding-right": "spacing",
|
|
171
|
+
"padding-bottom": "spacing",
|
|
172
|
+
"padding-left": "spacing",
|
|
173
|
+
"gap": "spacing",
|
|
174
|
+
"box-shadow": "shadow",
|
|
175
|
+
"text-shadow": "shadow",
|
|
176
|
+
"border-radius": "radius",
|
|
177
|
+
"display": "layout",
|
|
178
|
+
"flex-direction": "layout",
|
|
179
|
+
"justify-content": "layout",
|
|
180
|
+
"align-items": "layout"
|
|
181
|
+
};
|
|
182
|
+
function parseInlineStyle(styleStr) {
|
|
183
|
+
const result = {};
|
|
184
|
+
const declarations = styleStr.split(";").filter(Boolean);
|
|
185
|
+
for (const decl of declarations) {
|
|
186
|
+
const colonIdx = decl.indexOf(":");
|
|
187
|
+
if (colonIdx === -1) continue;
|
|
188
|
+
const prop = decl.substring(0, colonIdx).trim().toLowerCase();
|
|
189
|
+
const val = decl.substring(colonIdx + 1).trim();
|
|
190
|
+
if (prop && val) {
|
|
191
|
+
result[prop] = val;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
function parseHtml(htmlContent, filePath) {
|
|
197
|
+
const tokens = [];
|
|
198
|
+
const $ = cheerio.load(htmlContent);
|
|
199
|
+
const styleBlocks = [];
|
|
200
|
+
$("style").each((_, el) => {
|
|
201
|
+
const text = $(el).text();
|
|
202
|
+
if (text) {
|
|
203
|
+
styleBlocks.push(text);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
$("[style]").each((_, el) => {
|
|
207
|
+
const element = $(el);
|
|
208
|
+
const styleStr = element.attr("style");
|
|
209
|
+
if (!styleStr) return;
|
|
210
|
+
const selector = buildSelectorPath($, element);
|
|
211
|
+
const styles = parseInlineStyle(styleStr);
|
|
212
|
+
for (const [prop, value] of Object.entries(styles)) {
|
|
213
|
+
const category = TRACKED_PROPERTIES[prop];
|
|
214
|
+
if (!category) continue;
|
|
215
|
+
tokens.push({
|
|
216
|
+
category,
|
|
217
|
+
property: prop,
|
|
218
|
+
value,
|
|
219
|
+
selector: `[inline] ${selector}`,
|
|
220
|
+
file: filePath
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return tokens;
|
|
225
|
+
}
|
|
226
|
+
function extractStyleBlocks(htmlContent) {
|
|
227
|
+
const $ = cheerio.load(htmlContent);
|
|
228
|
+
const blocks = [];
|
|
229
|
+
$("style").each((_, el) => {
|
|
230
|
+
const text = $(el).text();
|
|
231
|
+
if (text) {
|
|
232
|
+
blocks.push(text);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return blocks;
|
|
236
|
+
}
|
|
237
|
+
function buildSelectorPath($, element) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
const tagName = element.prop("tagName")?.toLowerCase() ?? "div";
|
|
240
|
+
const id = element.attr("id");
|
|
241
|
+
const className = element.attr("class");
|
|
242
|
+
let sel = tagName;
|
|
243
|
+
if (id) {
|
|
244
|
+
sel += `#${id}`;
|
|
245
|
+
} else if (className) {
|
|
246
|
+
const classList = className.split(/\s+/).slice(0, 2).join(".");
|
|
247
|
+
sel += `.${classList}`;
|
|
248
|
+
}
|
|
249
|
+
parts.push(sel);
|
|
250
|
+
return parts.join(" > ");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/core/snapshot.ts
|
|
254
|
+
var SNAPSHOT_DIR = ".design-guard";
|
|
255
|
+
var SNAPSHOT_FILE = "snapshot.json";
|
|
256
|
+
var CONFIG_FILE = "config.json";
|
|
257
|
+
function getSnapshotPath(projectRoot) {
|
|
258
|
+
return path.join(projectRoot, SNAPSHOT_DIR, SNAPSHOT_FILE);
|
|
259
|
+
}
|
|
260
|
+
function getConfigPath(projectRoot) {
|
|
261
|
+
return path.join(projectRoot, SNAPSHOT_DIR, CONFIG_FILE);
|
|
262
|
+
}
|
|
263
|
+
function loadConfig(projectRoot) {
|
|
264
|
+
const configPath = getConfigPath(projectRoot);
|
|
265
|
+
if (fs.existsSync(configPath)) {
|
|
266
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
267
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
268
|
+
}
|
|
269
|
+
return { ...DEFAULT_CONFIG };
|
|
270
|
+
}
|
|
271
|
+
function saveConfig(projectRoot, config) {
|
|
272
|
+
const dir = path.join(projectRoot, SNAPSHOT_DIR);
|
|
273
|
+
if (!fs.existsSync(dir)) {
|
|
274
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
fs.writeFileSync(getConfigPath(projectRoot), JSON.stringify(config, null, 2), "utf-8");
|
|
277
|
+
}
|
|
278
|
+
async function scanProject(projectRoot, config, stitchHtmlPath) {
|
|
279
|
+
const allTokens = [];
|
|
280
|
+
const scannedFiles = [];
|
|
281
|
+
if (stitchHtmlPath) {
|
|
282
|
+
const absPath = path.resolve(projectRoot, stitchHtmlPath);
|
|
283
|
+
if (fs.existsSync(absPath)) {
|
|
284
|
+
const htmlContent = fs.readFileSync(absPath, "utf-8");
|
|
285
|
+
const htmlTokens = parseHtml(htmlContent, stitchHtmlPath);
|
|
286
|
+
allTokens.push(...htmlTokens);
|
|
287
|
+
const styleBlocks = extractStyleBlocks(htmlContent);
|
|
288
|
+
for (const block of styleBlocks) {
|
|
289
|
+
const cssTokens = parseCss(block, stitchHtmlPath);
|
|
290
|
+
allTokens.push(...cssTokens);
|
|
291
|
+
const vars = extractCssVariables(block, stitchHtmlPath);
|
|
292
|
+
allTokens.push(...vars);
|
|
293
|
+
}
|
|
294
|
+
scannedFiles.push(stitchHtmlPath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const cssFiles = await fg(config.cssFiles, {
|
|
298
|
+
cwd: projectRoot,
|
|
299
|
+
ignore: config.ignore,
|
|
300
|
+
absolute: false
|
|
301
|
+
});
|
|
302
|
+
for (const file of cssFiles) {
|
|
303
|
+
const absPath = path.join(projectRoot, file);
|
|
304
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
305
|
+
const tokens = parseCss(content, file);
|
|
306
|
+
allTokens.push(...tokens);
|
|
307
|
+
const vars = extractCssVariables(content, file);
|
|
308
|
+
allTokens.push(...vars);
|
|
309
|
+
scannedFiles.push(file);
|
|
310
|
+
}
|
|
311
|
+
const htmlFiles = await fg(config.htmlFiles, {
|
|
312
|
+
cwd: projectRoot,
|
|
313
|
+
ignore: config.ignore,
|
|
314
|
+
absolute: false
|
|
315
|
+
});
|
|
316
|
+
for (const file of htmlFiles) {
|
|
317
|
+
if (scannedFiles.includes(file)) continue;
|
|
318
|
+
const absPath = path.join(projectRoot, file);
|
|
319
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
320
|
+
const tokens = parseHtml(content, file);
|
|
321
|
+
allTokens.push(...tokens);
|
|
322
|
+
const styleBlocks = extractStyleBlocks(content);
|
|
323
|
+
for (const block of styleBlocks) {
|
|
324
|
+
const cssTokens = parseCss(block, file);
|
|
325
|
+
allTokens.push(...cssTokens);
|
|
326
|
+
}
|
|
327
|
+
scannedFiles.push(file);
|
|
328
|
+
}
|
|
329
|
+
const filtered = allTokens.filter(
|
|
330
|
+
(t) => config.trackCategories.includes(t.category)
|
|
331
|
+
);
|
|
332
|
+
const seen = /* @__PURE__ */ new Set();
|
|
333
|
+
const deduplicated = filtered.filter((t) => {
|
|
334
|
+
const key = `${t.file}:${t.selector}:${t.property}`;
|
|
335
|
+
if (seen.has(key)) return false;
|
|
336
|
+
seen.add(key);
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
return { tokens: deduplicated, files: scannedFiles };
|
|
340
|
+
}
|
|
341
|
+
function buildSummary(tokens) {
|
|
342
|
+
const summary = {
|
|
343
|
+
color: 0,
|
|
344
|
+
font: 0,
|
|
345
|
+
spacing: 0,
|
|
346
|
+
shadow: 0,
|
|
347
|
+
radius: 0,
|
|
348
|
+
layout: 0,
|
|
349
|
+
other: 0
|
|
350
|
+
};
|
|
351
|
+
for (const token of tokens) {
|
|
352
|
+
summary[token.category]++;
|
|
353
|
+
}
|
|
354
|
+
return summary;
|
|
355
|
+
}
|
|
356
|
+
async function createSnapshot(projectRoot, stitchHtmlPath) {
|
|
357
|
+
const config = loadConfig(projectRoot);
|
|
358
|
+
const { tokens, files } = await scanProject(projectRoot, config, stitchHtmlPath);
|
|
359
|
+
const snapshot = {
|
|
360
|
+
version: "1.0.0",
|
|
361
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
362
|
+
projectRoot,
|
|
363
|
+
sourceFiles: files,
|
|
364
|
+
tokens,
|
|
365
|
+
summary: buildSummary(tokens)
|
|
366
|
+
};
|
|
367
|
+
return snapshot;
|
|
368
|
+
}
|
|
369
|
+
function saveSnapshot(projectRoot, snapshot) {
|
|
370
|
+
const dir = path.join(projectRoot, SNAPSHOT_DIR);
|
|
371
|
+
if (!fs.existsSync(dir)) {
|
|
372
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
373
|
+
}
|
|
374
|
+
const snapshotPath = getSnapshotPath(projectRoot);
|
|
375
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
376
|
+
return snapshotPath;
|
|
377
|
+
}
|
|
378
|
+
function loadSnapshot(projectRoot) {
|
|
379
|
+
const snapshotPath = getSnapshotPath(projectRoot);
|
|
380
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
const raw = fs.readFileSync(snapshotPath, "utf-8");
|
|
384
|
+
return JSON.parse(raw);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/core/drift.ts
|
|
388
|
+
function tokenKey(token) {
|
|
389
|
+
return `${token.file}::${token.selector}::${token.property}`;
|
|
390
|
+
}
|
|
391
|
+
async function detectDrift(projectRoot, snapshot, threshold = 10) {
|
|
392
|
+
const config = loadConfig(projectRoot);
|
|
393
|
+
const { tokens: currentTokens } = await scanProject(projectRoot, config);
|
|
394
|
+
const snapshotMap = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const token of snapshot.tokens) {
|
|
396
|
+
snapshotMap.set(tokenKey(token), token);
|
|
397
|
+
}
|
|
398
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
399
|
+
for (const token of currentTokens) {
|
|
400
|
+
currentMap.set(tokenKey(token), token);
|
|
401
|
+
}
|
|
402
|
+
const driftItems = [];
|
|
403
|
+
for (const [key, original] of snapshotMap) {
|
|
404
|
+
const current = currentMap.get(key);
|
|
405
|
+
if (!current) {
|
|
406
|
+
driftItems.push({
|
|
407
|
+
original,
|
|
408
|
+
current: null,
|
|
409
|
+
changeType: "deleted"
|
|
410
|
+
});
|
|
411
|
+
} else if (current.value !== original.value) {
|
|
412
|
+
driftItems.push({
|
|
413
|
+
original,
|
|
414
|
+
current,
|
|
415
|
+
changeType: "modified"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const totalTokens = snapshot.tokens.length;
|
|
420
|
+
const changedTokens = driftItems.length;
|
|
421
|
+
const driftScore = totalTokens > 0 ? Math.round(changedTokens / totalTokens * 100 * 100) / 100 : 0;
|
|
422
|
+
const categorySummary = {};
|
|
423
|
+
const categories = ["color", "font", "spacing", "shadow", "radius", "layout", "other"];
|
|
424
|
+
for (const cat of categories) {
|
|
425
|
+
const total = snapshot.tokens.filter((t) => t.category === cat).length;
|
|
426
|
+
const changed = driftItems.filter((d) => d.original.category === cat).length;
|
|
427
|
+
categorySummary[cat] = {
|
|
428
|
+
total,
|
|
429
|
+
changed,
|
|
430
|
+
driftPercent: total > 0 ? Math.round(changed / total * 100 * 100) / 100 : 0
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
435
|
+
snapshotCreatedAt: snapshot.createdAt,
|
|
436
|
+
totalTokens,
|
|
437
|
+
changedTokens,
|
|
438
|
+
driftScore,
|
|
439
|
+
threshold,
|
|
440
|
+
passed: driftScore <= threshold,
|
|
441
|
+
items: driftItems,
|
|
442
|
+
categorySummary
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/core/rules-generator.ts
|
|
447
|
+
import fs2 from "fs";
|
|
448
|
+
import path2 from "path";
|
|
449
|
+
function generateRules(snapshot, format) {
|
|
450
|
+
const generators = {
|
|
451
|
+
"cursorrules": () => generateCursorRules(snapshot),
|
|
452
|
+
"claude-md": () => generateClaudeMd(snapshot),
|
|
453
|
+
"agents-md": () => generateAgentsMd(snapshot),
|
|
454
|
+
"copilot": () => generateCopilotInstructions(snapshot),
|
|
455
|
+
"clinerules": () => generateClineRules(snapshot)
|
|
456
|
+
};
|
|
457
|
+
return generators[format]();
|
|
458
|
+
}
|
|
459
|
+
function saveRules(projectRoot, format, content, append = false) {
|
|
460
|
+
const fileMap = {
|
|
461
|
+
"cursorrules": ".cursorrules",
|
|
462
|
+
"claude-md": "CLAUDE.md",
|
|
463
|
+
"agents-md": "AGENTS.md",
|
|
464
|
+
"copilot": ".github/copilot-instructions.md",
|
|
465
|
+
"clinerules": ".clinerules"
|
|
466
|
+
};
|
|
467
|
+
const filePath = path2.join(projectRoot, fileMap[format]);
|
|
468
|
+
const dir = path2.dirname(filePath);
|
|
469
|
+
if (!fs2.existsSync(dir)) {
|
|
470
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
471
|
+
}
|
|
472
|
+
if (append && fs2.existsSync(filePath)) {
|
|
473
|
+
const existing = fs2.readFileSync(filePath, "utf-8");
|
|
474
|
+
fs2.writeFileSync(filePath, existing + "\n\n" + content, "utf-8");
|
|
475
|
+
} else {
|
|
476
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
477
|
+
}
|
|
478
|
+
return filePath;
|
|
479
|
+
}
|
|
480
|
+
function buildTokenList(snapshot) {
|
|
481
|
+
const colorTokens = snapshot.tokens.filter((t) => t.category === "color");
|
|
482
|
+
const fontTokens = snapshot.tokens.filter((t) => t.category === "font");
|
|
483
|
+
const spacingTokens = snapshot.tokens.filter((t) => t.category === "spacing");
|
|
484
|
+
const radiusTokens = snapshot.tokens.filter((t) => t.category === "radius");
|
|
485
|
+
const lines = [];
|
|
486
|
+
if (colorTokens.length > 0) {
|
|
487
|
+
lines.push("### Colors (DO NOT CHANGE)");
|
|
488
|
+
const unique = [...new Set(colorTokens.map((t) => `${t.property}: ${t.value}`))];
|
|
489
|
+
for (const t of unique.slice(0, 30)) {
|
|
490
|
+
lines.push(`- \`${t}\``);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (fontTokens.length > 0) {
|
|
494
|
+
lines.push("\n### Fonts (DO NOT CHANGE)");
|
|
495
|
+
const unique = [...new Set(fontTokens.map((t) => `${t.property}: ${t.value}`))];
|
|
496
|
+
for (const t of unique.slice(0, 15)) {
|
|
497
|
+
lines.push(`- \`${t}\``);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (spacingTokens.length > 0) {
|
|
501
|
+
lines.push("\n### Spacing (DO NOT CHANGE)");
|
|
502
|
+
const unique = [...new Set(spacingTokens.map((t) => `${t.property}: ${t.value}`))];
|
|
503
|
+
for (const t of unique.slice(0, 20)) {
|
|
504
|
+
lines.push(`- \`${t}\``);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (radiusTokens.length > 0) {
|
|
508
|
+
lines.push("\n### Border Radius (DO NOT CHANGE)");
|
|
509
|
+
const unique = [...new Set(radiusTokens.map((t) => `${t.property}: ${t.value}`))];
|
|
510
|
+
for (const t of unique.slice(0, 10)) {
|
|
511
|
+
lines.push(`- \`${t}\``);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return lines.join("\n");
|
|
515
|
+
}
|
|
516
|
+
function generateCursorRules(snapshot) {
|
|
517
|
+
return `# Design Guard \u2014 Locked Design Tokens
|
|
518
|
+
# Generated by drift-guard on ${snapshot.createdAt}
|
|
519
|
+
# DO NOT modify these values when adding features or fixing bugs.
|
|
520
|
+
|
|
521
|
+
## CRITICAL: Design Drift Prevention
|
|
522
|
+
|
|
523
|
+
When modifying this codebase, you MUST preserve the following design tokens exactly as they are.
|
|
524
|
+
Do NOT change colors, fonts, spacing, or border-radius values unless explicitly asked to update the design.
|
|
525
|
+
|
|
526
|
+
If you need to add new styles, use the existing design tokens as reference.
|
|
527
|
+
If a design change is needed, tell the user to run \`drift-guard snapshot update\` after making the change.
|
|
528
|
+
|
|
529
|
+
${buildTokenList(snapshot)}
|
|
530
|
+
|
|
531
|
+
## Rules
|
|
532
|
+
1. NEVER change existing CSS color values
|
|
533
|
+
2. NEVER change font-family, font-size, or font-weight values
|
|
534
|
+
3. NEVER change margin, padding, or gap values on existing elements
|
|
535
|
+
4. NEVER change border-radius values
|
|
536
|
+
5. When adding new components, use the SAME design tokens listed above
|
|
537
|
+
6. If you must change a design token, warn the user first
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
function generateClaudeMd(snapshot) {
|
|
541
|
+
return `# Design Guard \u2014 Locked Design Tokens
|
|
542
|
+
|
|
543
|
+
> Generated by drift-guard on ${snapshot.createdAt}
|
|
544
|
+
> Run \`npx drift-guard check\` to verify design consistency
|
|
545
|
+
|
|
546
|
+
## Design Drift Prevention Rules
|
|
547
|
+
|
|
548
|
+
When working on this codebase, preserve the existing design exactly as-is.
|
|
549
|
+
The following design tokens are LOCKED and should NOT be modified:
|
|
550
|
+
|
|
551
|
+
${buildTokenList(snapshot)}
|
|
552
|
+
|
|
553
|
+
## Important
|
|
554
|
+
- Adding new features should NOT change existing visual styles
|
|
555
|
+
- If a CSS change affects existing components, flag it to the user
|
|
556
|
+
- New components should follow the same design language (colors, fonts, spacing)
|
|
557
|
+
- Run \`npx drift-guard check\` before committing to verify no design drift
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
function generateAgentsMd(snapshot) {
|
|
561
|
+
return `# AGENTS.md \u2014 Design Guard Rules
|
|
562
|
+
|
|
563
|
+
> Auto-generated by drift-guard. Do not edit manually.
|
|
564
|
+
> Snapshot: ${snapshot.createdAt} | Tokens: ${snapshot.tokens.length}
|
|
565
|
+
|
|
566
|
+
## Design Token Protection
|
|
567
|
+
|
|
568
|
+
This project uses drift-guard to prevent design drift during AI-assisted development.
|
|
569
|
+
The following design tokens are protected:
|
|
570
|
+
|
|
571
|
+
${buildTokenList(snapshot)}
|
|
572
|
+
|
|
573
|
+
## Agent Instructions
|
|
574
|
+
1. Do NOT modify existing CSS values for colors, fonts, spacing, or border-radius
|
|
575
|
+
2. New components MUST use the same design tokens
|
|
576
|
+
3. Run \`npx drift-guard check\` before suggesting commits
|
|
577
|
+
4. If design changes are intentional, inform the user to update the snapshot
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
function generateCopilotInstructions(snapshot) {
|
|
581
|
+
return `# Copilot Instructions \u2014 Design Guard
|
|
582
|
+
|
|
583
|
+
This project has locked design tokens. When suggesting code changes:
|
|
584
|
+
|
|
585
|
+
1. Preserve all existing CSS color, font, spacing, and border-radius values
|
|
586
|
+
2. Use the project's design tokens for new components
|
|
587
|
+
3. Do not override CSS custom properties (variables starting with --)
|
|
588
|
+
|
|
589
|
+
Protected tokens (${snapshot.tokens.length} total):
|
|
590
|
+
|
|
591
|
+
${buildTokenList(snapshot)}
|
|
592
|
+
`;
|
|
593
|
+
}
|
|
594
|
+
function generateClineRules(snapshot) {
|
|
595
|
+
return `# Design Guard \u2014 Protected Design Tokens
|
|
596
|
+
# Generated: ${snapshot.createdAt}
|
|
597
|
+
|
|
598
|
+
DESIGN_PROTECTION=true
|
|
599
|
+
|
|
600
|
+
## Rules
|
|
601
|
+
- Do NOT change existing CSS values
|
|
602
|
+
- Preserve colors: ${snapshot.summary.color} tokens locked
|
|
603
|
+
- Preserve fonts: ${snapshot.summary.font} tokens locked
|
|
604
|
+
- Preserve spacing: ${snapshot.summary.spacing} tokens locked
|
|
605
|
+
- Preserve radius: ${snapshot.summary.radius} tokens locked
|
|
606
|
+
|
|
607
|
+
## Verification
|
|
608
|
+
Run: npx drift-guard check
|
|
609
|
+
Threshold: Changes to more than 10% of design tokens will be flagged
|
|
610
|
+
|
|
611
|
+
${buildTokenList(snapshot)}
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export {
|
|
616
|
+
DEFAULT_CONFIG,
|
|
617
|
+
loadConfig,
|
|
618
|
+
saveConfig,
|
|
619
|
+
scanProject,
|
|
620
|
+
createSnapshot,
|
|
621
|
+
saveSnapshot,
|
|
622
|
+
loadSnapshot,
|
|
623
|
+
detectDrift,
|
|
624
|
+
generateRules,
|
|
625
|
+
saveRules
|
|
626
|
+
};
|
|
627
|
+
//# sourceMappingURL=chunk-27T45SVD.js.map
|