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.
- package/dist/cli.js +237 -10
- package/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +2 -0
- package/template/.agents/skills/data/prisma-cli/SKILL.md +25 -7
- package/template/.agents/skills/data/prisma-cli/references/agent-safety.md +27 -0
- package/template/.agents/skills/data/prisma-cli/references/complete.md +22 -0
- package/template/.agents/skills/data/prisma-cli/references/db-push.md +2 -0
- package/template/.agents/skills/data/prisma-cli/references/init.md +3 -0
- package/template/.agents/skills/data/prisma-cli/references/mcp.md +2 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-dev.md +1 -1
- package/template/.agents/skills/data/prisma-cli/references/migrate-reset.md +2 -0
- package/template/.agents/skills/data/prisma-client-api/SKILL.md +1 -1
- package/template/.agents/skills/data/prisma-client-api/references/constructor.md +13 -0
- package/template/.agents/skills/data/prisma-client-api/references/raw-queries.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/android.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +3 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +3 -1
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +2 -0
- package/template/.agents/skills/design/impeccable/reference/critique.md +23 -5
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +15 -68
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +13 -12
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/doctor.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +17 -11
- package/template/.agents/skills/design/impeccable/reference/init.md +9 -3
- package/template/.agents/skills/design/impeccable/reference/ios.md +6 -0
- package/template/.agents/skills/design/impeccable/reference/new-work.md +69 -29
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +13 -5
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/routing.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/visualize.md +21 -22
- package/template/.agents/skills/design/impeccable/scripts/build-phase.mjs +1022 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-diff.mjs +391 -0
- package/template/.agents/skills/design/impeccable/scripts/comp-spec.mjs +513 -0
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +297 -41
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +10 -19
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +124 -9
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +279 -19
- package/template/.agents/skills/design/impeccable/scripts/data/font-index-failures.json +121 -0
- package/template/.agents/skills/design/impeccable/scripts/data/font-index.json +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detect.mjs +9 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +192 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +10 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +339 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +1482 -722
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +1 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +64 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +580 -29
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +62 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +31 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +23 -22
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +18 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +471 -370
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/color.mjs +474 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +17 -2
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +14 -21
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +81 -48
- package/template/.agents/skills/design/impeccable/scripts/font-match.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +219 -12
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +93 -15
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +41 -19
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +507 -117
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +10 -9
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +40 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/design-parser.mjs +120 -82
- package/template/.agents/skills/design/impeccable/scripts/lib/font-fingerprint.mjs +564 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/font-index.mjs +130 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/hero-checks.mjs +246 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/image-metrics.mjs +306 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +41 -59
- package/template/.agents/skills/design/impeccable/scripts/lib/is-generated.mjs +5 -2
- package/template/.agents/skills/design/impeccable/scripts/lib/live-path-globs.mjs +37 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/open-system-browser.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/png.mjs +281 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/raster.mjs +194 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +26 -19
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +31 -3
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +93 -17
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +9 -11
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +31 -2
- package/template/.agents/skills/design/impeccable/scripts/live/project-ignores.mjs +139 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +10 -2
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +26 -2
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +15 -27
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +4 -25
- package/template/.agents/skills/design/impeccable/scripts/live/ui-surfaces.mjs +75 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +21 -37
- package/template/.agents/skills/design/impeccable/scripts/live-browser-ignores.js +242 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +62 -23
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +68 -112
- package/template/.agents/skills/design/impeccable/scripts/live-copy-edit-agent.mjs +132 -15
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +2 -42
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +5 -4
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +56 -19
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +12 -37
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +8 -5
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +1006 -155
- package/template/.agents/skills/design/shadcn/rules/chat.md +26 -0
- package/template/.agents/skills/hono/SKILL.md +17 -3
- package/template/.agents/skills/integrations/langfuse/SKILL.md +8 -4
- package/template/.agents/skills/integrations/langfuse/references/cli.md +1 -1
- package/template/.agents/skills/integrations/langfuse/references/create-dataset.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +9 -10
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +8 -6
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +34 -0
- package/template/.agents/skills/integrations/langfuse/references/setting-up-evals.md +65 -0
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +20 -40
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -30
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +3 -1
- package/template/.agents/skills/supabase/CHANGELOG.md +7 -0
- package/template/.agents/skills/supabase/SKILL.md +5 -1
- package/template/.agents/skills/writing/humanizer/LICENSE +21 -0
- package/template/.agents/skills/writing/humanizer/README.md +209 -0
- package/template/.agents/skills/writing/humanizer/SKILL.md +173 -338
- package/template/.agents/upstreams.json +21 -22
- package/template/.agents/skills/design/impeccable/scripts/live/ui-core.mjs +0 -180
|
@@ -13,7 +13,16 @@ const FALLBACK_DIRS = ['.agents/context', 'docs'];
|
|
|
13
13
|
// CLI can't import (separate tree). `.git` and `package.json` are the common
|
|
14
14
|
// boundaries; `.impeccable` is our own project marker.
|
|
15
15
|
const PROJECT_ROOT_MARKERS = ['.git', 'package.json', '.impeccable'];
|
|
16
|
+
// Monorepo-root recognition, mirroring context.mjs's isMonorepoRoot: declared
|
|
17
|
+
// workspace globs (package.json `workspaces`, pnpm-workspace.yaml `packages:`)
|
|
18
|
+
// or a marker file beside apps/ or packages/ children.
|
|
19
|
+
const MONOREPO_MARKER_FILES = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
|
|
20
|
+
const MONOREPO_FALLBACK_PROJECT_DIRS = ['apps', 'packages'];
|
|
16
21
|
const COLOR_CHANNEL_TOLERANCE = 6;
|
|
22
|
+
// Shadow blacks at different alphas are different tokens (0.28 vs 0.55 is the
|
|
23
|
+
// difference between a documented shadow and drift), so shadow matching cannot
|
|
24
|
+
// reuse the r/g/b-only channel tolerance.
|
|
25
|
+
const SHADOW_ALPHA_TOLERANCE = 0.02;
|
|
17
26
|
const RADIUS_TOLERANCE_PX = 0.5;
|
|
18
27
|
const FONT_SIZE_TOLERANCE_PX = 0.5;
|
|
19
28
|
const FONT_SIZE_LITERAL_RE = /^-?[\d.]+(?:px|rem)$/;
|
|
@@ -142,10 +151,73 @@ function stripInlineYamlComment(s) {
|
|
|
142
151
|
return s;
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
// YAML double-quoted scalars process backslash escapes. Stripping the outer
|
|
155
|
+
// quotes without unescaping leaves them in place, so a nested font family like
|
|
156
|
+
// fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
|
|
157
|
+
// reaches allowedFonts as '\"ibm plex sans' and never matches the same family
|
|
158
|
+
// declared in CSS. Scanner instead of a regex: the escape set is small and the
|
|
159
|
+
// backslash handling stays readable.
|
|
160
|
+
// The full YAML 1.2 double-quote escape set (spec section 5.7).
|
|
161
|
+
const YAML_SIMPLE_ESCAPES = {
|
|
162
|
+
'0': '\0',
|
|
163
|
+
a: '\x07',
|
|
164
|
+
b: '\b',
|
|
165
|
+
t: '\t',
|
|
166
|
+
n: '\n',
|
|
167
|
+
v: '\v',
|
|
168
|
+
f: '\f',
|
|
169
|
+
r: '\r',
|
|
170
|
+
e: '\x1b',
|
|
171
|
+
' ': ' ',
|
|
172
|
+
'"': '"',
|
|
173
|
+
'/': '/',
|
|
174
|
+
'\\': '\\',
|
|
175
|
+
N: '\u0085',
|
|
176
|
+
_: '\u00a0',
|
|
177
|
+
L: '\u2028',
|
|
178
|
+
P: '\u2029',
|
|
179
|
+
};
|
|
180
|
+
const YAML_HEX_ESCAPE_LENGTHS = { x: 2, u: 4, U: 8 };
|
|
181
|
+
|
|
182
|
+
function unescapeYamlDoubleQuoted(body) {
|
|
183
|
+
let out = '';
|
|
184
|
+
for (let i = 0; i < body.length; i++) {
|
|
185
|
+
const ch = body[i];
|
|
186
|
+
if (ch !== '\\' || i === body.length - 1) {
|
|
187
|
+
out += ch;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const next = body[i + 1];
|
|
191
|
+
if (Object.prototype.hasOwnProperty.call(YAML_SIMPLE_ESCAPES, next)) {
|
|
192
|
+
out += YAML_SIMPLE_ESCAPES[next];
|
|
193
|
+
i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// \xNN, \uNNNN, \UNNNNNNNN. Malformed or out-of-range sequences stay
|
|
197
|
+
// literal rather than corrupting the rest of the scalar.
|
|
198
|
+
const hexLen = YAML_HEX_ESCAPE_LENGTHS[next];
|
|
199
|
+
if (hexLen) {
|
|
200
|
+
const hex = body.slice(i + 2, i + 2 + hexLen);
|
|
201
|
+
const codePoint = hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex) ? parseInt(hex, 16) : -1;
|
|
202
|
+
if (codePoint >= 0 && codePoint <= 0x10ffff) {
|
|
203
|
+
out += String.fromCodePoint(codePoint);
|
|
204
|
+
i += 1 + hexLen;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
out += ch;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
function parseScalar(raw) {
|
|
146
214
|
const s = raw.trim();
|
|
147
|
-
if (
|
|
148
|
-
return s.slice(1, -1);
|
|
215
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
216
|
+
return unescapeYamlDoubleQuoted(s.slice(1, -1));
|
|
217
|
+
}
|
|
218
|
+
// Single-quoted YAML escapes only the quote itself, by doubling it.
|
|
219
|
+
if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
|
|
220
|
+
return s.slice(1, -1).split("''").join("'");
|
|
149
221
|
}
|
|
150
222
|
if (s === 'true') return true;
|
|
151
223
|
if (s === 'false') return false;
|
|
@@ -411,6 +483,25 @@ function addSidecarRadii(out, sidecar) {
|
|
|
411
483
|
}
|
|
412
484
|
}
|
|
413
485
|
|
|
486
|
+
// Sidecar `extensions.shadows` entries ({ name, value, purpose }) carry the
|
|
487
|
+
// documented shadow vocabulary that Stitch's frontmatter schema can't hold.
|
|
488
|
+
// Their colors go into a separate allowlist — NOT allowedColorKeys — because a
|
|
489
|
+
// shadow black is only documented *as a shadow*: feeding it into the general
|
|
490
|
+
// color allowlist would legalize #000 as a page ground (alpha is dropped from
|
|
491
|
+
// colorKey), which is the hole issue #547 warns against.
|
|
492
|
+
function addSidecarShadows(out, sidecar) {
|
|
493
|
+
const shadows = sidecar?.extensions?.shadows;
|
|
494
|
+
if (!Array.isArray(shadows)) return;
|
|
495
|
+
|
|
496
|
+
for (const entry of shadows) {
|
|
497
|
+
if (typeof entry?.value !== 'string') continue;
|
|
498
|
+
for (const match of entry.value.matchAll(CSS_COLOR_RE)) {
|
|
499
|
+
const parsed = parseDesignColor(match[0]);
|
|
500
|
+
if (parsed) out.allowedShadowColors.push({ color: parsed });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
414
505
|
function normalizeDesignSystem(input = {}) {
|
|
415
506
|
const frontmatter = input.frontmatter || {};
|
|
416
507
|
const sidecar = input.sidecar || null;
|
|
@@ -423,6 +514,7 @@ function normalizeDesignSystem(input = {}) {
|
|
|
423
514
|
allowedColorKeys: new Map(),
|
|
424
515
|
allowedRadii: [],
|
|
425
516
|
allowedFontSizes: [],
|
|
517
|
+
allowedShadowColors: [],
|
|
426
518
|
hasPillRadius: false,
|
|
427
519
|
};
|
|
428
520
|
|
|
@@ -432,6 +524,7 @@ function normalizeDesignSystem(input = {}) {
|
|
|
432
524
|
addSidecarColors(out, sidecar);
|
|
433
525
|
addRoundedScale(out, frontmatter.rounded);
|
|
434
526
|
addSidecarRadii(out, sidecar);
|
|
527
|
+
addSidecarShadows(out, sidecar);
|
|
435
528
|
|
|
436
529
|
out.hasFonts = out.allowedFonts.size > 0;
|
|
437
530
|
out.hasColors = out.allowedColorKeys.size > 0;
|
|
@@ -487,14 +580,179 @@ function designSystemStartDir(targetPath, cwd = process.cwd()) {
|
|
|
487
580
|
}
|
|
488
581
|
}
|
|
489
582
|
|
|
583
|
+
// Same two groups as context.mjs's readProjectPatternGroups: Impeccable
|
|
584
|
+
// projectRoots govern any path they match (positive or negated); package-manager
|
|
585
|
+
// globs only apply to paths the Impeccable group does not match.
|
|
586
|
+
function readWorkspacePatternGroups(dir) {
|
|
587
|
+
const impeccable = [];
|
|
588
|
+
for (const name of ['config.json', 'config.local.json']) {
|
|
589
|
+
const roots = safeReadJson(path.join(dir, '.impeccable', name))?.projectRoots;
|
|
590
|
+
if (Array.isArray(roots)) {
|
|
591
|
+
impeccable.push(...roots.filter(entry => typeof entry === 'string' && entry.trim()).map(entry => entry.trim()));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const pkg = [];
|
|
595
|
+
const workspaces = safeReadJson(path.join(dir, 'package.json'))?.workspaces;
|
|
596
|
+
if (Array.isArray(workspaces)) pkg.push(...workspaces);
|
|
597
|
+
else if (Array.isArray(workspaces?.packages)) pkg.push(...workspaces.packages);
|
|
598
|
+
const lernaPackages = safeReadJson(path.join(dir, 'lerna.json'))?.packages;
|
|
599
|
+
if (Array.isArray(lernaPackages)) pkg.push(...lernaPackages);
|
|
600
|
+
try {
|
|
601
|
+
let inPackages = false;
|
|
602
|
+
for (const line of fs.readFileSync(path.join(dir, 'pnpm-workspace.yaml'), 'utf-8').split(/\r?\n/)) {
|
|
603
|
+
const trimmed = stripInlineYamlComment(line).trim();
|
|
604
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
605
|
+
const flow = trimmed.match(/^packages:\s*\[(.*)\]\s*$/);
|
|
606
|
+
if (flow) {
|
|
607
|
+
pkg.push(...flow[1].split(',').map(entry => entry.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean));
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (/^packages:\s*$/.test(trimmed)) { inPackages = true; continue; }
|
|
611
|
+
if (!inPackages) continue;
|
|
612
|
+
const item = trimmed.match(/^-\s*(.+)$/);
|
|
613
|
+
if (item) pkg.push(item[1].trim().replace(/^['"]|['"]$/g, ''));
|
|
614
|
+
else if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) break;
|
|
615
|
+
}
|
|
616
|
+
} catch { /* no pnpm-workspace.yaml */ }
|
|
617
|
+
return [impeccable, pkg];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function readWorkspacePatterns(dir) {
|
|
621
|
+
return readWorkspacePatternGroups(dir).flat();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function isMonorepoRoot(dir) {
|
|
625
|
+
if (readWorkspacePatterns(dir).some(pattern => !String(pattern).trim().startsWith('!'))) return true;
|
|
626
|
+
if (!MONOREPO_MARKER_FILES.some(file => fs.existsSync(path.join(dir, file)))) return false;
|
|
627
|
+
return MONOREPO_FALLBACK_PROJECT_DIRS.some(name => {
|
|
628
|
+
try {
|
|
629
|
+
return fs.readdirSync(path.join(dir, name), { withFileTypes: true }).some(entry => entry.isDirectory());
|
|
630
|
+
} catch {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function monorepoOwnsPath(root, boundaryDir) {
|
|
637
|
+
const rel = path.relative(root, boundaryDir);
|
|
638
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
|
|
639
|
+
const relSegments = rel.split(path.sep).filter(Boolean);
|
|
640
|
+
|
|
641
|
+
function normalizeWorkspacePattern(pattern) {
|
|
642
|
+
return String(pattern || '')
|
|
643
|
+
.trim()
|
|
644
|
+
.replace(/^['"]|['"]$/g, '')
|
|
645
|
+
.replace(/^\.\//, '')
|
|
646
|
+
.replace(/\/+$/, '');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function escapeRegExp(s) {
|
|
650
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function segmentMatches(patternSegment, relSegment) {
|
|
654
|
+
if (patternSegment === '*') return true;
|
|
655
|
+
if (!patternSegment.includes('*')) return patternSegment === relSegment;
|
|
656
|
+
const re = new RegExp(`^${escapeRegExp(patternSegment).replace(/\\\*/g, '[^/]*')}$`);
|
|
657
|
+
return re.test(relSegment);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function matchGlobSegments(patternSegments, relSegments) {
|
|
661
|
+
function rec(pi, ri) {
|
|
662
|
+
if (pi === patternSegments.length) return ri === relSegments.length;
|
|
663
|
+
if (patternSegments[pi] === '**') {
|
|
664
|
+
if (pi === patternSegments.length - 1) return true;
|
|
665
|
+
for (let k = ri; k <= relSegments.length; k++) {
|
|
666
|
+
if (rec(pi + 1, k)) return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
if (ri >= relSegments.length) return false;
|
|
671
|
+
if (!segmentMatches(patternSegments[pi], relSegments[ri])) return false;
|
|
672
|
+
return rec(pi + 1, ri + 1);
|
|
673
|
+
}
|
|
674
|
+
return rec(0, 0);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Negations like !packages/excluded must also cover nested dirs under that path.
|
|
678
|
+
function matchesNegation(pattern) {
|
|
679
|
+
const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
|
|
680
|
+
if (!patternSegments.length) return false;
|
|
681
|
+
if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
|
|
682
|
+
if (relSegments.length < patternSegments.length) return false;
|
|
683
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
684
|
+
if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Positive globs identify workspace packages at exact depth (`*` is a direct
|
|
690
|
+
// child). A nested package.json under that package is still owned: the
|
|
691
|
+
// ancestor directory of glob length must itself be a package.
|
|
692
|
+
function positiveOwns(pattern) {
|
|
693
|
+
const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
|
|
694
|
+
if (!patternSegments.length) return false;
|
|
695
|
+
if (patternSegments.includes('**')) return matchGlobSegments(patternSegments, relSegments);
|
|
696
|
+
if (relSegments.length < patternSegments.length) return false;
|
|
697
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
698
|
+
if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
|
|
699
|
+
}
|
|
700
|
+
if (relSegments.length === patternSegments.length) return true;
|
|
701
|
+
const ancestorDir = path.join(root, ...relSegments.slice(0, patternSegments.length));
|
|
702
|
+
return fs.existsSync(path.join(ancestorDir, 'package.json'));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function groupOwns(rawPatterns) {
|
|
706
|
+
const patterns = rawPatterns.map(normalizeWorkspacePattern).filter(Boolean);
|
|
707
|
+
if (!patterns.length) return null;
|
|
708
|
+
const excluded = patterns.some((pattern) => (
|
|
709
|
+
pattern.startsWith('!') && matchesNegation(pattern.slice(1))
|
|
710
|
+
));
|
|
711
|
+
const included = patterns.filter((pattern) => !pattern.startsWith('!')).some(positiveOwns);
|
|
712
|
+
if (!excluded && !included) return null;
|
|
713
|
+
if (excluded) return false;
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const [impeccable, pkg] = readWorkspacePatternGroups(root);
|
|
718
|
+
const fromImpeccable = groupOwns(impeccable);
|
|
719
|
+
if (fromImpeccable !== null) return fromImpeccable;
|
|
720
|
+
const fromPkg = groupOwns(pkg);
|
|
721
|
+
if (fromPkg !== null) return fromPkg;
|
|
722
|
+
if ([...impeccable, ...pkg].some((pattern) => !normalizeWorkspacePattern(pattern).startsWith('!'))) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
return relSegments.length >= 2 && MONOREPO_FALLBACK_PROJECT_DIRS.includes(relSegments[0]);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Both forms of the home directory. The walk compares path strings, and a
|
|
729
|
+
// symlinked home (e.g. /home -> /var/home) never string-matches the physical
|
|
730
|
+
// paths a cwd-resolved target produces, which would let the post-boundary walk
|
|
731
|
+
// sail through $HOME and inherit from it.
|
|
732
|
+
function homeDirForms() {
|
|
733
|
+
const homeDir = path.resolve(os.homedir());
|
|
734
|
+
const forms = new Set([homeDir]);
|
|
735
|
+
try {
|
|
736
|
+
forms.add(fs.realpathSync(homeDir));
|
|
737
|
+
} catch { /* keep the logical form only */ }
|
|
738
|
+
return forms;
|
|
739
|
+
}
|
|
740
|
+
|
|
490
741
|
// Walk up from `startDir` to the directory that governs the target's design
|
|
491
742
|
// system, mirroring skill/scripts/context.mjs's project-boundary semantics:
|
|
492
743
|
//
|
|
493
744
|
// - A directory carrying a DESIGN.md (directly or in a fallback dir) IS the
|
|
494
745
|
// design root — that's where the rules live.
|
|
495
746
|
// - A directory carrying a project marker (.git / package.json / .impeccable)
|
|
496
|
-
// but no DESIGN.md is a project BOUNDARY
|
|
497
|
-
//
|
|
747
|
+
// but no DESIGN.md is a project BOUNDARY. A nested package.json inherits
|
|
748
|
+
// the ancestor DESIGN.md only when that ancestor's workspace declarations
|
|
749
|
+
// include the path (negations win; a nested package under a matched
|
|
750
|
+
// workspace still inherits). Marker-only roots (turbo/nx/lerna/pnpm
|
|
751
|
+
// with no globs) still own apps/<name> and packages/<name>. A stray nested
|
|
752
|
+
// package that matches no glob does not inherit. This is detect's
|
|
753
|
+
// contamination contract, not skill-context's repoRoot fallback for
|
|
754
|
+
// excluded paths. A nested separate repository (.git with no workspace
|
|
755
|
+
// declaration) still inherits nothing (issue #570).
|
|
498
756
|
// - Reaching the home directory / filesystem root with neither means no
|
|
499
757
|
// design system at all — never process.cwd()'s.
|
|
500
758
|
//
|
|
@@ -502,15 +760,33 @@ function designSystemStartDir(targetPath, cwd = process.cwd()) {
|
|
|
502
760
|
// runs out. This is the fix for cross-project contamination.
|
|
503
761
|
export function findDesignRoot(startDir) {
|
|
504
762
|
let dir = path.resolve(startDir);
|
|
505
|
-
const
|
|
763
|
+
const homeDirs = homeDirForms();
|
|
764
|
+
let boundary = null;
|
|
506
765
|
while (true) {
|
|
507
|
-
if (resolveDesignMdPath(dir)) return { dir, hasDesign: true };
|
|
508
|
-
if (
|
|
509
|
-
|
|
766
|
+
if (!boundary && resolveDesignMdPath(dir)) return { dir, hasDesign: true };
|
|
767
|
+
if (boundary) {
|
|
768
|
+
// Past the boundary the walk only looks for the monorepo root that owns
|
|
769
|
+
// the workspace path (workspace globs including negations, or marker-only
|
|
770
|
+
// apps/packages fallback). Monorepo-root before .git, same order as
|
|
771
|
+
// context.mjs: a workspace root carrying its own .git is still recognized,
|
|
772
|
+
// while a .git that declares no workspaces is a separate repository and
|
|
773
|
+
// stops the walk with nothing inherited. The home directory is never an
|
|
774
|
+
// owning root, same as context.mjs's findMonorepoRoot, which stops at
|
|
775
|
+
// homeDir before its monorepo check.
|
|
776
|
+
if (!homeDirs.has(dir) && isMonorepoRoot(dir)) {
|
|
777
|
+
if (monorepoOwnsPath(dir, boundary.dir)) return { dir, hasDesign: !!resolveDesignMdPath(dir) };
|
|
778
|
+
return boundary;
|
|
779
|
+
}
|
|
780
|
+
if (fs.existsSync(path.join(dir, '.git'))) return boundary;
|
|
781
|
+
} else if (PROJECT_ROOT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) {
|
|
782
|
+
boundary = { dir, hasDesign: false };
|
|
783
|
+
// A boundary that is itself a monorepo root, or a separate repository
|
|
784
|
+
// with its own .git, inherits nothing from above.
|
|
785
|
+
if (isMonorepoRoot(dir) || fs.existsSync(path.join(dir, '.git'))) return boundary;
|
|
510
786
|
}
|
|
511
|
-
if (dir
|
|
787
|
+
if (homeDirs.has(dir)) return boundary;
|
|
512
788
|
const parent = path.dirname(dir);
|
|
513
|
-
if (parent === dir) return
|
|
789
|
+
if (parent === dir) return boundary;
|
|
514
790
|
dir = parent;
|
|
515
791
|
}
|
|
516
792
|
}
|
|
@@ -551,6 +827,20 @@ function isAllowedColorRaw(raw, designSystem) {
|
|
|
551
827
|
return false;
|
|
552
828
|
}
|
|
553
829
|
|
|
830
|
+
// A color is a documented shadow color only when both the r/g/b channels AND
|
|
831
|
+
// the alpha match a sidecar shadow token's color. Alpha has to be compared
|
|
832
|
+
// here because colorKey()/colorsClose() drop it, and a match on r/g/b alone
|
|
833
|
+
// would let every black at every alpha through.
|
|
834
|
+
function isAllowedShadowColorRaw(raw, designSystem) {
|
|
835
|
+
if (!designSystem?.allowedShadowColors?.length) return false;
|
|
836
|
+
const parsed = parseDesignColor(String(raw || '').trim().toLowerCase());
|
|
837
|
+
if (!parsed) return false;
|
|
838
|
+
return designSystem.allowedShadowColors.some(entry =>
|
|
839
|
+
colorsClose(parsed, entry.color) &&
|
|
840
|
+
Math.abs((parsed.a ?? 1) - (entry.color.a ?? 1)) <= SHADOW_ALPHA_TOLERANCE,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
554
844
|
function isAllowedRadiusRaw(raw, designSystem) {
|
|
555
845
|
if (!designSystem?.hasRadii) return true;
|
|
556
846
|
const text = String(raw || '').trim().toLowerCase();
|
|
@@ -628,6 +918,40 @@ function isProbablyColorLiteral(line, match) {
|
|
|
628
918
|
return styleContext || cssFunctionContext || jsColorKeyContext;
|
|
629
919
|
}
|
|
630
920
|
|
|
921
|
+
// One complete `${...}` template interpolation. Its content may carry paired
|
|
922
|
+
// quoted strings (function arguments, ternary branches) and one level of
|
|
923
|
+
// braces (an object-literal argument, itself allowing paired quotes). Deeper
|
|
924
|
+
// nesting would need a parser, so the regex deliberately fails safe there:
|
|
925
|
+
// the context check misses and the finding fires — a false positive a waiver
|
|
926
|
+
// can silence, never a leak.
|
|
927
|
+
const QUOTED_STRING_SRC = `"[^"]*"|'[^']*'`;
|
|
928
|
+
const INTERPOLATION_SRC =
|
|
929
|
+
`\\$\\{(?:${QUOTED_STRING_SRC}|\\{(?:${QUOTED_STRING_SRC}|[^{}"'\`])*\\}|[^{}"'\`])*\\}`;
|
|
930
|
+
// The two shadow-context tails. Unlike jsColorKeyContext, the JS tail admits
|
|
931
|
+
// commas: a multi-layer shadow string is comma-separated, and a later
|
|
932
|
+
// property on the same line is still blocked because it sits past the
|
|
933
|
+
// string's closing quote. Both tails admit complete interpolations; a bare
|
|
934
|
+
// `}`, quote, or `;` still ends the context.
|
|
935
|
+
const SHADOW_CSS_CONTEXT_RE = new RegExp(
|
|
936
|
+
`(?:^|[{\\s;"'\`(,])(?:box-shadow|text-shadow)\\s*:\\s*(?:${INTERPOLATION_SRC}|[^;{}"'\`])*$`, 'i',
|
|
937
|
+
);
|
|
938
|
+
const SHADOW_JS_CONTEXT_RE = new RegExp(
|
|
939
|
+
`(?:^|[,{]\\s*)(?:boxShadow|textShadow)\\s*[:=]\\s*["'\`]?(?:${INTERPOLATION_SRC}|[^"'\`}])*$`, 'i',
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
// True when the color literal sits inside a box-shadow / text-shadow value —
|
|
943
|
+
// the only contexts where a documented shadow color is legal. Anchored to the
|
|
944
|
+
// end of `before` (no ; } { or quote in between) so a shadow property earlier
|
|
945
|
+
// on the line can't leak the allowance into a later declaration. Kept separate
|
|
946
|
+
// from isProbablyColorLiteral(), which stays a boolean for its existing call
|
|
947
|
+
// sites and deliberately discards which property matched.
|
|
948
|
+
function isShadowPropertyContext(line, match) {
|
|
949
|
+
const index = match.index ?? -1;
|
|
950
|
+
if (index < 0) return false;
|
|
951
|
+
const before = line.slice(0, index);
|
|
952
|
+
return SHADOW_CSS_CONTEXT_RE.test(before) || SHADOW_JS_CONTEXT_RE.test(before);
|
|
953
|
+
}
|
|
954
|
+
|
|
631
955
|
function isInsideCssAttributeSelector(line, index) {
|
|
632
956
|
if (index < 0) return false;
|
|
633
957
|
const before = line.slice(0, index);
|
|
@@ -671,7 +995,9 @@ function extractRadiusTokens(value) {
|
|
|
671
995
|
return String(value || '')
|
|
672
996
|
.replace(/\s*\/\s*/g, ' ')
|
|
673
997
|
.split(/\s+/)
|
|
674
|
-
|
|
998
|
+
// var() fallbacks leave the closing parenthesis on the final token. Strip
|
|
999
|
+
// it before length resolution so `8px)` is not treated as unitless 8rem.
|
|
1000
|
+
.map(token => token.trim().replace(/\)+$/, ''))
|
|
675
1001
|
.filter(Boolean);
|
|
676
1002
|
}
|
|
677
1003
|
|
|
@@ -761,6 +1087,7 @@ function checkSourceDesignSystem(content, filePath, options = {}) {
|
|
|
761
1087
|
if (!isProbablyColorLiteral(line, match)) continue;
|
|
762
1088
|
const raw = cssColorLabel(match[0]);
|
|
763
1089
|
if (isAllowedColorRaw(raw, designSystem)) continue;
|
|
1090
|
+
if (isShadowPropertyContext(line, match) && isAllowedShadowColorRaw(raw, designSystem)) continue;
|
|
764
1091
|
findings.push(makeDesignFinding(
|
|
765
1092
|
'design-system-color',
|
|
766
1093
|
filePath,
|
|
@@ -975,6 +1302,7 @@ export {
|
|
|
975
1302
|
loadDesignSystemForCwd,
|
|
976
1303
|
isAllowedFont,
|
|
977
1304
|
isAllowedColorRaw,
|
|
1305
|
+
isAllowedShadowColorRaw,
|
|
978
1306
|
isAllowedRadiusRaw,
|
|
979
1307
|
isAllowedFontSizeRaw,
|
|
980
1308
|
checkSourceDesignSystem,
|