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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* API image generation fallback: renders a mock or world board with the
|
|
4
|
+
* user's own OpenAI key when the harness has no native image generation.
|
|
5
|
+
*
|
|
6
|
+
* context.mjs reports availability (it checks OPENAI_API_KEY); harness-native
|
|
7
|
+
* generation always wins when present. This uses gpt-image-2 and spends the
|
|
8
|
+
* user's API credit (roughly $0.05-0.25 per image at default quality), so the
|
|
9
|
+
* skill states that before the first call in a session.
|
|
10
|
+
*
|
|
11
|
+
* node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium]
|
|
12
|
+
* node generate-image.mjs --prompt-file prompt.txt --out mock.png
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import zlib from 'node:zlib';
|
|
16
|
+
|
|
17
|
+
function arg(name, fallback = null) {
|
|
18
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
19
|
+
if (i === -1) return fallback;
|
|
20
|
+
const v = process.argv[i + 1];
|
|
21
|
+
return v && !v.startsWith('--') ? v : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Fake mode (IMPECCABLE_IMAGE_GEN_FAKE=1)
|
|
26
|
+
//
|
|
27
|
+
// Deterministic offline stand-in for the OpenAI call: same prompt -> identical
|
|
28
|
+
// bytes, no network, no key, cost line reads $0.00. Used by the new-work smoke
|
|
29
|
+
// suite so the concept/serve-question/image chain can run without spend. The
|
|
30
|
+
// output renders the prompt over a 2-3 color palette hashed from the prompt,
|
|
31
|
+
// plus a "SYNTHETIC COMP" corner label. SVG carries the readable text; the
|
|
32
|
+
// raster (.png/.webp/.jpg) fallback carries palette stripes and stows the
|
|
33
|
+
// prompt + marker in a PNG tEXt chunk so downstream stays a valid image.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
// FNV-1a 32-bit: tiny, dependency-free, stable across runs and platforms.
|
|
37
|
+
function hash32(str) {
|
|
38
|
+
let h = 0x811c9dc5;
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
h ^= str.charCodeAt(i);
|
|
41
|
+
h = Math.imul(h, 0x01000193);
|
|
42
|
+
}
|
|
43
|
+
return h >>> 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hslToRgb(hDeg, s, l) {
|
|
47
|
+
const h = ((hDeg % 360) + 360) % 360 / 360;
|
|
48
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
49
|
+
const p = 2 * l - q;
|
|
50
|
+
const hue = (t) => {
|
|
51
|
+
let tt = t;
|
|
52
|
+
if (tt < 0) tt += 1;
|
|
53
|
+
if (tt > 1) tt -= 1;
|
|
54
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
55
|
+
if (tt < 1 / 2) return q;
|
|
56
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
57
|
+
return p;
|
|
58
|
+
};
|
|
59
|
+
return [hue(h + 1 / 3), hue(h), hue(h - 1 / 3)].map((c) => Math.round(c * 255));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const toHex = ([r, g, b]) =>
|
|
63
|
+
'#' + [r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('');
|
|
64
|
+
|
|
65
|
+
// Two or three deterministic swatches derived from the prompt hash. The band
|
|
66
|
+
// count itself is prompt-derived, so different prompts differ in palette.
|
|
67
|
+
function palette(prompt) {
|
|
68
|
+
const h = hash32(prompt);
|
|
69
|
+
const base = h % 360;
|
|
70
|
+
const bands = 2 + (h >>> 9) % 2; // 2 or 3
|
|
71
|
+
const spread = 40 + (h >>> 3) % 120;
|
|
72
|
+
const out = [];
|
|
73
|
+
for (let i = 0; i < bands; i++) {
|
|
74
|
+
const hue = base + i * spread;
|
|
75
|
+
const light = 0.32 + ((h >>> (i * 5)) % 40) / 100; // 0.32 - 0.71
|
|
76
|
+
out.push(hslToRgb(hue, 0.55, light));
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function svgFake(prompt, [w, h]) {
|
|
82
|
+
const colors = palette(prompt).map(toHex);
|
|
83
|
+
const stops = colors
|
|
84
|
+
.map((c, i) => `<stop offset="${Math.round((i / (colors.length - 1)) * 100)}%" stop-color="${c}"/>`)
|
|
85
|
+
.join('');
|
|
86
|
+
// Greedy word wrap tuned to the canvas width so the prompt stays legible.
|
|
87
|
+
const perLine = Math.max(12, Math.floor(w / 26));
|
|
88
|
+
const words = String(prompt).replace(/\s+/g, ' ').trim().split(' ');
|
|
89
|
+
const lines = [];
|
|
90
|
+
let cur = '';
|
|
91
|
+
for (const word of words) {
|
|
92
|
+
if ((cur + ' ' + word).trim().length > perLine) {
|
|
93
|
+
if (cur) lines.push(cur);
|
|
94
|
+
cur = word;
|
|
95
|
+
} else {
|
|
96
|
+
cur = (cur + ' ' + word).trim();
|
|
97
|
+
}
|
|
98
|
+
if (lines.length >= 10) break;
|
|
99
|
+
}
|
|
100
|
+
if (cur && lines.length < 11) lines.push(cur);
|
|
101
|
+
const escape = (s) => String(s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
102
|
+
const fontSize = Math.round(w / 24);
|
|
103
|
+
const startY = h / 2 - ((lines.length - 1) * fontSize * 1.3) / 2;
|
|
104
|
+
const text = lines
|
|
105
|
+
.map((line, i) => `<text x="${w / 2}" y="${Math.round(startY + i * fontSize * 1.3)}" font-family="Helvetica, Arial, sans-serif" font-size="${fontSize}" fill="#ffffff" text-anchor="middle" dominant-baseline="middle">${escape(line)}</text>`)
|
|
106
|
+
.join('');
|
|
107
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
108
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
109
|
+
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">${stops}</linearGradient></defs>
|
|
110
|
+
<rect width="${w}" height="${h}" fill="url(#g)"/>
|
|
111
|
+
<rect x="0" y="0" width="${w}" height="${h}" fill="#000000" fill-opacity="0.22"/>
|
|
112
|
+
${text}
|
|
113
|
+
<rect x="${w - Math.round(w / 4.2)}" y="${h - Math.round(h / 16)}" width="${Math.round(w / 4.2)}" height="${Math.round(h / 16)}" fill="#000000" fill-opacity="0.55"/>
|
|
114
|
+
<text x="${w - Math.round(w / 8.4)}" y="${h - Math.round(h / 32)}" font-family="Helvetica, Arial, sans-serif" font-size="${Math.round(w / 60)}" letter-spacing="2" fill="#ffffff" text-anchor="middle" dominant-baseline="middle">SYNTHETIC COMP</text>
|
|
115
|
+
</svg>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Minimal valid PNG: palette stripes plus a tEXt chunk carrying the marker and
|
|
120
|
+
// prompt, so a .png/.webp fake stays a decodable image and still contains the
|
|
121
|
+
// "SYNTHETIC" bytes downstream tools look for.
|
|
122
|
+
function crc32(buf) {
|
|
123
|
+
let c = 0xffffffff;
|
|
124
|
+
for (let i = 0; i < buf.length; i++) {
|
|
125
|
+
c ^= buf[i];
|
|
126
|
+
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
127
|
+
}
|
|
128
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pngChunk(type, data) {
|
|
132
|
+
const typeBuf = Buffer.from(type, 'latin1');
|
|
133
|
+
const body = Buffer.concat([typeBuf, data]);
|
|
134
|
+
const len = Buffer.alloc(4);
|
|
135
|
+
len.writeUInt32BE(data.length, 0);
|
|
136
|
+
const crc = Buffer.alloc(4);
|
|
137
|
+
crc.writeUInt32BE(crc32(body), 0);
|
|
138
|
+
return Buffer.concat([len, body, crc]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pngFake(prompt, [w, h]) {
|
|
142
|
+
const colors = palette(prompt); // [[r,g,b], ...]
|
|
143
|
+
const bandH = Math.ceil(h / colors.length);
|
|
144
|
+
// Raw image: each scanline prefixed with a 0 filter byte, RGB pixels.
|
|
145
|
+
const stride = w * 3;
|
|
146
|
+
const raw = Buffer.alloc(h * (stride + 1));
|
|
147
|
+
for (let y = 0; y < h; y++) {
|
|
148
|
+
const rowStart = y * (stride + 1);
|
|
149
|
+
raw[rowStart] = 0;
|
|
150
|
+
const [r, g, b] = colors[Math.min(colors.length - 1, Math.floor(y / bandH))];
|
|
151
|
+
for (let x = 0; x < w; x++) {
|
|
152
|
+
const p = rowStart + 1 + x * 3;
|
|
153
|
+
raw[p] = r;
|
|
154
|
+
raw[p + 1] = g;
|
|
155
|
+
raw[p + 2] = b;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const ihdr = Buffer.alloc(13);
|
|
159
|
+
ihdr.writeUInt32BE(w, 0);
|
|
160
|
+
ihdr.writeUInt32BE(h, 4);
|
|
161
|
+
ihdr[8] = 8; // bit depth
|
|
162
|
+
ihdr[9] = 2; // color type: truecolor RGB
|
|
163
|
+
const idat = zlib.deflateSync(raw, { level: 9 });
|
|
164
|
+
const textData = Buffer.concat([
|
|
165
|
+
Buffer.from('Comment', 'latin1'),
|
|
166
|
+
Buffer.from([0]),
|
|
167
|
+
Buffer.from(`SYNTHETIC COMP: ${String(prompt).replace(/\s+/g, ' ').trim()}`, 'latin1'),
|
|
168
|
+
]);
|
|
169
|
+
return Buffer.concat([
|
|
170
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
171
|
+
pngChunk('IHDR', ihdr),
|
|
172
|
+
pngChunk('tEXt', textData),
|
|
173
|
+
pngChunk('IDAT', idat),
|
|
174
|
+
pngChunk('IEND', Buffer.alloc(0)),
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseSize(sizeStr) {
|
|
179
|
+
const m = String(sizeStr).match(/^(\d+)x(\d+)$/);
|
|
180
|
+
if (!m) return [1536, 1024];
|
|
181
|
+
return [Number(m[1]), Number(m[2])];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (process.env.IMPECCABLE_IMAGE_GEN_FAKE) {
|
|
185
|
+
const fakePromptFile = arg('prompt-file');
|
|
186
|
+
const fakePrompt = fakePromptFile ? fs.readFileSync(fakePromptFile, 'utf8') : arg('prompt');
|
|
187
|
+
const fakeOut = arg('out');
|
|
188
|
+
if (!fakePrompt || !fakeOut) {
|
|
189
|
+
console.error('generate-image: --prompt (or --prompt-file) and --out are required.');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const dims = parseSize(arg('size', '1536x1024'));
|
|
193
|
+
const bytes = fakeOut.endsWith('.svg')
|
|
194
|
+
? Buffer.from(svgFake(fakePrompt, dims), 'utf8')
|
|
195
|
+
: pngFake(fakePrompt, dims);
|
|
196
|
+
fs.writeFileSync(fakeOut, bytes);
|
|
197
|
+
console.log(`IMAGE: ${fakeOut} (${dims[0]}x${dims[1]}, fake synthetic comp, $0.00, no API call)`);
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const key = process.env.OPENAI_API_KEY;
|
|
202
|
+
if (!key) {
|
|
203
|
+
console.error('generate-image: OPENAI_API_KEY is not set; use the harness-native image tool instead.');
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const promptFile = arg('prompt-file');
|
|
207
|
+
const prompt = promptFile ? fs.readFileSync(promptFile, 'utf8') : arg('prompt');
|
|
208
|
+
const out = arg('out');
|
|
209
|
+
if (!prompt || !out) {
|
|
210
|
+
console.error('generate-image: --prompt (or --prompt-file) and --out are required.');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const size = arg('size', '1536x1024');
|
|
214
|
+
const quality = arg('quality', 'medium');
|
|
215
|
+
|
|
216
|
+
const response = await fetch('https://api.openai.com/v1/images/generations', {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }),
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const json = await response.json();
|
|
226
|
+
const b64 = json?.data?.[0]?.b64_json;
|
|
227
|
+
if (!b64) {
|
|
228
|
+
console.error('generate-image: no image in response');
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
fs.writeFileSync(out, Buffer.from(b64, 'base64'));
|
|
232
|
+
// The prompt travels with the asset: embedded in the file itself (EXIF-class
|
|
233
|
+
// metadata via embed-prompt.mjs) so intent survives copies across harnesses,
|
|
234
|
+
// plus a sidecar for anything that indexes rather than opens the image.
|
|
235
|
+
try {
|
|
236
|
+
const { spawnSync } = await import('node:child_process');
|
|
237
|
+
spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
|
|
238
|
+
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2));
|
|
239
|
+
} catch { /* embedding is best-effort */ }
|
|
240
|
+
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The Impeccable hooks command manages the design hook runtime
|
|
4
4
|
* via the `hook` key and shared detector ignores via the `detector` key in
|
|
5
5
|
* .impeccable/config.json / .impeccable/config.local.json.
|
|
6
6
|
*
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* node hook-admin.mjs ignore-file <glob> # append to ignoreFiles
|
|
14
14
|
* node hook-admin.mjs ignore-value <rule> <value> # append to shared ignoreValues
|
|
15
15
|
* node hook-admin.mjs ignore-value <rule> <value> --local
|
|
16
|
+
* node hook-admin.mjs ignore-value <rule> "*" --file <glob> # rule off in <glob> only
|
|
17
|
+
* node hook-admin.mjs ignore-value <rule> "*" # refused: scope it or use ignore-rule
|
|
16
18
|
* node hook-admin.mjs reset # remove all config + cache
|
|
17
19
|
*
|
|
18
20
|
* Designed to be invoked by the LLM from the reference/hooks.md flow.
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
|
|
22
24
|
import fs from 'node:fs';
|
|
23
25
|
import path from 'node:path';
|
|
26
|
+
import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
|
|
24
27
|
|
|
25
28
|
import {
|
|
26
29
|
getConfigPath,
|
|
@@ -44,6 +47,26 @@ const IMPECCABLE_HOOK_COMMAND_MARKERS = [
|
|
|
44
47
|
];
|
|
45
48
|
const TIMEOUT_SECONDS = 5;
|
|
46
49
|
const STATUS_MESSAGE = 'Checking UI changes';
|
|
50
|
+
// The Stop deep pass scans every UI file touched in the session with the full
|
|
51
|
+
// rule set, so it gets a longer budget than the per-edit pass. Only Claude
|
|
52
|
+
// Code and Codex dispatch a native Stop hook event, so only those manifests
|
|
53
|
+
// carry the entry. Keep these shapes in sync with
|
|
54
|
+
// scripts/lib/transformers/hooks.js in the repo.
|
|
55
|
+
const STOP_TIMEOUT_SECONDS = 30;
|
|
56
|
+
const STOP_STATUS_MESSAGE = 'Design deep pass';
|
|
57
|
+
|
|
58
|
+
function stopManifestEntry(command) {
|
|
59
|
+
return {
|
|
60
|
+
hooks: [
|
|
61
|
+
{
|
|
62
|
+
type: 'command',
|
|
63
|
+
command,
|
|
64
|
+
timeout: STOP_TIMEOUT_SECONDS,
|
|
65
|
+
statusMessage: STOP_STATUS_MESSAGE,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
47
70
|
|
|
48
71
|
const HOOK_MANIFEST_TARGETS = [
|
|
49
72
|
{
|
|
@@ -52,7 +75,7 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
52
75
|
destRel: '.claude/settings.local.json',
|
|
53
76
|
sharedDestRel: '.claude/settings.json',
|
|
54
77
|
manifest: () => ({
|
|
55
|
-
description: 'Impeccable design detector:
|
|
78
|
+
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
|
|
56
79
|
hooks: {
|
|
57
80
|
PostToolUse: [
|
|
58
81
|
{
|
|
@@ -67,6 +90,7 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
67
90
|
],
|
|
68
91
|
},
|
|
69
92
|
],
|
|
93
|
+
Stop: [stopManifestEntry('node "${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"')],
|
|
70
94
|
},
|
|
71
95
|
}),
|
|
72
96
|
},
|
|
@@ -75,7 +99,6 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
75
99
|
skillRel: '.agents/skills/impeccable',
|
|
76
100
|
destRel: '.codex/hooks.json',
|
|
77
101
|
manifest: () => ({
|
|
78
|
-
description: 'Impeccable design detector: runs after Edit/Write/apply_patch on UI files and surfaces findings as system reminders.',
|
|
79
102
|
hooks: {
|
|
80
103
|
PostToolUse: [
|
|
81
104
|
{
|
|
@@ -83,13 +106,14 @@ const HOOK_MANIFEST_TARGETS = [
|
|
|
83
106
|
hooks: [
|
|
84
107
|
{
|
|
85
108
|
type: 'command',
|
|
86
|
-
command: 'node "
|
|
109
|
+
command: 'node ".agents/skills/impeccable/scripts/hook.mjs"',
|
|
87
110
|
timeout: TIMEOUT_SECONDS,
|
|
88
111
|
statusMessage: STATUS_MESSAGE,
|
|
89
112
|
},
|
|
90
113
|
],
|
|
91
114
|
},
|
|
92
115
|
],
|
|
116
|
+
Stop: [stopManifestEntry('node ".agents/skills/impeccable/scripts/hook.mjs"')],
|
|
93
117
|
},
|
|
94
118
|
}),
|
|
95
119
|
},
|
|
@@ -185,7 +209,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) {
|
|
|
185
209
|
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
186
210
|
const existingHook = stripDetectorKeys(hookSection(existing));
|
|
187
211
|
// Merge over the existing hook object so fields the merge helpers don't manage
|
|
188
|
-
// (consent, quiet, auditLog) survive
|
|
212
|
+
// (consent, quiet, auditLog) survive an Impeccable hooks edit.
|
|
189
213
|
const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
|
|
190
214
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
191
215
|
fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
|
|
@@ -265,7 +289,10 @@ function mergeIgnoreValueEntries(existing, incoming) {
|
|
|
265
289
|
}
|
|
266
290
|
|
|
267
291
|
function ignoreValueEntryKey(entry) {
|
|
268
|
-
|
|
292
|
+
// Sorted: a file scope is a set. Comparing stored order made an on-disk scope
|
|
293
|
+
// miss the sorted argv form, so a re-add duplicated the entry and a remove
|
|
294
|
+
// silently failed. Every key that hashes `files` must sort — there are four.
|
|
295
|
+
const files = Array.isArray(entry.files) && entry.files.length > 0 ? [...entry.files].sort().join('\x1f') : '';
|
|
269
296
|
return `${entry.rule}\0${entry.value}\0${files}`;
|
|
270
297
|
}
|
|
271
298
|
|
|
@@ -283,7 +310,14 @@ function statusReport(cwd) {
|
|
|
283
310
|
if (info.exists) return relPath;
|
|
284
311
|
return `${relPath} (${absent})`;
|
|
285
312
|
};
|
|
286
|
-
|
|
313
|
+
// Show the file scope. Dropping it rendered a file-scoped entry as
|
|
314
|
+
// `design-system-font-size=*`, which reads as the project-wide wildcard this
|
|
315
|
+
// command refuses — the opposite of what is on disk. Matches the
|
|
316
|
+
// `rule=value [files]` shape `impeccable ignores list` already prints.
|
|
317
|
+
const ignoreValues = cfg.ignoreValues.map((entry) => {
|
|
318
|
+
const scope = Array.isArray(entry.files) && entry.files.length ? ` [${entry.files.join(', ')}]` : '';
|
|
319
|
+
return `${entry.rule}=${entry.value}${scope}`;
|
|
320
|
+
});
|
|
287
321
|
|
|
288
322
|
const lines = [
|
|
289
323
|
`Impeccable design hook`,
|
|
@@ -514,9 +548,9 @@ function parseIgnoreRuleArgs(args) {
|
|
|
514
548
|
function addIgnoreRule(cwd, args) {
|
|
515
549
|
const parsed = parseIgnoreRuleArgs(args);
|
|
516
550
|
const rule = parsed.rule;
|
|
517
|
-
if (!rule) throw new Error(
|
|
551
|
+
if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`);
|
|
518
552
|
if (rule === 'overused-font' && !parsed.allValues) {
|
|
519
|
-
throw new Error(
|
|
553
|
+
throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font <font> for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`);
|
|
520
554
|
}
|
|
521
555
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
522
556
|
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
|
|
@@ -525,21 +559,36 @@ function addIgnoreRule(cwd, args) {
|
|
|
525
559
|
}
|
|
526
560
|
|
|
527
561
|
function addIgnoreFile(cwd, glob) {
|
|
528
|
-
if (!glob) throw new Error(
|
|
562
|
+
if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`);
|
|
529
563
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
530
564
|
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
|
|
531
565
|
writeDetectorConfig(cwd, config);
|
|
532
566
|
return `Added "${glob}" to detector.ignoreFiles. Current: ${config.ignoreFiles.join(', ')}`;
|
|
533
567
|
}
|
|
534
568
|
|
|
569
|
+
// An empty glob used to be dropped by filter(Boolean), so `--file=` reported
|
|
570
|
+
// success and wrote an entry with no files: the user asked to scope a rule to one
|
|
571
|
+
// file and silently got the project-wide suppression instead. Refuse it.
|
|
572
|
+
function requireGlob(raw, flag) {
|
|
573
|
+
const glob = String(raw ?? '').trim();
|
|
574
|
+
if (!glob) throw new Error(`${flag} requires a non-empty glob`);
|
|
575
|
+
// A following flag is not a glob. `--file --reason "why"` consumed `--reason`
|
|
576
|
+
// as the scope and left the reason text to fold into the value, storing
|
|
577
|
+
// value="* why" files=["--reason"] and reporting success. Same silent-no-op
|
|
578
|
+
// class as an unknown flag folding into the value; refuse it the same way.
|
|
579
|
+
if (glob.startsWith('--')) throw new Error(`${flag} requires a glob, got the flag ${glob}`);
|
|
580
|
+
return glob;
|
|
581
|
+
}
|
|
582
|
+
|
|
535
583
|
function parseIgnoreValueArgs(args) {
|
|
536
584
|
const positionals = [];
|
|
585
|
+
const files = [];
|
|
537
586
|
let shared = false;
|
|
538
587
|
let local = false;
|
|
539
588
|
let reason = '';
|
|
540
589
|
|
|
541
590
|
for (let i = 0; i < args.length; i++) {
|
|
542
|
-
const arg = args[i];
|
|
591
|
+
const arg = String(args[i] || '');
|
|
543
592
|
if (arg === '--shared') {
|
|
544
593
|
shared = true;
|
|
545
594
|
} else if (arg === '--local') {
|
|
@@ -550,8 +599,20 @@ function parseIgnoreValueArgs(args) {
|
|
|
550
599
|
chunks.push(args[++i]);
|
|
551
600
|
}
|
|
552
601
|
reason = chunks.join(' ').trim();
|
|
553
|
-
} else if (
|
|
554
|
-
reason =
|
|
602
|
+
} else if (arg.startsWith('--reason=')) {
|
|
603
|
+
reason = arg.slice('--reason='.length).trim();
|
|
604
|
+
} else if (arg === '--file' || arg === '--files') {
|
|
605
|
+
if (i + 1 >= args.length) throw new Error(`${arg} requires a glob`);
|
|
606
|
+
files.push(requireGlob(args[++i], arg));
|
|
607
|
+
} else if (arg.startsWith('--file=')) {
|
|
608
|
+
files.push(requireGlob(arg.slice('--file='.length), '--file'));
|
|
609
|
+
} else if (arg.startsWith('--files=')) {
|
|
610
|
+
files.push(requireGlob(arg.slice('--files='.length), '--files'));
|
|
611
|
+
} else if (arg.startsWith('--')) {
|
|
612
|
+
// Otherwise a typo folds into the value: `ignore-value overused-font Inter
|
|
613
|
+
// --shard` stored the value "inter --shard", which matches no finding, and
|
|
614
|
+
// reported success. Matches `impeccable ignores add-value`.
|
|
615
|
+
throw new Error(`Unknown ignore-value flag: ${arg}`);
|
|
555
616
|
} else {
|
|
556
617
|
positionals.push(arg);
|
|
557
618
|
}
|
|
@@ -561,6 +622,9 @@ function parseIgnoreValueArgs(args) {
|
|
|
561
622
|
return {
|
|
562
623
|
rule: String(rule || '').trim().toLowerCase(),
|
|
563
624
|
value: normalizeIgnoreValue(valueParts.join(' ')),
|
|
625
|
+
// Sorted: the dedup key compares the files array, so an unsorted scope made
|
|
626
|
+
// `--file b.css --file a.css` a different entry from `--file a.css --file b.css`.
|
|
627
|
+
files: Array.from(new Set(files.filter(Boolean))).sort(),
|
|
564
628
|
shared,
|
|
565
629
|
local,
|
|
566
630
|
reason,
|
|
@@ -570,17 +634,31 @@ function parseIgnoreValueArgs(args) {
|
|
|
570
634
|
function addIgnoreValue(cwd, args) {
|
|
571
635
|
const parsed = parseIgnoreValueArgs(args);
|
|
572
636
|
if (!parsed.rule || !parsed.value) {
|
|
573
|
-
throw new Error(
|
|
637
|
+
throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`);
|
|
574
638
|
}
|
|
575
639
|
|
|
576
640
|
if (parsed.shared && parsed.local) {
|
|
577
641
|
throw new Error('Pass only one scope flag: --shared or --local');
|
|
578
642
|
}
|
|
579
643
|
|
|
644
|
+
// A bare `*` would suppress the rule everywhere, which is ignore-rule's job and
|
|
645
|
+
// not what a finding in one file justifies. detector.ignoreValues honours a
|
|
646
|
+
// `files` scope, so require one — matching `impeccable ignores add-value`.
|
|
647
|
+
if (parsed.value === '*' && parsed.files.length === 0) {
|
|
648
|
+
// `ignore-rule overused-font` refuses on its own without --all-values, so
|
|
649
|
+
// naming the bare form here would hand the user a second error.
|
|
650
|
+
const projectWide = parsed.rule === 'overused-font'
|
|
651
|
+
? `${IMPECCABLE_COMMAND} hooks ignore-rule ${parsed.rule} --all-values`
|
|
652
|
+
: `${IMPECCABLE_COMMAND} hooks ignore-rule ${parsed.rule}`;
|
|
653
|
+
throw new Error(`Wildcard value ignores must be scoped with --file <glob>, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value design-system-font-size "*" --file "src/widget.js". To suppress the rule project-wide use ${projectWide}.`);
|
|
654
|
+
}
|
|
655
|
+
|
|
580
656
|
const local = parsed.local;
|
|
581
657
|
const config = mergeDetectorConfig(readRawDetectorConfig(cwd, { local }));
|
|
582
|
-
|
|
583
|
-
|
|
658
|
+
// Key on the file scope too: the same rule/value legitimately appears more than
|
|
659
|
+
// once with different scopes, and a rule+value-only key overwrote them.
|
|
660
|
+
const key = ignoreValueEntryKey({ rule: parsed.rule, value: parsed.value, files: parsed.files });
|
|
661
|
+
const existing = config.ignoreValues.find((entry) => ignoreValueEntryKey(entry) === key);
|
|
584
662
|
|
|
585
663
|
if (existing) {
|
|
586
664
|
if (parsed.reason) existing.reason = parsed.reason;
|
|
@@ -588,15 +666,17 @@ function addIgnoreValue(cwd, args) {
|
|
|
588
666
|
const entry = {
|
|
589
667
|
rule: parsed.rule,
|
|
590
668
|
value: parsed.value,
|
|
591
|
-
createdAt: new Date().toISOString(),
|
|
592
669
|
};
|
|
670
|
+
if (parsed.files.length) entry.files = parsed.files;
|
|
671
|
+
entry.createdAt = new Date().toISOString();
|
|
593
672
|
if (parsed.reason) entry.reason = parsed.reason;
|
|
594
673
|
config.ignoreValues.push(entry);
|
|
595
674
|
}
|
|
596
675
|
|
|
597
676
|
const target = writeDetectorConfig(cwd, config, { local });
|
|
598
677
|
const scope = local ? 'local detector.ignoreValues' : 'shared detector.ignoreValues';
|
|
599
|
-
|
|
678
|
+
const scopeSuffix = parsed.files.length ? ` scoped to ${parsed.files.join(', ')}` : '';
|
|
679
|
+
return `Added ${parsed.rule}=${parsed.value}${scopeSuffix} to ${scope} (${path.relative(cwd, target) || target}).`;
|
|
600
680
|
}
|
|
601
681
|
|
|
602
682
|
function reset(cwd) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
|
|
16
17
|
import {
|
|
@@ -21,13 +22,17 @@ import {
|
|
|
21
22
|
appendDesignSystemNote,
|
|
22
23
|
designSystemOptions,
|
|
23
24
|
filterFindings,
|
|
25
|
+
isNativePlatform,
|
|
24
26
|
loadDetector,
|
|
27
|
+
matchConfiguredExtension,
|
|
25
28
|
matchesAnyGlob,
|
|
26
29
|
persistCache,
|
|
27
30
|
readCache,
|
|
28
31
|
readConfig,
|
|
29
32
|
renderTemplate,
|
|
33
|
+
resolveCacheCwd,
|
|
30
34
|
resolveProjectCwd,
|
|
35
|
+
resolveProjectPlatform,
|
|
31
36
|
truthy,
|
|
32
37
|
writeAuditLog,
|
|
33
38
|
} from './hook-lib.mjs';
|
|
@@ -332,6 +337,22 @@ function isInsideProject(filePath, cwd) {
|
|
|
332
337
|
}
|
|
333
338
|
}
|
|
334
339
|
|
|
340
|
+
// The static HTML engine reads its input from disk, but preToolUse only has
|
|
341
|
+
// the proposed content. Stage it in a temp file so html-engine targets get the
|
|
342
|
+
// same DOM-structural rules pre-write that runHook applies post-edit.
|
|
343
|
+
async function detectProposedHtml(detector, content, filePath, scanOptions) {
|
|
344
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pre-'));
|
|
345
|
+
const tmpFile = path.join(dir, path.basename(filePath));
|
|
346
|
+
try {
|
|
347
|
+
fs.writeFileSync(tmpFile, content);
|
|
348
|
+
const findings = await detector.detectHtml(tmpFile, scanOptions);
|
|
349
|
+
// Findings carry the temp path; remap so file-scoped ignores still match.
|
|
350
|
+
return (findings || []).map((f) => (f && typeof f === 'object' ? { ...f, file: filePath } : f));
|
|
351
|
+
} finally {
|
|
352
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
335
356
|
function cursorBlockMessage(findings, filePath, config, cwd) {
|
|
336
357
|
const rendered = renderTemplate(findings, filePath, config, { cwd });
|
|
337
358
|
const blocked = rendered.replace(
|
|
@@ -379,9 +400,12 @@ async function main() {
|
|
|
379
400
|
return allow({ skipped: 'stdin-empty' });
|
|
380
401
|
}
|
|
381
402
|
|
|
382
|
-
const
|
|
403
|
+
const sessionCwd = resolveProjectCwd(event);
|
|
383
404
|
const started = Date.now();
|
|
384
|
-
const filePath = proposedFilePath(event,
|
|
405
|
+
const filePath = proposedFilePath(event, sessionCwd);
|
|
406
|
+
// Re-key config/cache to the edited file's project root when the session
|
|
407
|
+
// was launched from a non-project umbrella directory (issue #305).
|
|
408
|
+
const cwd = resolveCacheCwd(filePath, sessionCwd);
|
|
385
409
|
const audit = {
|
|
386
410
|
harness: 'cursor',
|
|
387
411
|
cwd,
|
|
@@ -394,9 +418,13 @@ async function main() {
|
|
|
394
418
|
if (SENSITIVE_PATH.test(filePath)) return allow({ ...audit, skipped: 'sensitive', durationMs: Date.now() - started });
|
|
395
419
|
if (GENERATED_PATH.test(filePath)) return allow({ ...audit, skipped: 'generated', durationMs: Date.now() - started });
|
|
396
420
|
|
|
421
|
+
// Config is read before the extension gate so `detector.extensions` entries
|
|
422
|
+
// (e.g. `.blade.php` template files, issue #316) can widen it.
|
|
423
|
+
const config = readConfig(cwd);
|
|
397
424
|
const ext = path.extname(filePath).toLowerCase();
|
|
398
|
-
|
|
399
|
-
|
|
425
|
+
const configuredExt = matchConfiguredExtension(filePath, config.extensions);
|
|
426
|
+
audit.ext = configuredExt ? configuredExt.ext : ext;
|
|
427
|
+
if (!ALLOWED_EXTS.has(ext) && !configuredExt) return allow({ ...audit, skipped: 'extension', durationMs: Date.now() - started });
|
|
400
428
|
|
|
401
429
|
const contentResult = proposedContent(event, cwd, filePath);
|
|
402
430
|
if (contentResult && typeof contentResult === 'object' && contentResult.skipped) {
|
|
@@ -405,9 +433,14 @@ async function main() {
|
|
|
405
433
|
const content = typeof contentResult === 'string' ? contentResult : '';
|
|
406
434
|
if (!content) return allow({ ...audit, skipped: 'no-proposed-content', durationMs: Date.now() - started });
|
|
407
435
|
|
|
408
|
-
const config = readConfig(cwd);
|
|
409
436
|
if (config.enabled === false) return allow({ ...audit, skipped: 'config-disabled', durationMs: Date.now() - started });
|
|
410
437
|
|
|
438
|
+
// Web rule engine, native project: stand aside (see resolveProjectPlatform).
|
|
439
|
+
const platform = resolveProjectPlatform(cwd);
|
|
440
|
+
if (isNativePlatform(platform)) {
|
|
441
|
+
return allow({ ...audit, skipped: 'native-platform', platform, durationMs: Date.now() - started });
|
|
442
|
+
}
|
|
443
|
+
|
|
411
444
|
const rel = relativePath(filePath, cwd);
|
|
412
445
|
if (matchesAnyGlob(rel, config.ignoreFiles) || matchesAnyGlob(filePath, config.ignoreFiles)) {
|
|
413
446
|
return allow({ ...audit, skipped: 'config-ignore-file', durationMs: Date.now() - started });
|
|
@@ -419,9 +452,16 @@ async function main() {
|
|
|
419
452
|
}
|
|
420
453
|
const scanOptions = designSystemOptions(config, detector, cwd);
|
|
421
454
|
|
|
455
|
+
// Mirror runHook's engine routing so template issues the HTML engine catches
|
|
456
|
+
// post-edit cannot slip past the pre-write gate.
|
|
457
|
+
const useHtmlEngine = configuredExt
|
|
458
|
+
? configuredExt.engine === 'html'
|
|
459
|
+
: (ext === '.html' || ext === '.htm');
|
|
422
460
|
let findings = [];
|
|
423
461
|
try {
|
|
424
|
-
findings =
|
|
462
|
+
findings = useHtmlEngine && typeof detector.detectHtml === 'function'
|
|
463
|
+
? await detectProposedHtml(detector, content, filePath, scanOptions)
|
|
464
|
+
: await detector.detectText(content, filePath, scanOptions);
|
|
425
465
|
} catch {
|
|
426
466
|
return allow({ ...audit, error: 'detector-threw', durationMs: Date.now() - started });
|
|
427
467
|
}
|