@atomixstudio/mcp 1.0.26 → 1.0.27

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 CHANGED
@@ -100,7 +100,7 @@ Run these prompts from your AI tool (e.g. **/--hello**, **/--get-started**):
100
100
 
101
101
  | Prompt | Description |
102
102
  |--------|-------------|
103
- | **/--hello** | Get started — overview, tokens, and tools. Run this first. |
103
+ | **/--hello** | Get started — overview, tokens, essential commands (including **/--sync-to-figma**). Run this first. |
104
104
  | **/--get-started** | Get started with design system in project. Three phases; creates files only after you approve. |
105
105
  | **/--rules** | Governance rules for your AI tool (Cursor, Copilot, Windsurf, etc.). |
106
106
  | **/--sync** | Sync tokens, AI rules, skills files, and dependencies manifest (icons, fonts). Use **/--refactor** to migrate deprecated tokens. |
package/dist/index.js CHANGED
@@ -1607,7 +1607,8 @@ function buildFigmaPayloadsFromDS(data) {
1607
1607
  }
1608
1608
  if (variables.length === 0 && modes.length === 0) modes.push("Light");
1609
1609
  const dsName = data.meta?.name;
1610
- const collectionName = dsName ? `${dsName} Foundations` : "Foundations";
1610
+ const collectionPrefix = dsName ? `${dsName} ` : "";
1611
+ const colorCollectionName = `${collectionPrefix}Colors`;
1611
1612
  const textStyles = [];
1612
1613
  const sizeToPx = (val, basePx = 16) => {
1613
1614
  if (typeof val === "number") return Math.round(val);
@@ -1642,7 +1643,15 @@ function buildFigmaPayloadsFromDS(data) {
1642
1643
  }
1643
1644
  return "Inter";
1644
1645
  };
1645
- const fontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1646
+ const toFontFamilyString = (val) => {
1647
+ if (typeof val === "string") {
1648
+ const s = val.trim().replace(/^["']|["']$/g, "");
1649
+ return s || "Inter";
1650
+ }
1651
+ return firstFont(val);
1652
+ };
1653
+ const fontFamilyMap = typography?.fontFamily ?? {};
1654
+ const defaultFontFamily = typography ? firstFont(typography.fontFamily ?? "Inter") : "Inter";
1646
1655
  const fontSizeMap = typography?.fontSize;
1647
1656
  const fontWeightMap = typography?.fontWeight;
1648
1657
  const lineHeightMap = typography?.lineHeight;
@@ -1653,6 +1662,10 @@ function buildFigmaPayloadsFromDS(data) {
1653
1662
  for (const [key, sizeVal] of Object.entries(fontSizeMap)) {
1654
1663
  const fontSize = sizeToPx(sizeVal);
1655
1664
  if (fontSize <= 0) continue;
1665
+ const role = typesetKeyToFontFamilyRole(key);
1666
+ const fontFamily = toFontFamilyString(
1667
+ fontFamilyMap[role] ?? fontFamilyMap.body ?? fontFamilyMap.heading ?? fontFamilyMap.display ?? defaultFontFamily
1668
+ );
1656
1669
  const lh = lineHeightMap && typeof lineHeightMap === "object" ? lineHeightMap[key] : void 0;
1657
1670
  const weight = fontWeightMap && typeof fontWeightMap === "object" ? fontWeightMap[key] : void 0;
1658
1671
  const fontWeight = weight != null ? String(weight) : "400";
@@ -1695,7 +1708,7 @@ function buildFigmaPayloadsFromDS(data) {
1695
1708
  const lineHeightUnitless = lhStr != null ? lhStr.endsWith("%") ? parseFloat(lhStr) / 100 : sizeToPx(lhStr) / fontSize : 1.5;
1696
1709
  const payload = {
1697
1710
  name: styleName.startsWith("Typography") ? styleName : `Typography / ${styleName.replace(/\//g, " / ")}`,
1698
- fontFamily,
1711
+ fontFamily: defaultFontFamily,
1699
1712
  fontWeight: String(style.fontWeight ?? "400"),
1700
1713
  fontSize,
1701
1714
  lineHeightUnit: "PERCENT",
@@ -1717,59 +1730,58 @@ function buildFigmaPayloadsFromDS(data) {
1717
1730
  const variables2 = [];
1718
1731
  for (const [key, val] of Object.entries(spacing.scale)) {
1719
1732
  const n = tokenValueToNumber(val);
1720
- if (n >= 0) variables2.push({ name: key, value: n });
1733
+ if (n >= 0) variables2.push({ name: `Spacing / ${key}`, value: n });
1721
1734
  }
1722
1735
  variables2.sort((a, b) => a.value - b.value);
1723
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1736
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Spacing`, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1724
1737
  }
1725
1738
  const radius = tokens?.radius;
1726
1739
  if (radius?.scale && typeof radius.scale === "object") {
1727
1740
  const variables2 = [];
1728
1741
  for (const [key, val] of Object.entries(radius.scale)) {
1729
1742
  const n = tokenValueToNumber(val);
1730
- if (n >= 0) variables2.push({ name: key, value: n });
1743
+ if (n >= 0) variables2.push({ name: `Radius / ${key}`, value: n });
1731
1744
  }
1732
1745
  variables2.sort((a, b) => a.value - b.value);
1733
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1746
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Radius`, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1734
1747
  }
1735
1748
  const borders = tokens?.borders;
1736
1749
  if (borders?.width && typeof borders.width === "object") {
1737
1750
  const variables2 = [];
1738
1751
  for (const [key, val] of Object.entries(borders.width)) {
1739
1752
  const n = tokenValueToNumber(val);
1740
- if (n >= 0) variables2.push({ name: key, value: n });
1753
+ if (n >= 0) variables2.push({ name: `Borders / ${key}`, value: n });
1741
1754
  }
1742
1755
  variables2.sort((a, b) => a.value - b.value);
1743
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1756
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Borders`, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1744
1757
  }
1745
1758
  const sizing = tokens?.sizing;
1759
+ const sizingVariables = [];
1746
1760
  if (sizing?.height && typeof sizing.height === "object") {
1747
- const variables2 = [];
1748
1761
  for (const [key, val] of Object.entries(sizing.height)) {
1749
1762
  const n = tokenValueToNumber(val);
1750
- if (n >= 0) variables2.push({ name: key, value: n });
1763
+ if (n >= 0) sizingVariables.push({ name: `Height / ${key}`, value: n });
1751
1764
  }
1752
- variables2.sort((a, b) => a.value - b.value);
1753
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Height", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1754
1765
  }
1755
1766
  if (sizing?.icon && typeof sizing.icon === "object") {
1756
- const variables2 = [];
1757
1767
  for (const [key, val] of Object.entries(sizing.icon)) {
1758
1768
  const n = tokenValueToNumber(val);
1759
- if (n >= 0) variables2.push({ name: key, value: n });
1769
+ if (n >= 0) sizingVariables.push({ name: `Icon / ${key}`, value: n });
1760
1770
  }
1761
- variables2.sort((a, b) => a.value - b.value);
1762
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Icon", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1771
+ }
1772
+ sizingVariables.sort((a, b) => a.value - b.value);
1773
+ if (sizingVariables.length > 0) {
1774
+ numberVariableCollections.push({ collectionName: `${collectionPrefix}Sizing`, categoryKey: "Sizing", variables: sizingVariables, scopes: ["WIDTH_HEIGHT"] });
1763
1775
  }
1764
1776
  const layout = tokens?.layout;
1765
1777
  if (layout?.breakpoint && typeof layout.breakpoint === "object") {
1766
1778
  const variables2 = [];
1767
1779
  for (const [key, val] of Object.entries(layout.breakpoint)) {
1768
1780
  const n = tokenValueToNumber(val);
1769
- if (n >= 0) variables2.push({ name: key, value: n });
1781
+ if (n >= 0) variables2.push({ name: `Breakpoint / ${key}`, value: n });
1770
1782
  }
1771
1783
  variables2.sort((a, b) => a.value - b.value);
1772
- if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Breakpoint", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1784
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${collectionPrefix}Layout`, categoryKey: "Layout", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1773
1785
  }
1774
1786
  const effectStyles = [];
1775
1787
  const shadows = tokens?.shadows;
@@ -1800,7 +1812,7 @@ function buildFigmaPayloadsFromDS(data) {
1800
1812
  return nameA.localeCompare(nameB);
1801
1813
  });
1802
1814
  return {
1803
- colorVariables: { collectionName, modes, variables },
1815
+ colorVariables: { collectionName: colorCollectionName, modes, variables },
1804
1816
  paintStyles,
1805
1817
  textStyles,
1806
1818
  numberVariableCollections,
@@ -1810,7 +1822,7 @@ function buildFigmaPayloadsFromDS(data) {
1810
1822
  function getExpectedFigmaNamesFromDS(data) {
1811
1823
  const payloads = buildFigmaPayloadsFromDS(data);
1812
1824
  const numberVariableNames = payloads.numberVariableCollections.flatMap(
1813
- (c) => c.variables.map((v) => `${c.categoryKey} / ${v.name}`)
1825
+ (c) => c.variables.map((v) => v.name)
1814
1826
  );
1815
1827
  return {
1816
1828
  colorVariableNames: payloads.colorVariables.variables.map((v) => v.name),
@@ -1941,10 +1953,10 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
1941
1953
  }
1942
1954
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
1943
1955
  function typesetKeyToFontFamilyRole(key) {
1944
- const prefix = key.split("-")[0] ?? "";
1945
- if (prefix === "display") return "display";
1946
- if (prefix === "heading") return "heading";
1947
- if (prefix === "mono") return "mono";
1956
+ const prefix = (key.split("-")[0] ?? key).toLowerCase();
1957
+ if (prefix === "display" || prefix.startsWith("display")) return "display";
1958
+ if (prefix === "heading" || prefix.startsWith("heading")) return "heading";
1959
+ if (prefix === "mono" || prefix.startsWith("mono")) return "mono";
1948
1960
  if (prefix.startsWith("body")) return "body";
1949
1961
  return "body";
1950
1962
  }
@@ -1962,12 +1974,15 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
1962
1974
  for (const key of Object.keys(fontSize)) {
1963
1975
  const role = typesetKeyToFontFamilyRole(key);
1964
1976
  const familyName = fontFamily[role] ?? fontFamily.body;
1965
- const fontFamilyVar = familyName ? `var(--${p}typography-font-family-${role})` : "";
1977
+ const fontFamilyVarName = familyName ? `--${p}typography-font-family-${role}` : void 0;
1978
+ const fontFamilyVar = familyName ? `var(${fontFamilyVarName})` : "";
1966
1979
  const keyKebab = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1967
1980
  typesets.push({
1968
1981
  key,
1969
1982
  cssClass: `.typeset-${keyKebab}`,
1970
1983
  fontFamilyVar: fontFamilyVar || "inherit",
1984
+ fontFamilyVarName,
1985
+ fontFamilyValue: familyName,
1971
1986
  fontSizeVar: `var(--${p}typography-${key}-size)`,
1972
1987
  fontWeightVar: `var(--${p}typography-${key}-weight)`,
1973
1988
  lineHeightVar: `var(--${p}typography-${key}-line-height)`,
@@ -1983,7 +1998,7 @@ function buildTypesetsList(typography, cssPrefix = "atmx") {
1983
1998
  var server = new Server(
1984
1999
  {
1985
2000
  name: "atomix-mcp-user",
1986
- version: "1.0.25"
2001
+ version: "1.0.26"
1987
2002
  },
1988
2003
  {
1989
2004
  capabilities: {
@@ -2652,7 +2667,7 @@ Version: ${designSystemData.meta.version}`,
2652
2667
  text: JSON.stringify({
2653
2668
  count: typesets.length,
2654
2669
  typesets,
2655
- instruction: "Emit one CSS rule per typeset using the cssClass and the listed var() properties. Include font-family, font-size, font-weight, line-height; add letter-spacing when present; add text-transform and text-decoration when hasTextTransform/hasTextDecoration are true so the result is 1:1 with the design system.",
2670
+ instruction: "Emit one CSS rule per typeset using the cssClass and the listed var() properties. Include font-family, font-size, font-weight, line-height; add letter-spacing when present; add text-transform and text-decoration when hasTextTransform/hasTextDecoration are true so the result is 1:1 with the design system. The synced token file quotes font names that contain spaces; no override is needed in the typeset file. For native (iOS/Android) projects, font names with spaces must be quoted or use the platform's canonical font name in theme or resource files.",
2656
2671
  ...underlineThickness != null && underlineOffset != null && {
2657
2672
  underlineVars: {
2658
2673
  thickness: `var(--${cssPrefix}-typography-underline-thickness)`,
@@ -3083,7 +3098,7 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3083
3098
  showcase: platform2 === "web" || !platform2 ? {
3084
3099
  path: "atomix-setup-showcase.html",
3085
3100
  template: SHOWCASE_HTML_TEMPLATE,
3086
- substitutionInstructions: "Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export."
3101
+ substitutionInstructions: 'Replace placeholders with values from the synced token file. MCP/sync/export use the --atmx- prefix. {{TOKENS_CSS_PATH}} = path to the synced token file (e.g. ./tokens.css, same as syncAll output). {{TYPESETS_LINK}} = if a typeset CSS file was created (e.g. typesets.css), the full tag e.g. <link rel="stylesheet" href="typesets.css">, otherwise empty string. {{DS_NAME}} = design system name. {{BRAND_PRIMARY_VAR}} = var(--atmx-color-brand-primary). {{BRAND_PRIMARY_FOREGROUND_VAR}} = var(--atmx-color-brand-primary-foreground). {{HEADING_FONT_VAR}} = var(--atmx-typography-font-family-heading) or var(--atmx-typography-font-family-display). {{FONT_FAMILY_VAR}} = var(--atmx-typography-font-family-body). {{LARGEST_DISPLAY_TYPESET_CLASS}} = largest display typeset class from listTypesets (display role, largest font size; e.g. typeset-display-2xl or typeset-display-bold), or empty string if no typeset file. {{BODY_TYPESET_CLASS}} = typeset class for body text from listTypesets (e.g. typeset-body-md), or empty string if no typeset file. {{FONT_LINK_TAG}} = Google Fonts <link> for the font, or empty string. Do not invent CSS variable names; use only vars that exist in the export.'
3087
3102
  } : void 0,
3088
3103
  meta: {
3089
3104
  dsName: data.meta.name,
@@ -3142,11 +3157,10 @@ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
3142
3157
  if (payloads.numberVariableCollections.length > 0) {
3143
3158
  const numberResults = [];
3144
3159
  try {
3145
- const foundationsName = payloads.colorVariables.collectionName;
3146
3160
  for (const coll of payloads.numberVariableCollections) {
3147
3161
  const result = await sendBridgeRequest("create_number_variables", {
3148
- collectionName: foundationsName,
3149
- variables: coll.variables.map((v) => ({ name: `${coll.categoryKey} / ${v.name}`, value: v.value })),
3162
+ collectionName: coll.collectionName,
3163
+ variables: coll.variables.map((v) => ({ name: v.name, value: v.value })),
3150
3164
  scopes: coll.scopes
3151
3165
  });
3152
3166
  numberResults.push({ categoryKey: coll.categoryKey, result });
@@ -3625,6 +3639,7 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3625
3639
  <title>Setup complete \u2014 {{DS_NAME}}</title>
3626
3640
  {{FONT_LINK_TAG}}
3627
3641
  <link rel="stylesheet" href="{{TOKENS_CSS_PATH}}">
3642
+ {{TYPESETS_LINK}}
3628
3643
  <style>
3629
3644
  * { box-sizing: border-box; }
3630
3645
  body {
@@ -3635,9 +3650,11 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3635
3650
  min-height: 100vh;
3636
3651
  padding: 2rem;
3637
3652
  display: flex;
3653
+ justify-content: center;
3654
+ align-items: center;
3638
3655
  }
3639
3656
  .wrap { width: 375px; max-width: 100%; }
3640
- .icon { width: 2rem; height: 2rem; margin: 0 auto 1rem; }
3657
+ .icon { width: 2rem; height: 2rem; margin: 0 0 1rem; }
3641
3658
  h1 {
3642
3659
  font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3643
3660
  font-size: clamp(3rem, 4vw, 5rem);
@@ -3653,12 +3670,12 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3653
3670
  .tips a { color: inherit; text-decoration: underline; }
3654
3671
  </style>
3655
3672
  </head>
3656
- <body>
3673
+ <body class="{{BODY_TYPESET_CLASS}}">
3657
3674
  <div class="wrap">
3658
3675
  <div class="icon" aria-hidden="true">
3659
3676
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="44" height="44"><path d="M20 6L9 17l-5-5"/></svg>
3660
3677
  </div>
3661
- <h1>You're all set with {{DS_NAME}}</h1>
3678
+ <h1 class="{{LARGEST_DISPLAY_TYPESET_CLASS}}">You're all set with {{DS_NAME}}</h1>
3662
3679
  <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon.</p>
3663
3680
  <div class="now">
3664
3681
  <strong>What you can do now:</strong>
@@ -4311,7 +4328,7 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4311
4328
  - **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css"). syncAll also writes skills and atomix-dependencies.json. You must call syncAll; do not only suggest the user run it later.
4312
4329
  - **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
4313
4330
  - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4314
- - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1), FONT_FAMILY_VAR (body), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4331
+ - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, TYPESETS_LINK (if you created a typeset CSS file, use the full \`<link rel="stylesheet" href="\u2026">\` tag; otherwise empty string), DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1 fallback), FONT_FAMILY_VAR (body fallback), LARGEST_DISPLAY_TYPESET_CLASS (largest display typeset from listTypesets) and BODY_TYPESET_CLASS (e.g. typeset-body-md; leave empty if no typeset file), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4315
4332
  - Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
4316
4333
  - **After reporting \u2013 styles/theme:**
4317
4334
  - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and emit **one CSS class per typeset** (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
@@ -4431,6 +4448,8 @@ function onShutdown() {
4431
4448
  }
4432
4449
  process.on("SIGINT", onShutdown);
4433
4450
  process.on("SIGTERM", onShutdown);
4451
+ process.stdin.on("end", onShutdown);
4452
+ process.stdin.on("close", onShutdown);
4434
4453
  async function main() {
4435
4454
  await startServer();
4436
4455
  }