quiver-cli 1.2.0 → 1.3.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.
Files changed (121) hide show
  1. package/dist/cli.js +237 -10
  2. package/package.json +1 -1
  3. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  4. package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
  5. package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
  6. package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
  7. package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
  8. package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
  9. package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
  10. package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
  11. package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
  12. package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
  13. package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
  14. package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
  15. package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
  16. package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
  17. package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
  18. package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
  19. package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
  21. package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
  22. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
  23. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
  24. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  25. package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
  26. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  27. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  28. package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
  29. package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
  30. package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
  31. package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
  32. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  33. package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
  34. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  35. package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
  36. package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
  37. package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
  38. package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
  39. package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
  40. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
  41. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
  42. package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
  43. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
  44. package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
  45. package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
  46. package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
  47. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
  48. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
  49. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
  50. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
  51. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
  52. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
  53. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
  54. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
  55. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
  56. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
  57. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
  58. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
  59. package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
  60. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
  61. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
  62. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
  63. package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
  64. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
  65. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
  66. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
  67. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
  68. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
  69. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
  70. package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
  71. package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
  72. package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
  73. package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
  74. package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
  76. package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
  77. package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
  78. package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
  79. package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
  85. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
  86. package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
  88. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
  89. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
  90. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
  91. package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
  92. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
  93. package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
  95. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
  96. package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
  97. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
  98. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
  99. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
  100. package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
  101. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
  102. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
  103. package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
  104. package/template/.agents/skills/hono/SKILL.md +17 -3
  105. package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
  106. package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
  107. package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
  108. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
  109. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
  110. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
  111. package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
  112. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
  113. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
  114. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
  115. package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
  116. package/template/.agents/skills/supabase/SKILL.md +5 -1
  117. package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
  118. package/template/.agents/skills/writing/humanizer/README.md +209 -0
  119. package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
  120. package/template/.agents/upstreams.json +21 -22
  121. package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
@@ -41,6 +41,369 @@ function shouldRunPageAnalyzers(content, filePath) {
41
41
  return !ext || PAGE_ANALYZER_EXTS.has(ext);
42
42
  }
43
43
 
44
+ const JS_SOURCE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
45
+ const STYLESHEET_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
46
+ const REGEX_PREFIX_KEYWORDS = new Set(['await', 'case', 'default', 'delete', 'do', 'else', 'in', 'instanceof', 'new', 'of', 'return', 'throw', 'typeof', 'void', 'yield']);
47
+ const BLOCK_BRACE_PREFIX_KEYWORDS = new Set(['do', 'else', 'finally', 'try']);
48
+
49
+ function isInsideOpeningJsxTag(source) {
50
+ const tagStart = source.lastIndexOf('<');
51
+ if (tagStart === -1 || !/^<[A-Za-z][\w.:-]*/.test(source.slice(tagStart))) return false;
52
+
53
+ let quote = '';
54
+ for (let cursor = tagStart + 1; cursor < source.length; cursor++) {
55
+ const char = source[cursor];
56
+ if (quote) {
57
+ if (char === '\\') cursor++;
58
+ else if (char === quote) quote = '';
59
+ } else if (char === "'" || char === '"') {
60
+ quote = char;
61
+ } else if (char === '>') {
62
+ return false;
63
+ }
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * Blank JavaScript comments without moving any following source. Regex
70
+ * findings keep their original line numbers, while prose examples inside
71
+ * comments cannot masquerade as rendered markup.
72
+ */
73
+ function stripJsComments(content, options = {}) {
74
+ let state = 'code';
75
+ let output = '';
76
+ let lastSignificant = '';
77
+ let previousSignificant = '';
78
+ let antePreviousSignificant = '';
79
+ let currentWord = '';
80
+ let currentWordPrefix = '';
81
+ let wordSeparated = false;
82
+ let regexCharClass = false;
83
+ let jsxExpressionDepth = 0;
84
+ let lastClosedBraceKind = '';
85
+ const braceKinds = [];
86
+ const templateExpressionDepths = [];
87
+
88
+ const braceKind = (startsJsxExpression = false) => (
89
+ !startsJsxExpression && (
90
+ !lastSignificant ||
91
+ lastSignificant === ')' ||
92
+ lastSignificant === ';' ||
93
+ lastSignificant === '}' ||
94
+ (previousSignificant === '=' && lastSignificant === '>') ||
95
+ BLOCK_BRACE_PREFIX_KEYWORDS.has(currentWord)
96
+ ) ? 'block' : 'expression'
97
+ );
98
+
99
+ const recordSignificant = (char) => {
100
+ if (/\s/.test(char)) {
101
+ wordSeparated = true;
102
+ return;
103
+ }
104
+ const isWordChar = /[\w$]/.test(char);
105
+ if (isWordChar && (wordSeparated || !currentWord)) {
106
+ currentWord = '';
107
+ currentWordPrefix = lastSignificant;
108
+ } else if (!isWordChar) {
109
+ currentWordPrefix = '';
110
+ }
111
+ wordSeparated = false;
112
+ antePreviousSignificant = previousSignificant;
113
+ previousSignificant = lastSignificant;
114
+ lastSignificant = char;
115
+ currentWord = isWordChar ? currentWord + char : '';
116
+ };
117
+
118
+ for (let i = 0; i < content.length; i++) {
119
+ const char = content[i];
120
+ const next = content[i + 1];
121
+
122
+ if (state === 'line-comment') {
123
+ if (char === '\n') {
124
+ output += char;
125
+ state = 'code';
126
+ } else {
127
+ output += ' ';
128
+ }
129
+ continue;
130
+ }
131
+
132
+ if (state === 'block-comment') {
133
+ if (char === '*' && next === '/') {
134
+ output += ' ';
135
+ i++;
136
+ state = 'code';
137
+ } else {
138
+ output += char === '\n' ? '\n' : ' ';
139
+ }
140
+ continue;
141
+ }
142
+
143
+ if (state === 'regex') {
144
+ output += char;
145
+ if (char === '\\' && next) {
146
+ output += next;
147
+ i++;
148
+ } else if (char === '[') {
149
+ regexCharClass = true;
150
+ } else if (char === ']') {
151
+ regexCharClass = false;
152
+ } else if (char === '/' && !regexCharClass) {
153
+ state = 'code';
154
+ recordSignificant('/');
155
+ }
156
+ continue;
157
+ }
158
+
159
+ if (state === 'template' && char === '$' && next === '{') {
160
+ output += '${';
161
+ i++;
162
+ recordSignificant('$');
163
+ recordSignificant('{');
164
+ templateExpressionDepths.push(1);
165
+ braceKinds.push('expression');
166
+ if (jsxExpressionDepth) jsxExpressionDepth++;
167
+ state = 'code';
168
+ continue;
169
+ }
170
+
171
+ if (state !== 'code') {
172
+ output += char;
173
+ if (char === '\\' && next) {
174
+ output += next;
175
+ i++;
176
+ } else if (
177
+ (state === 'single-quote' && char === "'") ||
178
+ (state === 'double-quote' && char === '"') ||
179
+ (state === 'template' && char === '`')
180
+ ) {
181
+ state = 'code';
182
+ recordSignificant(char);
183
+ }
184
+ continue;
185
+ }
186
+
187
+ const jsxUrlSeparator = options.jsx && char === '/' && next === '/' &&
188
+ jsxExpressionDepth === 0 &&
189
+ (output.endsWith('http:') ||
190
+ output.endsWith('https:') ||
191
+ (/<[A-Za-z](?:[^>]*[^/])?>[^<]*$/.test(output.slice(output.lastIndexOf('\n') + 1)) &&
192
+ /^[\w.-]+\.[A-Za-z]{2,}(?=[:/?#\s<]|$)/.test(content.slice(i + 2))));
193
+ const afterPostfixUpdate = (lastSignificant === '+' || lastSignificant === '-') &&
194
+ previousSignificant === lastSignificant &&
195
+ antePreviousSignificant !== lastSignificant;
196
+ if (char === '/' && next === '/' && jsxUrlSeparator) {
197
+ output += '//';
198
+ i++;
199
+ recordSignificant('/');
200
+ recordSignificant('/');
201
+ } else if (char === '/' && next === '/') {
202
+ output += ' ';
203
+ i++;
204
+ state = 'line-comment';
205
+ } else if (char === '/' && next === '*') {
206
+ output += ' ';
207
+ i++;
208
+ state = 'block-comment';
209
+ } else if (templateExpressionDepths.length && char === '{') {
210
+ output += char;
211
+ templateExpressionDepths[templateExpressionDepths.length - 1]++;
212
+ braceKinds.push(braceKind());
213
+ if (jsxExpressionDepth) jsxExpressionDepth++;
214
+ recordSignificant(char);
215
+ } else if (templateExpressionDepths.length && char === '}') {
216
+ output += char;
217
+ const depthIndex = templateExpressionDepths.length - 1;
218
+ templateExpressionDepths[depthIndex]--;
219
+ lastClosedBraceKind = braceKinds.pop() || '';
220
+ if (jsxExpressionDepth) jsxExpressionDepth--;
221
+ recordSignificant(char);
222
+ if (templateExpressionDepths[depthIndex] === 0) {
223
+ templateExpressionDepths.pop();
224
+ state = 'template';
225
+ }
226
+ } else if (
227
+ char === '/' &&
228
+ (!lastSignificant ||
229
+ (/[=([{!?:;,&|+\-*%^~<>]/.test(lastSignificant) && !afterPostfixUpdate) ||
230
+ (lastSignificant === '}' && lastClosedBraceKind === 'block') ||
231
+ (previousSignificant === '=' && lastSignificant === '>') ||
232
+ (currentWordPrefix !== '.' && REGEX_PREFIX_KEYWORDS.has(currentWord)))
233
+ ) {
234
+ output += char;
235
+ state = 'regex';
236
+ regexCharClass = false;
237
+ } else {
238
+ output += char;
239
+ const startsJsxExpression = options.jsx && char === '{' && jsxExpressionDepth === 0 &&
240
+ (/<[A-Za-z](?:[^>]*[^/])?>[^<]*$/.test(output.slice(output.lastIndexOf('\n') + 1, -1)) ||
241
+ isInsideOpeningJsxTag(output.slice(0, -1)));
242
+ if (char === '{') braceKinds.push(braceKind(startsJsxExpression));
243
+ else if (char === '}') lastClosedBraceKind = braceKinds.pop() || '';
244
+ if (char === '{' && (jsxExpressionDepth || startsJsxExpression)) jsxExpressionDepth++;
245
+ else if (char === '}' && jsxExpressionDepth) jsxExpressionDepth--;
246
+ recordSignificant(char);
247
+ if (char === "'") state = 'single-quote';
248
+ else if (char === '"') state = 'double-quote';
249
+ else if (char === '`') state = 'template';
250
+ }
251
+ }
252
+
253
+ return output;
254
+ }
255
+
256
+ function stripCssComments(content) {
257
+ return content.replace(/\/\*[\s\S]*?\*\//g, comment => comment.replace(/[^\n]/g, ' '));
258
+ }
259
+
260
+ function blankHtmlComments(text) {
261
+ return text.replace(/<!--[\s\S]*?-->/g, comment => comment.replace(/[^\n]/g, ' '));
262
+ }
263
+
264
+ function blankCssLineCommentsInStyleBlocks(text) {
265
+ const re = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
266
+ let output = '';
267
+ let lastIndex = 0;
268
+ let match;
269
+ while ((match = re.exec(text)) !== null) {
270
+ const inner = match[1];
271
+ const openLength = match[0].length - inner.length - '</style>'.length;
272
+ output += text.slice(lastIndex, match.index);
273
+ output += match[0].slice(0, openLength);
274
+ output += blankCssLineComments(inner);
275
+ output += match[0].slice(openLength + inner.length);
276
+ lastIndex = re.lastIndex;
277
+ }
278
+ return output + text.slice(lastIndex);
279
+ }
280
+
281
+ function blankHtmlAndCssCommentsOutsideScripts(text) {
282
+ const re = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
283
+ let output = '';
284
+ let lastIndex = 0;
285
+ let match;
286
+ while ((match = re.exec(text)) !== null) {
287
+ output += blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex, match.index))));
288
+ output += match[0];
289
+ lastIndex = re.lastIndex;
290
+ }
291
+ return output + blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex))));
292
+ }
293
+
294
+ function blankCssLineComments(text) {
295
+ let output = '';
296
+ let state = 'code';
297
+ let urlDepth = 0;
298
+ for (let i = 0; i < text.length; i++) {
299
+ const char = text[i];
300
+ const next = text[i + 1];
301
+ if (state === 'line') {
302
+ if (char === '\n') {
303
+ output += '\n';
304
+ state = 'code';
305
+ } else {
306
+ output += ' ';
307
+ }
308
+ continue;
309
+ }
310
+ if (state === 'single' || state === 'double') {
311
+ output += char;
312
+ if (char === '\\' && next) {
313
+ output += next;
314
+ i++;
315
+ } else if ((state === 'single' && char === "'") || (state === 'double' && char === '"')) {
316
+ state = 'code';
317
+ }
318
+ continue;
319
+ }
320
+ const prev = output.length ? output[output.length - 1] : '';
321
+ if (char === '/' && next === '/' && urlDepth === 0 && prev !== ':' && prev !== '(' && prev !== '\\') {
322
+ output += ' ';
323
+ i++;
324
+ state = 'line';
325
+ continue;
326
+ }
327
+ if (char === "'") state = 'single';
328
+ else if (char === '"') state = 'double';
329
+ if (char === '(') {
330
+ const behind = output.replace(/\s+$/, '');
331
+ if (urlDepth > 0 || /url$/i.test(behind)) urlDepth++;
332
+ } else if (char === ')' && urlDepth) {
333
+ urlDepth--;
334
+ }
335
+ output += char;
336
+ }
337
+ return output;
338
+ }
339
+
340
+ function findAstroFrontmatterClose(text) {
341
+ if (!text.startsWith('---')) return -1;
342
+ let cursor = text.indexOf('\n');
343
+ if (cursor === -1) return -1;
344
+ cursor += 1;
345
+ while (cursor < text.length) {
346
+ if (text[cursor - 1] === '\n' && text.startsWith('---', cursor)) {
347
+ let end = cursor + 3;
348
+ while (text[end] === ' ' || text[end] === '\t') end++;
349
+ if (end >= text.length || text[end] === '\n' || text[end] === '\r') return cursor - 1;
350
+ }
351
+ const char = text[cursor];
352
+ const next = text[cursor + 1];
353
+ if (char === "'" || char === '"') {
354
+ const close = findQuotedStringEnd(text, cursor, char);
355
+ if (close === -1) return -1;
356
+ cursor = close + 1;
357
+ continue;
358
+ }
359
+ if (char === '`') {
360
+ const close = findTemplateLiteralEnd(text, cursor);
361
+ if (close === -1) return -1;
362
+ cursor = close + 1;
363
+ continue;
364
+ }
365
+ if (char === '/' && next === '/') {
366
+ const lineEnd = text.indexOf('\n', cursor);
367
+ if (lineEnd === -1) return -1;
368
+ cursor = lineEnd;
369
+ continue;
370
+ }
371
+ if (char === '/' && next === '*') {
372
+ const commentEnd = text.indexOf('*/', cursor + 2);
373
+ if (commentEnd === -1) return -1;
374
+ cursor = commentEnd + 2;
375
+ continue;
376
+ }
377
+ if (char === '/' && next !== '/' && next !== '*') {
378
+ const close = findRegexLiteralEnd(text, cursor);
379
+ if (close !== -1) {
380
+ cursor = close + 1;
381
+ continue;
382
+ }
383
+ }
384
+ cursor++;
385
+ }
386
+ return -1;
387
+ }
388
+
389
+ function blankAstroFrontmatterComments(text) {
390
+ const close = findAstroFrontmatterClose(text);
391
+ if (close === -1) return text;
392
+ return stripJsComments(text.slice(0, close)) + text.slice(close);
393
+ }
394
+
395
+ function blankCommentsForMatchers(text, ext) {
396
+ if (PAGE_ANALYZER_EXTS.has(ext)) {
397
+ const withFrontmatter = ext === '.astro' ? blankAstroFrontmatterComments(text) : text;
398
+ return blankHtmlAndCssCommentsOutsideScripts(withFrontmatter);
399
+ }
400
+ if (STYLESHEET_EXTS.has(ext)) {
401
+ const withoutBlocks = stripCssComments(text);
402
+ return ext === '.css' ? withoutBlocks : blankCssLineComments(withoutBlocks);
403
+ }
404
+ return text;
405
+ }
406
+
44
407
  function firstOverusedGoogleFont(text) {
45
408
  return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
46
409
  }
@@ -210,25 +573,28 @@ const REGEX_MATCHERS = [
210
573
  },
211
574
  fmt: (m) => `cubic-bezier(${m[1]}, ${m[2]}, ${m[3]}, ${m[4]})` },
212
575
  // --- Layout property transition ---
213
- { id: 'layout-transition', regex: /transition\s*:\s*([^;{}]+)/gi,
576
+ // JSX inline style objects use comma-delimited quoted values, not semicolons (issue #548).
577
+ { id: 'layout-transition', regex: /transition\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi,
214
578
  test: (m) => {
215
- const val = m[1].toLowerCase();
579
+ const val = (m[2] ?? m[3] ?? '').toLowerCase();
216
580
  if (/\ball\b/.test(val)) return false;
217
581
  return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val);
218
582
  },
219
583
  fmt: (m) => {
220
- const found = m[1].match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
221
- return `transition: ${found ? found.join(', ') : m[1].trim()}`;
584
+ const raw = m[2] ?? m[3] ?? '';
585
+ const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
586
+ return `transition: ${found ? found.join(', ') : raw.trim()}`;
222
587
  } },
223
- { id: 'layout-transition', regex: /transition-property\s*:\s*([^;{}]+)/gi,
588
+ { id: 'layout-transition', regex: /transition-property\s*:\s*(?:(['"])((?:(?!\1)[^\\]|\\.)*)\1|([^;{}]+))/gi,
224
589
  test: (m) => {
225
- const val = m[1].toLowerCase();
590
+ const val = (m[2] ?? m[3] ?? '').toLowerCase();
226
591
  if (/\ball\b/.test(val)) return false;
227
592
  return /\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding\b|\bmargin\b/.test(val);
228
593
  },
229
594
  fmt: (m) => {
230
- const found = m[1].match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
231
- return `transition-property: ${found ? found.join(', ') : m[1].trim()}`;
595
+ const raw = m[2] ?? m[3] ?? '';
596
+ const found = raw.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
597
+ return `transition-property: ${found ? found.join(', ') : raw.trim()}`;
232
598
  } },
233
599
  // --- Broken image: src="" or src="#" or src=" " ---
234
600
  { id: 'broken-image', regex: /<img\b[^>]*?\bsrc\s*=\s*(?:""|''|"\s+"|'\s+'|"#"|'#')/gi,
@@ -528,18 +894,198 @@ function extractStyleBlocks(content, ext) {
528
894
 
529
895
  const CSS_IN_JS_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx']);
530
896
 
897
+ function findQuotedStringEnd(content, start, quote) {
898
+ for (let cursor = start + 1; cursor < content.length; cursor++) {
899
+ if (content[cursor] === '\\') cursor++;
900
+ else if (content[cursor] === quote) return cursor;
901
+ }
902
+ return -1;
903
+ }
904
+
905
+ function findRegexLiteralEnd(content, start) {
906
+ let inCharacterClass = false;
907
+ for (let cursor = start + 1; cursor < content.length; cursor++) {
908
+ const char = content[cursor];
909
+ if (char === '\\') {
910
+ cursor++;
911
+ } else if (char === '[') {
912
+ inCharacterClass = true;
913
+ } else if (char === ']') {
914
+ inCharacterClass = false;
915
+ } else if (char === '/' && !inCharacterClass) {
916
+ while (/[A-Za-z]/.test(content[cursor + 1] || '')) cursor++;
917
+ return cursor;
918
+ } else if (char === '\n' || char === '\r') {
919
+ return -1;
920
+ }
921
+ }
922
+ return -1;
923
+ }
924
+
925
+ function findTemplateExpressionEnd(content, start) {
926
+ let depth = 1;
927
+ let lastSignificant = '';
928
+ let previousSignificant = '';
929
+ let antePreviousSignificant = '';
930
+ let currentWord = '';
931
+ let currentWordPrefix = '';
932
+ let wordSeparated = false;
933
+ let lastClosedBraceKind = '';
934
+ const braceKinds = [];
935
+
936
+ const braceKind = () => (
937
+ lastSignificant === ')' ||
938
+ lastSignificant === ';' ||
939
+ lastSignificant === '}' ||
940
+ (previousSignificant === '=' && lastSignificant === '>') ||
941
+ BLOCK_BRACE_PREFIX_KEYWORDS.has(currentWord)
942
+ ? 'block'
943
+ : 'expression'
944
+ );
945
+
946
+ const recordSignificant = (char) => {
947
+ if (/\s/.test(char)) {
948
+ wordSeparated = true;
949
+ return;
950
+ }
951
+ const isWordChar = /[\w$]/.test(char);
952
+ if (isWordChar && (wordSeparated || !currentWord)) {
953
+ currentWord = '';
954
+ currentWordPrefix = lastSignificant;
955
+ } else if (!isWordChar) {
956
+ currentWordPrefix = '';
957
+ }
958
+ wordSeparated = false;
959
+ antePreviousSignificant = previousSignificant;
960
+ previousSignificant = lastSignificant;
961
+ lastSignificant = char;
962
+ currentWord = isWordChar ? currentWord + char : '';
963
+ };
964
+
965
+ for (let cursor = start; cursor < content.length; cursor++) {
966
+ const char = content[cursor];
967
+ const next = content[cursor + 1];
968
+ const afterPostfixUpdate = (lastSignificant === '+' || lastSignificant === '-') &&
969
+ previousSignificant === lastSignificant &&
970
+ antePreviousSignificant !== lastSignificant;
971
+ if (char === "'" || char === '"') {
972
+ cursor = findQuotedStringEnd(content, cursor, char);
973
+ if (cursor === -1) return -1;
974
+ recordSignificant(')');
975
+ } else if (char === '/' && next === '/') {
976
+ const lineEnd = content.indexOf('\n', cursor + 2);
977
+ if (lineEnd === -1) return -1;
978
+ cursor = lineEnd;
979
+ } else if (char === '/' && next === '*') {
980
+ const commentEnd = content.indexOf('*/', cursor + 2);
981
+ if (commentEnd === -1) return -1;
982
+ cursor = commentEnd + 1;
983
+ } else if (
984
+ char === '/' &&
985
+ (!lastSignificant ||
986
+ (/[=([{!?:;,&|+\-*%^~<>]/.test(lastSignificant) && !afterPostfixUpdate) ||
987
+ (lastSignificant === '}' && lastClosedBraceKind === 'block') ||
988
+ (previousSignificant === '=' && lastSignificant === '>') ||
989
+ (currentWordPrefix !== '.' && REGEX_PREFIX_KEYWORDS.has(currentWord)))
990
+ ) {
991
+ cursor = findRegexLiteralEnd(content, cursor);
992
+ if (cursor === -1) return -1;
993
+ recordSignificant(')');
994
+ } else if (char === '`') {
995
+ cursor = findTemplateLiteralEnd(content, cursor);
996
+ if (cursor === -1) return -1;
997
+ recordSignificant(')');
998
+ } else if (char === '{') {
999
+ depth++;
1000
+ braceKinds.push(braceKind());
1001
+ recordSignificant(char);
1002
+ } else if (char === '}') {
1003
+ depth--;
1004
+ if (depth === 0) return cursor;
1005
+ lastClosedBraceKind = braceKinds.pop() || '';
1006
+ recordSignificant(char);
1007
+ } else {
1008
+ recordSignificant(char);
1009
+ }
1010
+ }
1011
+ return -1;
1012
+ }
1013
+
1014
+ function findTemplateLiteralEnd(content, start) {
1015
+ for (let cursor = start + 1; cursor < content.length; cursor++) {
1016
+ const char = content[cursor];
1017
+ if (char === '\\') {
1018
+ cursor++;
1019
+ } else if (char === '`') {
1020
+ return cursor;
1021
+ } else if (char === '$' && content[cursor + 1] === '{') {
1022
+ cursor = findTemplateExpressionEnd(content, cursor + 2);
1023
+ if (cursor === -1) return -1;
1024
+ }
1025
+ }
1026
+ return -1;
1027
+ }
1028
+
1029
+ function findCSSinJSTemplates(content) {
1030
+ const templates = [];
1031
+ const tagRe = /\b(?:styled(?:\.\w+|\([^)]+\))|css)/g;
1032
+ let match;
1033
+ while ((match = tagRe.exec(content)) !== null) {
1034
+ let cursor = match.index + match[0].length;
1035
+ while (/\s/.test(content[cursor] || '')) cursor++;
1036
+
1037
+ if (content[cursor] === '<') {
1038
+ let depth = 0;
1039
+ while (cursor < content.length) {
1040
+ const char = content[cursor];
1041
+ if (char === '<') depth++;
1042
+ else if (char === '>' && content[cursor - 1] !== '=') depth--;
1043
+ cursor++;
1044
+ if (depth === 0) break;
1045
+ }
1046
+ if (depth !== 0) continue;
1047
+ while (/\s/.test(content[cursor] || '')) cursor++;
1048
+ }
1049
+
1050
+ if (content[cursor] !== '`') continue;
1051
+ const contentStart = cursor + 1;
1052
+ cursor = findTemplateLiteralEnd(content, cursor);
1053
+ if (cursor === -1) continue;
1054
+
1055
+ templates.push({
1056
+ tagStart: match.index,
1057
+ contentStart,
1058
+ contentEnd: cursor,
1059
+ });
1060
+ tagRe.lastIndex = cursor + 1;
1061
+ }
1062
+ return templates;
1063
+ }
1064
+
531
1065
  function extractCSSinJS(content, ext) {
532
1066
  ext = ext.toLowerCase();
533
1067
  if (!CSS_IN_JS_EXTENSIONS.has(ext)) return [];
534
- const blocks = [];
535
- const re = /(?:styled(?:\.\w+|\([^)]+\))|css)\s*`([\s\S]*?)`/g;
536
- let m;
537
- while ((m = re.exec(content)) !== null) {
538
- const before = content.substring(0, m.index);
1068
+ return findCSSinJSTemplates(content).map((template) => {
1069
+ const before = content.substring(0, template.tagStart);
539
1070
  const startLine = before.split('\n').length;
540
- blocks.push({ content: m[1], startLine });
1071
+ return {
1072
+ content: content.slice(template.contentStart, template.contentEnd),
1073
+ startLine,
1074
+ };
1075
+ });
1076
+ }
1077
+
1078
+ function stripCssInJsComments(content, ext) {
1079
+ if (!CSS_IN_JS_EXTENSIONS.has(ext.toLowerCase())) return content;
1080
+ const templates = findCSSinJSTemplates(content);
1081
+ let output = '';
1082
+ let cursor = 0;
1083
+ for (const template of templates) {
1084
+ output += content.slice(cursor, template.contentStart);
1085
+ output += stripCssComments(content.slice(template.contentStart, template.contentEnd));
1086
+ cursor = template.contentEnd;
541
1087
  }
542
- return blocks;
1088
+ return output + content.slice(cursor);
543
1089
  }
544
1090
 
545
1091
  function runRegexMatchers(lines, filePath, lineOffset = 0, blockContext = null, options = {}) {
@@ -627,13 +1173,16 @@ function runTextContentAnalyzers(content, filePath, options = {}) {
627
1173
  function detectText(content, filePath, options = {}) {
628
1174
  const profile = options?.profile;
629
1175
  const findings = [];
630
- const lines = content.split('\n');
631
1176
  const ext = extFromFilePath(filePath);
1177
+ const commentStrippedSource = JS_SOURCE_EXTS.has(ext) ? stripJsComments(content, {
1178
+ jsx: ext === '.js' || ext === '.jsx' || ext === '.tsx',
1179
+ }) : blankCommentsForMatchers(content, ext);
1180
+ const source = stripCssInJsComments(commentStrippedSource, ext);
1181
+ const lines = source.split('\n');
632
1182
 
633
1183
  // Run regex matchers on the full file content (catches Tailwind classes, inline styles)
634
1184
  // Enable block context for CSS files where related properties span multiple lines
635
- const cssLike = new Set(['.css', '.scss', '.sass', '.less']);
636
- findings.push(...runRegexMatchers(lines, filePath, 0, cssLike.has(ext) || null, {
1185
+ findings.push(...runRegexMatchers(lines, filePath, 0, STYLESHEET_EXTS.has(ext) || null, {
637
1186
  profile,
638
1187
  phase: 'source',
639
1188
  }));
@@ -648,7 +1197,7 @@ function detectText(content, filePath, options = {}) {
648
1197
  scanCssTextForPseudoStripe(text).map(hit =>
649
1198
  finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
650
1199
 
651
- if (cssLike.has(ext)) {
1200
+ if (STYLESHEET_EXTS.has(ext)) {
652
1201
  findings.push(...scanInsetStripeCss(content, filePath));
653
1202
  findings.push(...pseudoStripeFindings(content, 0));
654
1203
  }
@@ -661,8 +1210,8 @@ function detectText(content, filePath, options = {}) {
661
1210
  phase: 'source',
662
1211
  ruleId: 'codex-grid-background',
663
1212
  target: filePath,
664
- }, () => scanCssTextForGridBackground(content).map(hit => {
665
- const line = content.substring(0, hit.index).split('\n').length;
1213
+ }, () => scanCssTextForGridBackground(source).map(hit => {
1214
+ const line = source.substring(0, hit.index).split('\n').length;
666
1215
  return finding('codex-grid-background', filePath, hit.snippet, line);
667
1216
  })));
668
1217
 
@@ -676,7 +1225,8 @@ function detectText(content, filePath, options = {}) {
676
1225
  }, () => extractStyleBlocks(content, ext))
677
1226
  : extractStyleBlocks(content, ext);
678
1227
  for (const block of styleBlocks) {
679
- const blockLines = block.content.split('\n');
1228
+ const blockContent = blankCssLineComments(stripCssComments(block.content));
1229
+ const blockLines = blockContent.split('\n');
680
1230
  findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
681
1231
  profile,
682
1232
  phase: 'style-block',
@@ -687,8 +1237,8 @@ function detectText(content, filePath, options = {}) {
687
1237
  // 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
688
1238
  // reported every selector one line low. runRegexMatchers keeps startLine - 1
689
1239
  // because it indexes its split lines from zero.
690
- findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
691
- findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
1240
+ findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 2));
1241
+ findings.push(...pseudoStripeFindings(blockContent, block.startLine - 2));
692
1242
  }
693
1243
 
694
1244
  // Extract and scan CSS-in-JS template literals
@@ -698,16 +1248,17 @@ function detectText(content, filePath, options = {}) {
698
1248
  phase: 'extract',
699
1249
  ruleId: 'css-in-js',
700
1250
  target: filePath,
701
- }, () => extractCSSinJS(content, ext))
702
- : extractCSSinJS(content, ext);
1251
+ }, () => extractCSSinJS(source, ext))
1252
+ : extractCSSinJS(source, ext);
703
1253
  for (const block of cssJsBlocks) {
704
- const blockLines = block.content.split('\n');
1254
+ const blockContent = stripCssComments(block.content);
1255
+ const blockLines = blockContent.split('\n');
705
1256
  findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
706
1257
  profile,
707
1258
  phase: 'css-in-js',
708
1259
  }));
709
- findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 1));
710
- findings.push(...pseudoStripeFindings(block.content, block.startLine - 1));
1260
+ findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 1));
1261
+ findings.push(...pseudoStripeFindings(blockContent, block.startLine - 1));
711
1262
  }
712
1263
 
713
1264
  if (options?.designSystem) {