quiver-cli 0.7.0 → 1.0.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/README.md +110 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +733 -499
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Critique persistence helper.
|
|
4
4
|
*
|
|
5
|
-
* Each run
|
|
5
|
+
* Each critique run writes a per-target snapshot to
|
|
6
6
|
* .impeccable/critique/<timestamp>__<slug>.md
|
|
7
7
|
* with a small YAML frontmatter carrying the score + P0/P1 counts.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* The polish workflow reads the latest matching snapshot at start as its
|
|
10
10
|
* fix backlog. No other skill auto-reads critique output.
|
|
11
11
|
*
|
|
12
12
|
* The slug is derived mechanically from the *resolved* primary artifact
|
|
@@ -29,8 +29,9 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
|
32
|
+
import { slugFromTarget } from './lib/target-slug.mjs';
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
export { slugFromTarget } from './lib/target-slug.mjs';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Mechanically derive a slug from a resolved target. Returns null if the
|
|
@@ -40,46 +41,6 @@ const SLUG_MAX = 50;
|
|
|
40
41
|
* concrete artifact before calling this — we never slug a natural-language
|
|
41
42
|
* phrase.
|
|
42
43
|
*/
|
|
43
|
-
export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
|
|
44
|
-
if (!resolved || typeof resolved !== 'string') return null;
|
|
45
|
-
const trimmed = resolved.trim();
|
|
46
|
-
if (!trimmed) return null;
|
|
47
|
-
|
|
48
|
-
// URL
|
|
49
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
50
|
-
let url;
|
|
51
|
-
try { url = new URL(trimmed); } catch { return null; }
|
|
52
|
-
const hostPath = `${url.hostname}${url.pathname}`;
|
|
53
|
-
return kebab(hostPath);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// File path. Make it project-relative so two devs critiquing the same
|
|
57
|
-
// checkout get the same slug regardless of where their repo is cloned.
|
|
58
|
-
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
|
59
|
-
let rel = path.relative(cwd, abs);
|
|
60
|
-
// If the target is outside cwd, fall back to the basename so we still
|
|
61
|
-
// produce a stable slug (vs the absolute path, which would include
|
|
62
|
-
// home dirs / usernames).
|
|
63
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
64
|
-
rel = path.basename(abs);
|
|
65
|
-
}
|
|
66
|
-
if (!rel || rel === '.' || rel === '') return null;
|
|
67
|
-
return kebab(rel);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function kebab(s) {
|
|
71
|
-
const slug = s
|
|
72
|
-
.toLowerCase()
|
|
73
|
-
.replace(/[/\\.]+/g, '-')
|
|
74
|
-
.replace(/[^a-z0-9-]+/g, '-')
|
|
75
|
-
.replace(/-+/g, '-')
|
|
76
|
-
.replace(/^-|-$/g, '');
|
|
77
|
-
if (!slug) return null;
|
|
78
|
-
// Cap from the tail — the tail (filename) is more identifying than the
|
|
79
|
-
// top-level directory.
|
|
80
|
-
return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
44
|
/**
|
|
84
45
|
* Filename-safe UTC ISO timestamp: hyphens for separators, trailing Z.
|
|
85
46
|
* Plain colons aren't allowed on Windows filesystems.
|
|
@@ -180,6 +141,15 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
|
|
180
141
|
|
|
181
142
|
// ---- CLI ---------------------------------------------------------------
|
|
182
143
|
|
|
144
|
+
// Accept either a ready slug or a concrete target (path/URL) everywhere, so
|
|
145
|
+
// callers never have to run the slug step separately. Anything containing a
|
|
146
|
+
// path or URL marker is resolved through slugFromTarget.
|
|
147
|
+
function coerceSlug(value) {
|
|
148
|
+
if (!value) return null;
|
|
149
|
+
if (/^[a-z0-9-]+$/.test(value) && !value.includes('/')) return value;
|
|
150
|
+
return slugFromTarget(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
183
153
|
function main(argv) {
|
|
184
154
|
const [cmd, ...args] = argv;
|
|
185
155
|
switch (cmd) {
|
|
@@ -190,8 +160,9 @@ function main(argv) {
|
|
|
190
160
|
return;
|
|
191
161
|
}
|
|
192
162
|
case 'write': {
|
|
193
|
-
const [
|
|
194
|
-
|
|
163
|
+
const [slugArg, bodyFile] = args;
|
|
164
|
+
const slug = coerceSlug(slugArg);
|
|
165
|
+
if (!slug || !bodyFile) { process.stderr.write('usage: write <slug-or-target> <body-file>\n'); process.exit(1); }
|
|
195
166
|
const raw = fs.readFileSync(bodyFile, 'utf-8');
|
|
196
167
|
// The body file may be a full report. The caller passes the meta as
|
|
197
168
|
// a JSON object on stdin if it wants structured frontmatter; otherwise
|
|
@@ -206,13 +177,13 @@ function main(argv) {
|
|
|
206
177
|
return;
|
|
207
178
|
}
|
|
208
179
|
case 'latest': {
|
|
209
|
-
const latest = readLatestSnapshot(args[0]);
|
|
180
|
+
const latest = readLatestSnapshot(coerceSlug(args[0]));
|
|
210
181
|
if (!latest) { process.exit(2); }
|
|
211
182
|
process.stdout.write(latest.body);
|
|
212
183
|
return;
|
|
213
184
|
}
|
|
214
185
|
case 'trend': {
|
|
215
|
-
const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
|
|
186
|
+
const rows = readTrend(coerceSlug(args[0]), { limit: args[1] ? Number(args[1]) : 5 });
|
|
216
187
|
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
|
217
188
|
return;
|
|
218
189
|
}
|
package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs
CHANGED
|
@@ -530,7 +530,11 @@ if (IS_BROWSER) {
|
|
|
530
530
|
function generateSelector(el) {
|
|
531
531
|
if (el === document.body) return 'body';
|
|
532
532
|
if (el === document.documentElement) return 'html';
|
|
533
|
-
|
|
533
|
+
// Read via getAttribute when `el.id` is not a string — a <form> with a
|
|
534
|
+
// named control (e.g. <input name="id">) shadows the builtin getter and
|
|
535
|
+
// returns the element, producing a garbage `#[object …]` selector (#407).
|
|
536
|
+
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
|
|
537
|
+
if (elId) return '#' + CSS.escape(elId);
|
|
534
538
|
|
|
535
539
|
const parts = [];
|
|
536
540
|
let current = el;
|
|
@@ -1222,7 +1226,11 @@ if (IS_BROWSER) {
|
|
|
1222
1226
|
return {
|
|
1223
1227
|
type: f.type || f.id,
|
|
1224
1228
|
category: ap ? ap.category : 'quality',
|
|
1225
|
-
severity: ap?.severity || 'warning',
|
|
1229
|
+
severity: f.severity || ap?.severity || 'warning',
|
|
1230
|
+
// Advisory findings (em-dash overuse, etc.) are surfaced but never
|
|
1231
|
+
// treated as failures; carry the flag so the overlay/extension can
|
|
1232
|
+
// render them with the mildest affordance and consumers can filter.
|
|
1233
|
+
advisory: (ap && ap.advisory === true) || f.advisory === true,
|
|
1226
1234
|
detail: f.detail || f.snippet,
|
|
1227
1235
|
ignoreValue: f.ignoreValue || f.value || '',
|
|
1228
1236
|
name: ap ? ap.name : (f.type || f.id),
|
|
@@ -1458,16 +1466,16 @@ if (IS_BROWSER) {
|
|
|
1458
1466
|
const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id);
|
|
1459
1467
|
const designSystem = browserDesignSystemConfig();
|
|
1460
1468
|
const designSeen = { fonts: new Set(), colors: new Set(), radii: new Set() };
|
|
1461
|
-
//
|
|
1462
|
-
// real browser env (detector page, live overlay, extension) running every
|
|
1463
|
-
// check is free, so we always surface them; the gating is purely a CLI
|
|
1464
|
-
// output concern, applied in the Node engines' detect* return paths.
|
|
1469
|
+
// All deterministic rules run in the browser and extension path.
|
|
1465
1470
|
|
|
1466
1471
|
for (const el of document.querySelectorAll('*')) {
|
|
1467
1472
|
// Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons)
|
|
1468
1473
|
if (el.closest('.impeccable-overlay, .impeccable-label, .impeccable-banner, .impeccable-tooltip')) continue;
|
|
1469
|
-
// Skip browser extension elements (Claude, etc.)
|
|
1470
|
-
|
|
1474
|
+
// Skip browser extension elements (Claude, etc.). Use getAttribute when
|
|
1475
|
+
// `el.id` is not a string: a <form> with a named control like
|
|
1476
|
+
// <input name="id"> shadows the builtin `id` getter and returns the
|
|
1477
|
+
// element, whose `.startsWith` throws (issue #407).
|
|
1478
|
+
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute('id') || '');
|
|
1471
1479
|
if (elId.startsWith('claude-') || elId.startsWith('cic-')) continue;
|
|
1472
1480
|
// Skip the impeccable live-mode overlay (highlight, tooltip, bar, picker, toast).
|
|
1473
1481
|
// These are inspector chrome, not part of the user's design.
|
|
@@ -1477,10 +1485,12 @@ if (IS_BROWSER) {
|
|
|
1477
1485
|
|
|
1478
1486
|
const findings = [
|
|
1479
1487
|
...checkElementBordersDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1488
|
+
...checkElementPseudoStripeDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1480
1489
|
...checkElementColorsDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1481
1490
|
...checkElementMotionDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1482
1491
|
...checkElementGlowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1483
1492
|
...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1493
|
+
...checkElementRadialSpotlightDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1484
1494
|
...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1485
1495
|
...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1486
1496
|
...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
@@ -1488,6 +1498,7 @@ if (IS_BROWSER) {
|
|
|
1488
1498
|
...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1489
1499
|
...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1490
1500
|
...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })),
|
|
1501
|
+
...checkElementBlinkingCursorDOM(el).map(f => ({ type: f.id, detail: f.snippet, ...(f.severity ? { severity: f.severity } : {}) })),
|
|
1491
1502
|
...checkElementDesignSystemDOM(el, designSystem, designSeen),
|
|
1492
1503
|
].filter(f => _ruleOk(f.type));
|
|
1493
1504
|
|
|
@@ -1518,7 +1529,7 @@ if (IS_BROWSER) {
|
|
|
1518
1529
|
addBrowserFindings(groupMap, document.body, typoFindings);
|
|
1519
1530
|
}
|
|
1520
1531
|
|
|
1521
|
-
const sectionKickerFindings =
|
|
1532
|
+
const sectionKickerFindings = checkKickerAboveHeadingDOM()
|
|
1522
1533
|
.map(f => ({ type: f.id, detail: f.snippet }))
|
|
1523
1534
|
.filter(f => _ruleOk(f.type));
|
|
1524
1535
|
if (sectionKickerFindings.length > 0) {
|
|
@@ -1526,12 +1537,66 @@ if (IS_BROWSER) {
|
|
|
1526
1537
|
addBrowserFindings(groupMap, document.body, sectionKickerFindings);
|
|
1527
1538
|
}
|
|
1528
1539
|
|
|
1540
|
+
const numberedLabelFindings = checkNumberedSectionLabelsDOM()
|
|
1541
|
+
.map(f => ({ type: f.id, detail: f.snippet }))
|
|
1542
|
+
.filter(f => _ruleOk(f.type));
|
|
1543
|
+
if (numberedLabelFindings.length > 0) {
|
|
1544
|
+
pageLevelFindings.push(...numberedLabelFindings);
|
|
1545
|
+
addBrowserFindings(groupMap, document.body, numberedLabelFindings);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const repeatedTextFindings = checkRepeatedContainerTextDOM()
|
|
1549
|
+
.map(f => ({ type: f.id, detail: f.snippet }))
|
|
1550
|
+
.filter(f => _ruleOk(f.type));
|
|
1551
|
+
if (repeatedTextFindings.length > 0) {
|
|
1552
|
+
pageLevelFindings.push(...repeatedTextFindings);
|
|
1553
|
+
addBrowserFindings(groupMap, document.body, repeatedTextFindings);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Em-dash overuse (advisory): browser parity with the static/regex path.
|
|
1557
|
+
// Reads rendered body text so it catches dashes written as HTML entities.
|
|
1558
|
+
// serializeFindings stamps the advisory flag from the registry.
|
|
1559
|
+
const emDashFindings = checkEmDashOveruseDOM()
|
|
1560
|
+
.map(f => ({ type: f.id, detail: f.snippet }))
|
|
1561
|
+
.filter(f => _ruleOk(f.type));
|
|
1562
|
+
if (emDashFindings.length > 0) {
|
|
1563
|
+
pageLevelFindings.push(...emDashFindings);
|
|
1564
|
+
addBrowserFindings(groupMap, document.body, emDashFindings);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1529
1567
|
const layoutFindings = checkLayout().filter(f => _ruleOk(f.type));
|
|
1530
1568
|
for (const f of layoutFindings) {
|
|
1531
1569
|
const el = f.el || document.body;
|
|
1532
1570
|
addBrowserFindings(groupMap, el, [{ type: f.type, detail: f.detail || f.snippet }]);
|
|
1533
1571
|
}
|
|
1534
1572
|
|
|
1573
|
+
// Heading rhythm (browser-only: needs real layout for the gap math)
|
|
1574
|
+
const headingRhythmFindings = checkHeadingRhythmDOM().filter(f => _ruleOk(f.type));
|
|
1575
|
+
for (const f of headingRhythmFindings) {
|
|
1576
|
+
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Edge-flush cards in horizontal scrollers (browser-only: needs real
|
|
1580
|
+
// layout for the scroller clip box vs card rect math)
|
|
1581
|
+
const edgeFlushFindings = checkEdgeFlushCardsDOM().filter(f => _ruleOk(f.type));
|
|
1582
|
+
for (const f of edgeFlushFindings) {
|
|
1583
|
+
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Text occlusion / element overlap (browser-only: needs real layout +
|
|
1587
|
+
// elementFromPoint to confirm what actually paints on top)
|
|
1588
|
+
const occlusionFindings = checkTextOcclusionDOM().filter(f => _ruleOk(f.type));
|
|
1589
|
+
for (const f of occlusionFindings) {
|
|
1590
|
+
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// First-viewport column overflow — the stretched-hero signature
|
|
1594
|
+
// (browser-only: needs real layout for the content-extent math)
|
|
1595
|
+
const colOverflowFindings = checkFirstViewportColumnOverflowDOM().filter(f => _ruleOk(f.type));
|
|
1596
|
+
for (const f of colOverflowFindings) {
|
|
1597
|
+
addBrowserFindings(groupMap, f.el || document.body, [{ type: f.type, detail: f.detail }]);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1535
1600
|
// Page-level quality checks (headings, etc.)
|
|
1536
1601
|
const qualityFindings = checkPageQualityDOM().filter(f => _ruleOk(f.type));
|
|
1537
1602
|
if (qualityFindings.length > 0) {
|
|
@@ -1557,7 +1622,25 @@ if (IS_BROWSER) {
|
|
|
1557
1622
|
}
|
|
1558
1623
|
const htmlPatternFindings = checkHtmlPatterns(docClone.outerHTML);
|
|
1559
1624
|
if (htmlPatternFindings.length > 0) {
|
|
1560
|
-
const mapped = htmlPatternFindings.map(f =>
|
|
1625
|
+
const mapped = htmlPatternFindings.map(f => {
|
|
1626
|
+
const item = { type: f.id, detail: f.snippet };
|
|
1627
|
+
if (f.severity) {
|
|
1628
|
+
item.severity = f.severity;
|
|
1629
|
+
} else if (f.id === 'pulsing-dot' && f.selector) {
|
|
1630
|
+
// The string scan promotes header/nav dots on its own; with a live
|
|
1631
|
+
// layout also promote dots resting in the first ~900px of the page
|
|
1632
|
+
// (the hero region), which the source scan cannot measure.
|
|
1633
|
+
try {
|
|
1634
|
+
const dotEl = document.querySelector(f.selector);
|
|
1635
|
+
if (dotEl) {
|
|
1636
|
+
const rect = dotEl.getBoundingClientRect();
|
|
1637
|
+
const pageTop = rect.top + (window.scrollY || 0);
|
|
1638
|
+
if (pageTop <= 900) item.severity = 'error';
|
|
1639
|
+
}
|
|
1640
|
+
} catch { /* unresolvable selector: keep registry severity */ }
|
|
1641
|
+
}
|
|
1642
|
+
return item;
|
|
1643
|
+
}).filter(f => _ruleOk(f.type));
|
|
1561
1644
|
pageLevelFindings.push(...mapped);
|
|
1562
1645
|
addBrowserFindings(groupMap, document.body, mapped);
|
|
1563
1646
|
}
|
|
@@ -1931,6 +2014,9 @@ if (IS_BROWSER) {
|
|
|
1931
2014
|
window.impeccableDetectAsync = detectAsync;
|
|
1932
2015
|
window.impeccableScan = scan;
|
|
1933
2016
|
window.impeccableScanAsync = scanAsync;
|
|
2017
|
+
// Raw measurement for the URL engine's content-hidden-at-rest pass: it
|
|
2018
|
+
// drives a reveal sweep from Node and thresholds the result itself.
|
|
2019
|
+
window.impeccableMeasureHiddenText = measureHiddenTextDOM;
|
|
1934
2020
|
window.impeccableCollectVisualContrastCandidates = collectVisualContrastCandidates;
|
|
1935
2021
|
window.impeccableAnalyzeVisualContrast = analyzeVisualContrast;
|
|
1936
2022
|
window.impeccableGetLastVisualContrastAnalyses = () => lastVisualContrastAnalyses.slice();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { loadDesignSystemForTarget } from '../design-system.mjs';
|
|
6
|
+
import { RULE_SCOPES, filterByScopes } from '../registry/antipatterns.mjs';
|
|
5
7
|
import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs';
|
|
6
8
|
import { detectHtml } from '../engines/static-html/detect-html.mjs';
|
|
7
9
|
import { detectText } from '../engines/regex/detect-text.mjs';
|
|
@@ -26,9 +28,37 @@ function formatFindingSummary(count) {
|
|
|
26
28
|
return `${count} anti-pattern${count === 1 ? '' : 's'} found.`;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
// Local filesystem path behind a file:// URL, or null when it can't be mapped.
|
|
32
|
+
function fileUrlToLocalPath(url) {
|
|
33
|
+
try {
|
|
34
|
+
return fileURLToPath(url);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Advisory findings are detected but never treated as failures: they list in a
|
|
41
|
+
// separate, visually dimmed section, are excluded from the failure count that
|
|
42
|
+
// drives the exit code, and carry `"advisory": true` in JSON so consumers can
|
|
43
|
+
// filter. Every advisory finding carries the flag (stamped by the registry via
|
|
44
|
+
// findings.mjs).
|
|
45
|
+
function isAdvisory(finding) {
|
|
46
|
+
return finding && finding.advisory === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function partitionAdvisory(findings) {
|
|
50
|
+
const primary = [];
|
|
51
|
+
const advisory = [];
|
|
52
|
+
for (const f of findings) (isAdvisory(f) ? advisory : primary).push(f);
|
|
53
|
+
return { primary, advisory };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ANSI dim, when stderr is a TTY. Advisory output is chrome, so keep it quiet.
|
|
57
|
+
function dim(text) {
|
|
58
|
+
return process.stderr.isTTY ? `\x1b[2m${text}\x1b[0m` : text;
|
|
59
|
+
}
|
|
31
60
|
|
|
61
|
+
function formatFindingsBody(findings) {
|
|
32
62
|
const grouped = {};
|
|
33
63
|
for (const f of findings) {
|
|
34
64
|
if (!grouped[f.file]) grouped[f.file] = [];
|
|
@@ -43,7 +73,28 @@ function formatFindings(findings, jsonMode) {
|
|
|
43
73
|
out.push(` → ${item.description}`);
|
|
44
74
|
}
|
|
45
75
|
}
|
|
46
|
-
out
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatAdvisorySection(advisory) {
|
|
80
|
+
if (!advisory || advisory.length === 0) return '';
|
|
81
|
+
const lines = [`\n${dim('── Advisory (not counted as failures) ──')}`];
|
|
82
|
+
for (const line of formatFindingsBody(advisory)) lines.push(dim(line));
|
|
83
|
+
lines.push(dim(`\n${advisory.length} advisory note${advisory.length === 1 ? '' : 's'}. Suppress with --no-advisory.`));
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Text/JSON formatter. `findings` is the full set; advisory items are separated
|
|
88
|
+
// out into their own section and excluded from the failure summary count. JSON
|
|
89
|
+
// output keeps every finding (each advisory one flagged) in a single array.
|
|
90
|
+
function formatFindings(findings, jsonMode) {
|
|
91
|
+
if (jsonMode) return JSON.stringify(findings, null, 2);
|
|
92
|
+
|
|
93
|
+
const { primary, advisory } = partitionAdvisory(findings);
|
|
94
|
+
const out = [...formatFindingsBody(primary)];
|
|
95
|
+
out.push(`\n${formatFindingSummary(primary.length)}`);
|
|
96
|
+
const advisorySection = formatAdvisorySection(advisory);
|
|
97
|
+
if (advisorySection) out.push(advisorySection);
|
|
47
98
|
return out.join('\n');
|
|
48
99
|
}
|
|
49
100
|
|
|
@@ -51,7 +102,11 @@ function formatFindings(findings, jsonMode) {
|
|
|
51
102
|
// Stdin handling
|
|
52
103
|
// ---------------------------------------------------------------------------
|
|
53
104
|
|
|
54
|
-
|
|
105
|
+
// `optionsFor` maps a local path to scan options carrying that path's own
|
|
106
|
+
// project design system (or base options when null). Falls back to a plain
|
|
107
|
+
// object so direct/legacy callers still work.
|
|
108
|
+
async function handleStdin(optionsFor = () => ({})) {
|
|
109
|
+
const resolve = typeof optionsFor === 'function' ? optionsFor : () => optionsFor;
|
|
55
110
|
const chunks = [];
|
|
56
111
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
57
112
|
const input = Buffer.concat(chunks).toString('utf-8');
|
|
@@ -59,11 +114,12 @@ async function handleStdin(options = {}) {
|
|
|
59
114
|
const parsed = JSON.parse(input);
|
|
60
115
|
const fp = parsed?.tool_input?.file_path;
|
|
61
116
|
if (fp && fs.existsSync(fp)) {
|
|
117
|
+
const options = resolve(fp);
|
|
62
118
|
return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())
|
|
63
119
|
? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);
|
|
64
120
|
}
|
|
65
121
|
} catch { /* not JSON */ }
|
|
66
|
-
return detectText(input, '<stdin>',
|
|
122
|
+
return detectText(input, '<stdin>', resolve(null));
|
|
67
123
|
}
|
|
68
124
|
|
|
69
125
|
|
|
@@ -91,14 +147,22 @@ Scan files or URLs for UI anti-patterns and design quality issues.
|
|
|
91
147
|
Options:
|
|
92
148
|
--json Output results as JSON
|
|
93
149
|
--quiet In text mode, only print the final findings count
|
|
94
|
-
--
|
|
95
|
-
|
|
150
|
+
--scope <name> Only report rules in the given design domain
|
|
151
|
+
(type, layout). Comma-separated.
|
|
152
|
+
--viewport <WxH> Browser viewport for URL scans (default 1280x800),
|
|
153
|
+
e.g. --viewport 390x844 for a mobile-width pass
|
|
96
154
|
--no-config Do not apply project config, detector ignores, inline
|
|
97
155
|
ignore comments, or DESIGN.md
|
|
98
156
|
--no-inline-ignores Do not honor in-file impeccable-disable* ignore comments
|
|
99
157
|
--no-design-system Do not load local DESIGN.md / .impeccable/design.json context
|
|
158
|
+
--no-advisory Suppress advisory findings entirely (e.g. em-dash overuse)
|
|
100
159
|
--help Show this help message
|
|
101
160
|
|
|
161
|
+
Advisory findings:
|
|
162
|
+
Some rules are advisory: detected and listed in a separate section, but never
|
|
163
|
+
counted as failures and never changing the exit code. They stay out of the
|
|
164
|
+
failure count so they never block automation. --no-advisory hides them.
|
|
165
|
+
|
|
102
166
|
Project config:
|
|
103
167
|
Respects .impeccable/config.json and .impeccable/config.local.json detector
|
|
104
168
|
settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,
|
|
@@ -115,7 +179,8 @@ Inline ignores:
|
|
|
115
179
|
Detection modes:
|
|
116
180
|
HTML files Static HTML/CSS analysis (default, catches linked CSS)
|
|
117
181
|
Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)
|
|
118
|
-
URLs Puppeteer full browser rendering (auto-detected
|
|
182
|
+
URLs Puppeteer full browser rendering (auto-detected;
|
|
183
|
+
http(s):// and file:// URLs)
|
|
119
184
|
|
|
120
185
|
Examples:
|
|
121
186
|
impeccable detect src/
|
|
@@ -135,6 +200,7 @@ async function detectCli() {
|
|
|
135
200
|
const jsonMode = args.includes('--json');
|
|
136
201
|
const quietMode = args.includes('--quiet');
|
|
137
202
|
const helpMode = args.includes('--help');
|
|
203
|
+
const noAdvisory = args.includes('--no-advisory');
|
|
138
204
|
// --fast (regex-only) is deprecated: since the jsdom removal, the static
|
|
139
205
|
// HTML/CSS analysis is fast and covers every rule, so the regex-only path
|
|
140
206
|
// only loses coverage for no real speed win. Accept the flag for back-compat
|
|
@@ -144,21 +210,74 @@ async function detectCli() {
|
|
|
144
210
|
'Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n',
|
|
145
211
|
);
|
|
146
212
|
}
|
|
213
|
+
if (args.includes('--gpt') || args.includes('--gemini')) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
'Note: --gpt and --gemini are deprecated and ignored. Generated-UI tells now run by default.\n',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
147
218
|
const configEnabled = !args.includes('--no-config');
|
|
148
219
|
const detectionConfig = configEnabled
|
|
149
220
|
? readDetectionConfig(process.cwd())
|
|
150
221
|
: { ignoreRules: [], ignoreFiles: [], ignoreValues: [] };
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
222
|
+
const scopes = [];
|
|
223
|
+
for (let i = 0; i < args.length; i++) {
|
|
224
|
+
if (args[i] !== '--scope' && !args[i].startsWith('--scope=')) continue;
|
|
225
|
+
const inline = args[i].startsWith('--scope=');
|
|
226
|
+
const value = inline ? args[i].slice('--scope='.length) : args[i + 1];
|
|
227
|
+
const parsed = (value && !value.startsWith('--'))
|
|
228
|
+
? value.split(',').map(s => s.trim()).filter(Boolean)
|
|
229
|
+
: [];
|
|
230
|
+
// A bare `--scope` would otherwise fall out of `targets` and scan unscoped;
|
|
231
|
+
// fail loudly so a mistyped pre-scan never runs the wrong rule set.
|
|
232
|
+
if (parsed.length === 0) {
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`Error: --scope requires a value. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`,
|
|
235
|
+
);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
scopes.push(...parsed);
|
|
239
|
+
args.splice(i, inline ? 1 : 2);
|
|
240
|
+
i -= 1;
|
|
241
|
+
}
|
|
242
|
+
let viewport = null;
|
|
243
|
+
for (let i = 0; i < args.length; i++) {
|
|
244
|
+
if (args[i] !== '--viewport' && !args[i].startsWith('--viewport=')) continue;
|
|
245
|
+
const inline = args[i].startsWith('--viewport=');
|
|
246
|
+
const value = inline ? args[i].slice('--viewport='.length) : args[i + 1];
|
|
247
|
+
const match = /^(\d{2,5})x(\d{2,5})$/i.exec(value || '');
|
|
248
|
+
if (!match) {
|
|
249
|
+
process.stderr.write('Error: --viewport requires a WxH value, e.g. --viewport 390x844\n');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
viewport = { width: Number(match[1]), height: Number(match[2]) };
|
|
253
|
+
args.splice(i, inline ? 1 : 2);
|
|
254
|
+
i -= 1;
|
|
255
|
+
}
|
|
256
|
+
const unknownScopes = scopes.filter(s => !RULE_SCOPES.has(s));
|
|
257
|
+
if (unknownScopes.length > 0) {
|
|
258
|
+
process.stderr.write(
|
|
259
|
+
`Error: unknown --scope value(s): ${unknownScopes.join(', ')}. Valid scopes: ${[...RULE_SCOPES].join(', ')}\n`,
|
|
260
|
+
);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
154
263
|
const designSystemEnabled = configEnabled && !args.includes('--no-design-system') && detectionConfig.designSystem?.enabled !== false;
|
|
155
|
-
const designSystem = designSystemEnabled ? loadDesignSystemForCwd(process.cwd()) : null;
|
|
156
264
|
// Inline `impeccable-disable*` waivers are part of the scanned file, so they
|
|
157
265
|
// apply by default. `--no-config` (raw scan) and the dedicated
|
|
158
266
|
// `--no-inline-ignores` both turn them off.
|
|
159
267
|
const inlineIgnoresEnabled = configEnabled && !args.includes('--no-inline-ignores');
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
268
|
+
const baseScanOptions = { inlineIgnores: inlineIgnoresEnabled };
|
|
269
|
+
if (viewport) baseScanOptions.viewport = viewport;
|
|
270
|
+
// DESIGN.md must resolve from EACH scan target's own project root, not from
|
|
271
|
+
// process.cwd(): scanning project B's files from inside project A applied A's
|
|
272
|
+
// design rules (cross-project contamination). Resolve per target, memoized by
|
|
273
|
+
// resolved project root so a multi-file scan pays the read once per project.
|
|
274
|
+
// A target with no project marker above it gets no design system (never cwd's).
|
|
275
|
+
const designSystemCache = new Map();
|
|
276
|
+
const scanOptionsFor = (localPath) => {
|
|
277
|
+
if (!designSystemEnabled || !localPath) return baseScanOptions;
|
|
278
|
+
const designSystem = loadDesignSystemForTarget(localPath, { cache: designSystemCache });
|
|
279
|
+
return designSystem ? { ...baseScanOptions, designSystem } : baseScanOptions;
|
|
280
|
+
};
|
|
162
281
|
const targets = args.filter(a => !a.startsWith('--'));
|
|
163
282
|
|
|
164
283
|
if (helpMode) { printUsage(); process.exit(0); }
|
|
@@ -166,19 +285,31 @@ async function detectCli() {
|
|
|
166
285
|
let allFindings = [];
|
|
167
286
|
|
|
168
287
|
if (!process.stdin.isTTY && targets.length === 0) {
|
|
169
|
-
allFindings = await handleStdin(
|
|
288
|
+
allFindings = await handleStdin(scanOptionsFor);
|
|
170
289
|
} else {
|
|
171
290
|
const paths = targets.length > 0 ? targets : [process.cwd()];
|
|
172
|
-
|
|
291
|
+
// file:// URLs get the same Puppeteer-rendered pass as http(s) — the
|
|
292
|
+
// real cascade, real computed styles, real layout. Callers that want a
|
|
293
|
+
// browser-grade scan of a local artifact can pass file:///abs/path.html
|
|
294
|
+
// instead of the bare path (which stays on the static engine).
|
|
295
|
+
const urlRe = /^(?:https?|file):\/\//i;
|
|
296
|
+
const urlTargetCount = paths.filter(target => urlRe.test(target)).length;
|
|
173
297
|
const browserDetector = urlTargetCount > 1 ? await createBrowserDetector() : null;
|
|
174
298
|
|
|
175
299
|
try {
|
|
176
300
|
for (const target of paths) {
|
|
177
|
-
if (
|
|
301
|
+
if (urlRe.test(target)) {
|
|
302
|
+
// A file:// URL points at a local artifact, so its design system
|
|
303
|
+
// resolves from that file's project. A remote http(s) URL has no
|
|
304
|
+
// local project — it gets base options (no design system), never
|
|
305
|
+
// process.cwd()'s.
|
|
306
|
+
const urlOptions = /^file:/i.test(target)
|
|
307
|
+
? scanOptionsFor(fileUrlToLocalPath(target))
|
|
308
|
+
: baseScanOptions;
|
|
178
309
|
try {
|
|
179
310
|
const scanner = browserDetector
|
|
180
|
-
? (url) => browserDetector.detectUrl(url,
|
|
181
|
-
: (url) => detectUrl(url,
|
|
311
|
+
? (url) => browserDetector.detectUrl(url, urlOptions)
|
|
312
|
+
: (url) => detectUrl(url, urlOptions);
|
|
182
313
|
allFindings.push(...await scanner(target));
|
|
183
314
|
} catch (e) { process.stderr.write(`Error: ${e.message}\n`); }
|
|
184
315
|
continue;
|
|
@@ -244,11 +375,14 @@ async function detectCli() {
|
|
|
244
375
|
|
|
245
376
|
for (const file of files) {
|
|
246
377
|
const ext = path.extname(file).toLowerCase();
|
|
378
|
+
// Each file resolves its own project design system (cached by root),
|
|
379
|
+
// so a scan spanning sibling projects applies the right rules per file.
|
|
380
|
+
const fileOptions = scanOptionsFor(file);
|
|
247
381
|
let fileFindings;
|
|
248
382
|
if (HTML_EXTENSIONS.has(ext)) {
|
|
249
|
-
fileFindings = await detectHtml(file,
|
|
383
|
+
fileFindings = await detectHtml(file, fileOptions);
|
|
250
384
|
} else {
|
|
251
|
-
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file,
|
|
385
|
+
fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, fileOptions);
|
|
252
386
|
}
|
|
253
387
|
// Annotate findings with import context
|
|
254
388
|
const importers = importedByMap.get(file);
|
|
@@ -263,10 +397,11 @@ async function detectCli() {
|
|
|
263
397
|
} else if (stat.isFile()) {
|
|
264
398
|
if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;
|
|
265
399
|
const ext = path.extname(resolved).toLowerCase();
|
|
400
|
+
const fileOptions = scanOptionsFor(resolved);
|
|
266
401
|
if (HTML_EXTENSIONS.has(ext)) {
|
|
267
|
-
allFindings.push(...await detectHtml(resolved,
|
|
402
|
+
allFindings.push(...await detectHtml(resolved, fileOptions));
|
|
268
403
|
} else {
|
|
269
|
-
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved,
|
|
404
|
+
allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, fileOptions));
|
|
270
405
|
}
|
|
271
406
|
}
|
|
272
407
|
}
|
|
@@ -276,12 +411,25 @@ async function detectCli() {
|
|
|
276
411
|
}
|
|
277
412
|
|
|
278
413
|
allFindings = filterDetectionFindings(allFindings, detectionConfig);
|
|
414
|
+
allFindings = filterByScopes(allFindings, scopes);
|
|
415
|
+
// --no-advisory drops advisory findings before any output or exit-code math.
|
|
416
|
+
if (noAdvisory) allFindings = allFindings.filter((f) => !isAdvisory(f));
|
|
417
|
+
|
|
418
|
+
// The exit code and failure count reflect non-advisory findings only. An
|
|
419
|
+
// advisory-only scan still prints its notes but exits 0 (a clean pass), so
|
|
420
|
+
// advisory rules never break CI or block automation.
|
|
421
|
+
const { primary, advisory } = partitionAdvisory(allFindings);
|
|
279
422
|
|
|
280
423
|
if (allFindings.length > 0) {
|
|
281
424
|
if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');
|
|
282
|
-
else if (quietMode)
|
|
425
|
+
else if (quietMode) {
|
|
426
|
+
process.stderr.write(formatFindingSummary(primary.length) + '\n');
|
|
427
|
+
if (advisory.length > 0) {
|
|
428
|
+
process.stderr.write(dim(`${advisory.length} advisory note${advisory.length === 1 ? '' : 's'} (not counted).`) + '\n');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
283
431
|
else process.stderr.write(formatFindings(allFindings, false) + '\n');
|
|
284
|
-
process.exit(2);
|
|
432
|
+
process.exit(primary.length > 0 ? 2 : 0);
|
|
285
433
|
}
|
|
286
434
|
if (jsonMode) process.stdout.write('[]\n');
|
|
287
435
|
process.exit(0);
|