@bastani/atomic 0.8.20 → 0.8.21
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/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
- package/dist/builtin/subagents/agents/debugger.md +4 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
- package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +67 -3
- package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
- package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
- package/dist/builtin/workflows/src/shared/store.ts +37 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
- package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +2 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -29
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/docs/quickstart.md +1 -2
- package/package.json +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ─── Section 2: Color Utilities ─────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
function isNeutralColor(color) {
|
|
4
|
+
if (!color || color === 'transparent') return true;
|
|
5
|
+
|
|
6
|
+
// rgb/rgba — use channel spread. Threshold 30 ≈ 11.7% of the 0–255 range.
|
|
7
|
+
const rgb = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
8
|
+
if (rgb) {
|
|
9
|
+
return (Math.max(+rgb[1], +rgb[2], +rgb[3]) - Math.min(+rgb[1], +rgb[2], +rgb[3])) < 30;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// oklch()/lch() — chroma is the second numeric component.
|
|
13
|
+
// oklch chroma is ~0–0.4 in sRGB gamut; >= 0.02 reads as tinted, not gray.
|
|
14
|
+
// lch chroma is ~0–150; >= 3 reads as tinted. jsdom emits both formats
|
|
15
|
+
// literally (it does NOT convert them to rgb).
|
|
16
|
+
const oklch = color.match(/oklch\(\s*[\d.]+%?\s*([\d.-]+)/i);
|
|
17
|
+
if (oklch) return parseFloat(oklch[1]) < 0.02;
|
|
18
|
+
const lch = color.match(/lch\(\s*[\d.]+%?\s*([\d.-]+)/i);
|
|
19
|
+
if (lch) return parseFloat(lch[1]) < 3;
|
|
20
|
+
|
|
21
|
+
// oklab()/lab() — a and b are signed axes; chroma = sqrt(a² + b²).
|
|
22
|
+
// oklab a/b are ~-0.4..0.4, threshold 0.02. lab a/b are ~-128..127, threshold 3.
|
|
23
|
+
const oklab = color.match(/oklab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i);
|
|
24
|
+
if (oklab) {
|
|
25
|
+
const a = parseFloat(oklab[1]), b = parseFloat(oklab[2]);
|
|
26
|
+
return Math.hypot(a, b) < 0.02;
|
|
27
|
+
}
|
|
28
|
+
const lab = color.match(/lab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i);
|
|
29
|
+
if (lab) {
|
|
30
|
+
const a = parseFloat(lab[1]), b = parseFloat(lab[2]);
|
|
31
|
+
return Math.hypot(a, b) < 3;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// hsl/hsla — saturation is the second numeric component (percent).
|
|
35
|
+
// Modern jsdom usually converts hsl() to rgb, but handle it directly for
|
|
36
|
+
// safety across versions and for any engine that preserves the format.
|
|
37
|
+
const hsl = color.match(/hsla?\(\s*[\d.-]+\s*,?\s*([\d.]+)%/i);
|
|
38
|
+
if (hsl) return parseFloat(hsl[1]) < 10;
|
|
39
|
+
|
|
40
|
+
// hwb(hue whiteness% blackness%) — a pixel is fully gray when
|
|
41
|
+
// whiteness + blackness >= 100; chroma-like saturation = 1 - (w+b)/100.
|
|
42
|
+
const hwb = color.match(/hwb\(\s*[\d.-]+\s+([\d.]+)%\s+([\d.]+)%/i);
|
|
43
|
+
if (hwb) {
|
|
44
|
+
const w = parseFloat(hwb[1]), b = parseFloat(hwb[2]);
|
|
45
|
+
return (1 - Math.min(100, w + b) / 100) < 0.1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Unknown / unrecognized format — err on the side of DETECTING rather
|
|
49
|
+
// than silently skipping. This is the opposite of the previous default,
|
|
50
|
+
// which was the root cause of the oklch bug.
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseRgb(color) {
|
|
55
|
+
if (!color || color === 'transparent') return null;
|
|
56
|
+
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function relativeLuminance({ r, g, b }) {
|
|
62
|
+
const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(c =>
|
|
63
|
+
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
64
|
+
);
|
|
65
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function contrastRatio(c1, c2) {
|
|
69
|
+
const l1 = relativeLuminance(c1);
|
|
70
|
+
const l2 = relativeLuminance(c2);
|
|
71
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseGradientColors(bgImage) {
|
|
75
|
+
if (!bgImage || !bgImage.includes('gradient')) return [];
|
|
76
|
+
const colors = [];
|
|
77
|
+
for (const m of bgImage.matchAll(/rgba?\([^)]+\)/g)) {
|
|
78
|
+
const c = parseRgb(m[0]);
|
|
79
|
+
if (c) colors.push(c);
|
|
80
|
+
}
|
|
81
|
+
for (const m of bgImage.matchAll(/#([0-9a-f]{6}|[0-9a-f]{3})\b/gi)) {
|
|
82
|
+
const h = m[1];
|
|
83
|
+
if (h.length === 6) {
|
|
84
|
+
colors.push({ r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16), a: 1 });
|
|
85
|
+
} else {
|
|
86
|
+
colors.push({ r: parseInt(h[0]+h[0],16), g: parseInt(h[1]+h[1],16), b: parseInt(h[2]+h[2],16), a: 1 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return colors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasChroma(c, threshold = 30) {
|
|
93
|
+
if (!c) return false;
|
|
94
|
+
return (Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b)) >= threshold;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getHue(c) {
|
|
98
|
+
if (!c) return 0;
|
|
99
|
+
const r = c.r / 255, g = c.g / 255, b = c.b / 255;
|
|
100
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
101
|
+
if (max === min) return 0;
|
|
102
|
+
const d = max - min;
|
|
103
|
+
let h;
|
|
104
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
105
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
106
|
+
else h = ((r - g) / d + 4) / 6;
|
|
107
|
+
return Math.round(h * 360);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function colorToHex(c) {
|
|
111
|
+
if (!c) return '?';
|
|
112
|
+
return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
isNeutralColor,
|
|
117
|
+
parseRgb,
|
|
118
|
+
relativeLuminance,
|
|
119
|
+
contrastRatio,
|
|
120
|
+
parseGradientColors,
|
|
121
|
+
hasChroma,
|
|
122
|
+
getHue,
|
|
123
|
+
colorToHex,
|
|
124
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ─── Section 1: Constants ───────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
const SAFE_TAGS = new Set([
|
|
4
|
+
'blockquote', 'nav', 'a', 'input', 'textarea', 'select',
|
|
5
|
+
'pre', 'code', 'span', 'th', 'td', 'tr', 'li', 'label',
|
|
6
|
+
'button', 'hr', 'html', 'head', 'body', 'script', 'style',
|
|
7
|
+
'link', 'meta', 'title', 'br', 'img', 'svg', 'path', 'circle',
|
|
8
|
+
'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
// Per-check safe-tags override for the border (side-tab / border-accent)
|
|
12
|
+
// rule. We intentionally re-allow <label> here because card-shaped clickable
|
|
13
|
+
// labels (e.g. .checklist-item wrapping a checkbox + content) are one of the
|
|
14
|
+
// canonical side-tab anti-pattern shapes and must be detected. The rule's
|
|
15
|
+
// other preconditions (non-neutral color, width >= 2px on a single side,
|
|
16
|
+
// radius > 0 or width >= 3, element size >= 20x20 in the browser path)
|
|
17
|
+
// already filter out plain inline form labels so this does not introduce
|
|
18
|
+
// false positives. See modern-color-borders.html for the test matrix.
|
|
19
|
+
const BORDER_SAFE_TAGS = new Set(
|
|
20
|
+
[...SAFE_TAGS].filter(t => t !== 'label')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const OVERUSED_FONTS = new Set([
|
|
24
|
+
// Older monoculture (still ubiquitous):
|
|
25
|
+
'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
|
|
26
|
+
// Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave):
|
|
27
|
+
'fraunces', 'instrument sans', 'instrument serif',
|
|
28
|
+
'geist', 'geist sans', 'geist mono',
|
|
29
|
+
'mona sans',
|
|
30
|
+
'plus jakarta sans', 'space grotesk', 'recoleta',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
|
|
34
|
+
// Keys are font names, values are arrays of hostname suffixes where the font is allowed.
|
|
35
|
+
const GOOGLE_DOMAINS = [
|
|
36
|
+
'google.com', 'youtube.com', 'android.com', 'chromium.org',
|
|
37
|
+
'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
|
|
38
|
+
];
|
|
39
|
+
const VERCEL_DOMAINS = ['vercel.com', 'nextjs.org', 'v0.app'];
|
|
40
|
+
const GITHUB_DOMAINS = ['github.com', 'githubnext.com'];
|
|
41
|
+
const BRAND_FONT_DOMAINS = {
|
|
42
|
+
'roboto': GOOGLE_DOMAINS,
|
|
43
|
+
'google sans': GOOGLE_DOMAINS,
|
|
44
|
+
'product sans': GOOGLE_DOMAINS,
|
|
45
|
+
'geist': VERCEL_DOMAINS,
|
|
46
|
+
'geist sans': VERCEL_DOMAINS,
|
|
47
|
+
'geist mono': VERCEL_DOMAINS,
|
|
48
|
+
'mona sans': GITHUB_DOMAINS,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function isBrandFontOnOwnDomain(font) {
|
|
52
|
+
if (typeof location === 'undefined') return false;
|
|
53
|
+
const allowed = BRAND_FONT_DOMAINS[font];
|
|
54
|
+
if (!allowed) return false;
|
|
55
|
+
const host = location.hostname.toLowerCase();
|
|
56
|
+
return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const GENERIC_FONTS = new Set([
|
|
60
|
+
'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
|
61
|
+
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
|
62
|
+
'-apple-system', 'blinkmacsystemfont', 'segoe ui',
|
|
63
|
+
'inherit', 'initial', 'unset', 'revert',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// WCAG large text thresholds are defined in points: 18pt normal text and
|
|
67
|
+
// 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
|
|
68
|
+
const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
|
|
69
|
+
const WCAG_LARGE_BOLD_TEXT_PX = 14 * (96 / 72);
|
|
70
|
+
|
|
71
|
+
// Serif faces that show up in italic-display heroes. The rule also fires when
|
|
72
|
+
// the primary face is unknown but the stack ends in the generic `serif` token,
|
|
73
|
+
// which catches custom/private faces with a serif fallback.
|
|
74
|
+
const KNOWN_SERIF_FONTS = new Set([
|
|
75
|
+
'fraunces', 'recoleta', 'newsreader', 'playfair display', 'playfair',
|
|
76
|
+
'cormorant', 'cormorant garamond', 'garamond', 'eb garamond',
|
|
77
|
+
'tiempos', 'tiempos headline', 'tiempos text',
|
|
78
|
+
'lora', 'vollkorn', 'spectral',
|
|
79
|
+
'source serif pro', 'source serif 4', 'source serif',
|
|
80
|
+
'ibm plex serif', 'merriweather',
|
|
81
|
+
'libre caslon', 'libre baskerville', 'baskerville',
|
|
82
|
+
'georgia', 'times new roman', 'times',
|
|
83
|
+
'dm serif display', 'dm serif text',
|
|
84
|
+
'instrument serif', 'gt sectra', 'ogg', 'canela',
|
|
85
|
+
'freight display', 'freight text',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
SAFE_TAGS,
|
|
90
|
+
BORDER_SAFE_TAGS,
|
|
91
|
+
OVERUSED_FONTS,
|
|
92
|
+
GOOGLE_DOMAINS,
|
|
93
|
+
VERCEL_DOMAINS,
|
|
94
|
+
GITHUB_DOMAINS,
|
|
95
|
+
BRAND_FONT_DOMAINS,
|
|
96
|
+
isBrandFontOnOwnDomain,
|
|
97
|
+
GENERIC_FONTS,
|
|
98
|
+
WCAG_LARGE_TEXT_PX,
|
|
99
|
+
WCAG_LARGE_BOLD_TEXT_PX,
|
|
100
|
+
KNOWN_SERIF_FONTS,
|
|
101
|
+
};
|
|
@@ -64,7 +64,12 @@ export function getLegacyLiveServerPath(cwd = process.cwd()) {
|
|
|
64
64
|
export function readLiveServerInfo(cwd = process.cwd()) {
|
|
65
65
|
for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) {
|
|
66
66
|
try {
|
|
67
|
-
|
|
67
|
+
const info = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
68
|
+
if (info && typeof info.pid === 'number' && !isLiveServerPidReachable(info.pid)) {
|
|
69
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
return { info, path: filePath };
|
|
68
73
|
} catch {
|
|
69
74
|
/* try next */
|
|
70
75
|
}
|
|
@@ -72,6 +77,17 @@ export function readLiveServerInfo(cwd = process.cwd()) {
|
|
|
72
77
|
return null;
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
export function isLiveServerPidReachable(pid) {
|
|
81
|
+
try {
|
|
82
|
+
process.kill(pid, 0);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// ESRCH means "no such process". EPERM means the process exists but this
|
|
86
|
+
// user cannot signal it, so the live server info is still valid.
|
|
87
|
+
return err?.code !== 'ESRCH';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
export function writeLiveServerInfo(cwd = process.cwd(), info) {
|
|
76
92
|
const filePath = getLiveServerPath(cwd);
|
|
77
93
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* within the first ~300 characters — catches non-git projects.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
@@ -41,7 +41,7 @@ export function isGeneratedFile(filePath, options = {}) {
|
|
|
41
41
|
|
|
42
42
|
function isGitIgnored(absPath, cwd) {
|
|
43
43
|
try {
|
|
44
|
-
|
|
44
|
+
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
|
45
45
|
cwd,
|
|
46
46
|
stdio: 'ignore',
|
|
47
47
|
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { isGeneratedFile } from './is-generated.mjs';
|
|
19
|
+
import { readBuffer as readManualEditsBuffer, writeBuffer as writeManualEditsBuffer } from './live-manual-edits-buffer.mjs';
|
|
19
20
|
|
|
20
21
|
const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
|
|
21
22
|
|
|
@@ -38,19 +39,22 @@ Modes:
|
|
|
38
39
|
Required:
|
|
39
40
|
--id SESSION_ID Session ID of the variant wrapper
|
|
40
41
|
|
|
42
|
+
Options:
|
|
43
|
+
--page-url URL Current browser page URL; scopes staged copy-edit cleanup
|
|
44
|
+
|
|
41
45
|
Output (JSON):
|
|
42
46
|
{ handled, file, carbonize }`);
|
|
43
47
|
process.exit(0);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
const id = argVal(args, '--id');
|
|
47
|
-
const
|
|
51
|
+
const variantNum = argVal(args, '--variant');
|
|
48
52
|
const paramValuesRaw = argVal(args, '--param-values');
|
|
53
|
+
const pageUrl = argVal(args, '--page-url');
|
|
49
54
|
const isDiscard = args.includes('--discard');
|
|
50
55
|
|
|
51
56
|
if (!id) { console.error('Missing --id'); process.exit(1); }
|
|
52
|
-
if (!isDiscard && !
|
|
53
|
-
const variantNum = isDiscard ? null : parseVariantNumber(variantNumRaw);
|
|
57
|
+
if (!isDiscard && !variantNum) { console.error('Need --discard or --variant N'); process.exit(1); }
|
|
54
58
|
|
|
55
59
|
let paramValues = null;
|
|
56
60
|
if (paramValuesRaw) {
|
|
@@ -87,16 +91,88 @@ Output (JSON):
|
|
|
87
91
|
console.log(JSON.stringify({ handled: true, file: relFile, carbonize: false, ...result }));
|
|
88
92
|
} else {
|
|
89
93
|
const result = handleAccept(id, variantNum, lines, targetFile, paramValues);
|
|
94
|
+
const acceptedOriginalText = result.acceptedOriginalText || '';
|
|
95
|
+
delete result.acceptedOriginalText;
|
|
90
96
|
// Single-line attention-grabber when cleanup is required. The full
|
|
91
97
|
// five-step checklist lives in reference/live.md (loaded once per
|
|
92
98
|
// session); repeating it per-event would waste tokens.
|
|
93
99
|
if (result.carbonize) {
|
|
94
100
|
result.todo = 'REQUIRED before next poll: carbonize cleanup in ' + relFile + '. See reference/live.md "Required after accept".';
|
|
95
101
|
}
|
|
102
|
+
// Scrub stash entries whose text appeared inside the just-replaced
|
|
103
|
+
// original wrap block. The accept embodies those manual edits (wrap was
|
|
104
|
+
// buffer-aware), so only those scoped ops are redundant.
|
|
105
|
+
if (result.handled !== false) {
|
|
106
|
+
try {
|
|
107
|
+
scrubManualEditsAgainstOriginalBlock(acceptedOriginalText, process.cwd(), pageUrl);
|
|
108
|
+
} catch {
|
|
109
|
+
// Non-fatal; the buffer stays as-is and the user can discard later.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
96
112
|
console.log(JSON.stringify({ handled: true, file: relFile, ...result }));
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
/**
|
|
117
|
+
* After a variant accept rewrites one wrapper, drop only buffer ops whose
|
|
118
|
+
* text appeared inside that wrapper's original block. The previous file-wide
|
|
119
|
+
* scrub dropped unrelated staged edits from other components/files whenever
|
|
120
|
+
* their originalText wasn't present in the just-accepted file.
|
|
121
|
+
*
|
|
122
|
+
* Match both originalText and newText because live-wrap rewrites the original
|
|
123
|
+
* preview block to reflect pending manual edits before variants are generated.
|
|
124
|
+
*/
|
|
125
|
+
function scrubManualEditsAgainstOriginalBlock(originalBlockText, cwd = process.cwd(), pageUrl = null) {
|
|
126
|
+
const originalBlock = String(originalBlockText || '');
|
|
127
|
+
if (!originalBlock) return;
|
|
128
|
+
if (!pageUrl) return;
|
|
129
|
+
const buffer = readManualEditsBuffer(cwd);
|
|
130
|
+
if (buffer.entries.length === 0) return;
|
|
131
|
+
let mutated = false;
|
|
132
|
+
for (const entry of buffer.entries) {
|
|
133
|
+
if (entry.pageUrl !== pageUrl) continue;
|
|
134
|
+
const before = entry.ops.length;
|
|
135
|
+
entry.ops = entry.ops.filter((op) => {
|
|
136
|
+
return !manualEditOpAppearsInBlock(op, originalBlock);
|
|
137
|
+
});
|
|
138
|
+
if (entry.ops.length !== before) mutated = true;
|
|
139
|
+
}
|
|
140
|
+
buffer.entries = buffer.entries.filter((entry) => entry.ops.length > 0);
|
|
141
|
+
if (mutated) writeManualEditsBuffer(cwd, buffer);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function manualEditOpAppearsInBlock(op, originalBlock) {
|
|
145
|
+
const candidates = [op?.newText, op?.originalText]
|
|
146
|
+
.filter((text) => typeof text === 'string' && text.length > 0);
|
|
147
|
+
return candidates.some((text) => originalBlockHasExactManualText(originalBlock, text));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function originalBlockHasExactManualText(originalBlock, text) {
|
|
151
|
+
const needle = normalizeManualEditText(text);
|
|
152
|
+
if (!needle) return false;
|
|
153
|
+
return manualEditTextSegments(originalBlock).some((segment) => segment === needle);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function manualEditTextSegments(source) {
|
|
157
|
+
return String(source || '')
|
|
158
|
+
.replace(/<[^>]*>/g, '\n')
|
|
159
|
+
.replace(/\{\/\*[\s\S]*?\*\/\}/g, '\n')
|
|
160
|
+
.replace(/<!--[\s\S]*?-->/g, '\n')
|
|
161
|
+
.split(/\n+/)
|
|
162
|
+
.map(normalizeManualEditText)
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeManualEditText(text) {
|
|
167
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compatibility export for older tests/callers. The unsafe file-wide scrub was
|
|
171
|
+
// removed; callers must pass accepted original-block text for scoped cleanup.
|
|
172
|
+
function scrubManualEditsAgainstFile(_targetFile, cwd = process.cwd(), originalBlockText = '', pageUrl = null) {
|
|
173
|
+
return scrubManualEditsAgainstOriginalBlock(originalBlockText, cwd, pageUrl);
|
|
174
|
+
}
|
|
175
|
+
|
|
100
176
|
// ---------------------------------------------------------------------------
|
|
101
177
|
// Discard
|
|
102
178
|
// ---------------------------------------------------------------------------
|
|
@@ -147,6 +223,7 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
|
|
147
223
|
// Extract the chosen variant's inner content
|
|
148
224
|
const variantContent = extractVariant(lines, block, variantNum);
|
|
149
225
|
if (!variantContent) return { handled: false, error: 'Variant ' + variantNum + ' not found' };
|
|
226
|
+
const originalContent = extractOriginal(lines, block);
|
|
150
227
|
|
|
151
228
|
// Extract CSS block if present
|
|
152
229
|
const cssContent = extractCss(lines, block, id);
|
|
@@ -205,7 +282,7 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
|
|
205
282
|
];
|
|
206
283
|
fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
|
|
207
284
|
|
|
208
|
-
return { carbonize: needsCarbonize };
|
|
285
|
+
return { carbonize: needsCarbonize, acceptedOriginalText: originalContent.join('\n') };
|
|
209
286
|
}
|
|
210
287
|
|
|
211
288
|
// ---------------------------------------------------------------------------
|
|
@@ -227,7 +304,7 @@ function findMarkerBlock(id, lines) {
|
|
|
227
304
|
if (lines[i].includes(endPattern)) { end = i; break; }
|
|
228
305
|
}
|
|
229
306
|
|
|
230
|
-
return (start !== -1 && end !== -1) ? { start, end } : null;
|
|
307
|
+
return (start !== -1 && end !== -1) ? { start, end, id } : null;
|
|
231
308
|
}
|
|
232
309
|
|
|
233
310
|
/**
|
|
@@ -254,11 +331,14 @@ function expandReplaceRange(block, lines, isJsx) {
|
|
|
254
331
|
// Walk back for the wrapper `<div data-impeccable-variants="..."` opener.
|
|
255
332
|
// The attr may sit on a continuation line of a multi-line opening tag, so
|
|
256
333
|
// also walk to the line that actually contains `<div`.
|
|
257
|
-
for (let i = start - 1; i >=
|
|
258
|
-
if (
|
|
334
|
+
for (let i = start - 1; i >= 0; i--) {
|
|
335
|
+
if (isVariantEndMarkerLine(lines[i], block.id)) break;
|
|
336
|
+
if (hasVariantWrapperAttr(lines[i], block.id)) {
|
|
259
337
|
let opener = i;
|
|
260
|
-
while (opener > 0 && !/<div\b/.test(lines[opener]))
|
|
261
|
-
|
|
338
|
+
while (opener > 0 && !/<div\b/.test(lines[opener]) && !isVariantEndMarkerLine(lines[opener], block.id)) {
|
|
339
|
+
opener--;
|
|
340
|
+
}
|
|
341
|
+
if (/<div\b/.test(lines[opener])) start = opener;
|
|
262
342
|
break;
|
|
263
343
|
}
|
|
264
344
|
}
|
|
@@ -296,6 +376,19 @@ function expandReplaceRange(block, lines, isJsx) {
|
|
|
296
376
|
return { start, end };
|
|
297
377
|
}
|
|
298
378
|
|
|
379
|
+
function escapeRegExp(value) {
|
|
380
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isVariantEndMarkerLine(line, id) {
|
|
384
|
+
return new RegExp('impeccable-variants-end\\s+' + escapeRegExp(id) + '(?:\\s|--|\\*/|$)').test(line);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function hasVariantWrapperAttr(line, id) {
|
|
388
|
+
const escaped = escapeRegExp(id);
|
|
389
|
+
return new RegExp(`data-impeccable-variants\\s*=\\s*(?:"${escaped}"|'${escaped}'|\\{["']${escaped}["']\\})`).test(line);
|
|
390
|
+
}
|
|
391
|
+
|
|
299
392
|
/**
|
|
300
393
|
* Join wrapper lines into a single string with `<style>` elements removed so
|
|
301
394
|
* marker matching and div-depth tracking aren't confused by:
|
|
@@ -308,92 +401,50 @@ function expandReplaceRange(block, lines, isJsx) {
|
|
|
308
401
|
function stripStyleAndJoin(lines, block) {
|
|
309
402
|
const out = [];
|
|
310
403
|
let inStyle = false;
|
|
311
|
-
|
|
312
404
|
for (let i = block.start; i <= block.end; i++) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const openIdx = lower.indexOf('<style');
|
|
340
|
-
if (openIdx === -1) {
|
|
341
|
-
output += remaining;
|
|
342
|
-
remaining = '';
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
output += remaining.slice(0, openIdx);
|
|
347
|
-
const openEnd = lower.indexOf('>', openIdx);
|
|
348
|
-
if (openEnd === -1) break;
|
|
349
|
-
|
|
350
|
-
const opener = remaining.slice(openIdx, openEnd + 1);
|
|
351
|
-
if (/\/\s*>$/.test(opener)) {
|
|
352
|
-
remaining = remaining.slice(openEnd + 1);
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const afterOpen = remaining.slice(openEnd + 1);
|
|
357
|
-
const afterOpenLower = afterOpen.toLowerCase();
|
|
358
|
-
const sameLineClose = afterOpenLower.indexOf('</style');
|
|
359
|
-
if (sameLineClose === -1) {
|
|
360
|
-
inStyle = true;
|
|
361
|
-
remaining = '';
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const closeEnd = afterOpenLower.indexOf('>', sameLineClose);
|
|
366
|
-
if (closeEnd === -1) {
|
|
367
|
-
inStyle = true;
|
|
368
|
-
remaining = '';
|
|
369
|
-
break;
|
|
405
|
+
let line = lines[i];
|
|
406
|
+
|
|
407
|
+
if (!inStyle) {
|
|
408
|
+
// Strip any complete <style> elements on this line (self-closed or
|
|
409
|
+
// same-line-closed), including their body content.
|
|
410
|
+
line = line
|
|
411
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/g, '')
|
|
412
|
+
.replace(/<style\b[^>]*\/\s*>/g, '');
|
|
413
|
+
|
|
414
|
+
// If a <style> opener remains (multi-line body starts here), strip from
|
|
415
|
+
// the opener to end-of-line and flip into skip mode.
|
|
416
|
+
const openerIdx = line.search(/<style\b/);
|
|
417
|
+
if (openerIdx !== -1) {
|
|
418
|
+
line = line.slice(0, openerIdx);
|
|
419
|
+
inStyle = true;
|
|
420
|
+
}
|
|
421
|
+
out.push(line);
|
|
422
|
+
} else {
|
|
423
|
+
// In multi-line style body; drop everything until we see </style>.
|
|
424
|
+
const closeIdx = line.search(/<\/style\s*>/);
|
|
425
|
+
if (closeIdx !== -1) {
|
|
426
|
+
inStyle = false;
|
|
427
|
+
out.push(line.slice(closeIdx).replace(/<\/style\s*>/, ''));
|
|
428
|
+
}
|
|
429
|
+
// else: skip line entirely
|
|
370
430
|
}
|
|
371
|
-
remaining = afterOpen.slice(closeEnd + 1);
|
|
372
431
|
}
|
|
373
|
-
|
|
374
|
-
return { text: output, stillInStyle: inStyle };
|
|
432
|
+
return out.join('\n');
|
|
375
433
|
}
|
|
376
434
|
|
|
377
435
|
/**
|
|
378
|
-
* Find the inner content of `<TAG ...
|
|
379
|
-
*
|
|
380
|
-
*
|
|
436
|
+
* Find the inner content of `<TAG ...attrMatch...>…</TAG>` inside `text`,
|
|
437
|
+
* handling nested same-tag elements via depth counting. `attrMatch` is a
|
|
438
|
+
* regex source fragment that must appear inside the opener tag.
|
|
439
|
+
* Returns the inner string (may be empty), or null if not found.
|
|
381
440
|
*/
|
|
382
|
-
function extractInnerByAttr(text,
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
if (attrIdx === -1) return null;
|
|
386
|
-
|
|
387
|
-
const openStart = text.lastIndexOf('<', attrIdx);
|
|
388
|
-
const openEnd = text.indexOf('>', attrIdx);
|
|
389
|
-
if (openStart === -1 || openEnd === -1 || openStart > attrIdx || openEnd < attrIdx) return null;
|
|
390
|
-
|
|
391
|
-
const opener = text.slice(openStart, openEnd + 1);
|
|
392
|
-
const openMatch = opener.match(/^<([A-Za-z][A-Za-z0-9]*)\b/);
|
|
441
|
+
function extractInnerByAttr(text, attrMatch) {
|
|
442
|
+
const openerRe = new RegExp('<([A-Za-z][A-Za-z0-9]*)\\b[^>]*' + attrMatch + '[^>]*>');
|
|
443
|
+
const openMatch = text.match(openerRe);
|
|
393
444
|
if (!openMatch) return null;
|
|
394
445
|
|
|
395
446
|
const tagName = openMatch[1];
|
|
396
|
-
const innerStart =
|
|
447
|
+
const innerStart = openMatch.index + openMatch[0].length;
|
|
397
448
|
|
|
398
449
|
// Match any opener or closer of this tag name after innerStart.
|
|
399
450
|
// (Does not match self-closing <TAG … />, which doesn't contribute to depth.)
|
|
@@ -421,7 +472,7 @@ function extractInnerByAttr(text, attrName, attrValue) {
|
|
|
421
472
|
*/
|
|
422
473
|
function extractOriginal(lines, block) {
|
|
423
474
|
const text = stripStyleAndJoin(lines, block);
|
|
424
|
-
const inner = extractInnerByAttr(text, 'data-impeccable-variant
|
|
475
|
+
const inner = extractInnerByAttr(text, 'data-impeccable-variant="original"');
|
|
425
476
|
if (inner === null) return [];
|
|
426
477
|
return inner.split('\n');
|
|
427
478
|
}
|
|
@@ -432,7 +483,7 @@ function extractOriginal(lines, block) {
|
|
|
432
483
|
*/
|
|
433
484
|
function extractVariant(lines, block, variantNum) {
|
|
434
485
|
const text = stripStyleAndJoin(lines, block);
|
|
435
|
-
const inner = extractInnerByAttr(text, 'data-impeccable-variant'
|
|
486
|
+
const inner = extractInnerByAttr(text, 'data-impeccable-variant="' + variantNum + '"');
|
|
436
487
|
if (inner === null) return null;
|
|
437
488
|
const result = inner.split('\n');
|
|
438
489
|
// Collapse a lone empty leading/trailing line (common after string splice).
|
|
@@ -629,18 +680,10 @@ function argVal(args, flag) {
|
|
|
629
680
|
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
|
|
630
681
|
}
|
|
631
682
|
|
|
632
|
-
function parseVariantNumber(value) {
|
|
633
|
-
if (!/^(?:0|[1-9]\d{0,2})$/.test(value ?? '')) {
|
|
634
|
-
console.error('Invalid --variant value; expected an integer from 0 to 999');
|
|
635
|
-
process.exit(1);
|
|
636
|
-
}
|
|
637
|
-
return Number(value);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
683
|
// Auto-execute when run directly
|
|
641
684
|
const _running = process.argv[1];
|
|
642
685
|
if (_running?.endsWith('live-accept.mjs') || _running?.endsWith('live-accept.mjs/')) {
|
|
643
686
|
acceptCli();
|
|
644
687
|
}
|
|
645
688
|
|
|
646
|
-
export { findMarkerBlock, extractOriginal, extractVariant, extractCss, deindentContent, detectCommentSyntax };
|
|
689
|
+
export { findMarkerBlock, extractOriginal, extractVariant, extractCss, deindentContent, detectCommentSyntax, scrubManualEditsAgainstFile, scrubManualEditsAgainstOriginalBlock };
|