@aitrien/motifspec 1.13.0 → 1.13.3

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +275 -257
  2. package/LICENSE +21 -21
  3. package/README.md +128 -150
  4. package/dist/backdrop.html +1 -1
  5. package/dist/chroma.css +36 -36
  6. package/dist/codex.css +1145 -1145
  7. package/dist/codex.js +126 -126
  8. package/dist/contrast.md +101 -101
  9. package/dist/fonts.css +3 -3
  10. package/dist/icons.svg +18 -18
  11. package/dist/plate.html +1 -1
  12. package/dist/plate.js +292 -292
  13. package/dist/theme-init.js +1 -1
  14. package/dist/tokens.css +466 -466
  15. package/dist/tokens.js +3330 -3330
  16. package/dist/tokens.json +3330 -3330
  17. package/docs/components/_template.md +37 -37
  18. package/docs/components/accession-label.md +106 -106
  19. package/docs/components/backdrop.md +83 -83
  20. package/docs/components/button-and-link.md +87 -87
  21. package/docs/components/callout.md +75 -75
  22. package/docs/components/code-block.md +109 -109
  23. package/docs/components/data-table.md +73 -73
  24. package/docs/components/figure.md +68 -68
  25. package/docs/components/heading-anchor.md +76 -76
  26. package/docs/components/icon.md +63 -63
  27. package/docs/components/ornament.md +77 -77
  28. package/docs/components/pagination.md +78 -78
  29. package/docs/components/post-list-item.md +91 -91
  30. package/docs/components/project-card.md +82 -82
  31. package/docs/components/prose.md +122 -122
  32. package/docs/components/site-footer.md +99 -99
  33. package/docs/components/site-header.md +133 -133
  34. package/docs/components/specimen-plate.md +92 -92
  35. package/docs/components/table-of-contents.md +83 -83
  36. package/docs/components/tag.md +70 -70
  37. package/docs/components/tooltip.md +67 -67
  38. package/docs/hugo.md +132 -120
  39. package/docs/syntax-highlighting.md +55 -55
  40. package/docs/templates.md +21 -30
  41. package/fonts/OFL-jetbrains-mono.txt +93 -93
  42. package/fonts/OFL-national-park.txt +93 -93
  43. package/fonts/OFL-yarndings-12.txt +93 -93
  44. package/icons/LICENSE +202 -202
  45. package/icons/manifest.json +75 -75
  46. package/layouts/_markup/render-blockquote.html +30 -30
  47. package/layouts/_markup/render-codeblock.html +18 -18
  48. package/layouts/_markup/render-heading.html +2 -2
  49. package/layouts/_markup/render-image.html +14 -14
  50. package/layouts/_markup/render-link.html +14 -14
  51. package/layouts/_markup/render-table.html +31 -31
  52. package/layouts/_partials/motifspec/accession/id.html +2 -2
  53. package/layouts/_partials/motifspec/accession/label.html +18 -18
  54. package/layouts/_partials/motifspec/accession/status.html +2 -2
  55. package/layouts/_partials/motifspec/article.html +24 -24
  56. package/layouts/_partials/motifspec/back-link.html +2 -2
  57. package/layouts/_partials/motifspec/body-end.html +5 -5
  58. package/layouts/_partials/motifspec/body-start.html +5 -5
  59. package/layouts/_partials/motifspec/figure.html +14 -14
  60. package/layouts/_partials/motifspec/filed-under.html +5 -5
  61. package/layouts/_partials/motifspec/h2s.html +7 -7
  62. package/layouts/_partials/motifspec/head.html +15 -15
  63. package/layouts/_partials/motifspec/pagination-entries.html +34 -34
  64. package/layouts/_partials/motifspec/pagination-pages.html +30 -30
  65. package/layouts/_partials/motifspec/post-list-item.html +15 -15
  66. package/layouts/_partials/motifspec/project-card.html +23 -23
  67. package/layouts/_partials/motifspec/section-heading.html +3 -3
  68. package/layouts/_partials/motifspec/site-header.html +27 -27
  69. package/layouts/_partials/motifspec/table-of-contents.html +21 -21
  70. package/layouts/_partials/motifspec/tag-filter.html +22 -22
  71. package/layouts/_partials/motifspec/tags.html +8 -8
  72. package/layouts/_partials/motifspec/tokens.html +9 -9
  73. package/package.json +68 -68
  74. package/scripts/check.mjs +153 -153
  75. package/scripts/lib.mjs +176 -176
  76. package/tokens/README.md +3 -3
  77. package/tokens/border.json +63 -63
  78. package/tokens/color.json +1902 -1902
  79. package/tokens/motion.json +207 -207
  80. package/tokens/radius.json +53 -53
  81. package/tokens/size.json +378 -378
  82. package/tokens/space.json +289 -289
  83. package/tokens/typography.json +859 -859
package/scripts/check.mjs CHANGED
@@ -1,153 +1,153 @@
1
- #!/usr/bin/env node
2
- // The MotifSpec rule checker: one implementation for this repo's templates (npm run audit), for sites that use
3
- // MotifSpec (their CI runs this file from the pinned module) and for the MCP server's check_styles tool.
4
- //
5
- // node scripts/check.mjs [project-root] check a consuming project's layouts/ and content/
6
- // node scripts/check.mjs [project-root] --dirs src,app check other folders instead (a non-Hugo project)
7
- // node scripts/check.mjs [project-root] --json print the problems as JSON
8
- // npx motifspec-check [same options] the same, from the npm package
9
- //
10
- // Rules come from the files of this release: class names from dist/codex.css, tokens from dist/tokens.json, icons
11
- // from icons/manifest.json. A site therefore checks against the version it has pinned, not against main.
12
- import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
13
- import { extname, join, relative, resolve } from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
- import { ROOT } from './lib.mjs';
16
-
17
- export const RULES = {
18
- stylesheet: 'Styles come only from MotifSpec. Remove the stylesheet; use components and tokens, or propose an addition to MotifSpec.',
19
- 'style-attribute': 'No style attributes. Use a component class from docs/components/*.md.',
20
- 'raw-colour': 'No raw colours. Use a semantic colour token through a component class, or var(--cx-color-…) in CSS.',
21
- 'raw-unit': 'No raw sizes or durations. Use a semantic space, size, radius, border or motion token.',
22
- 'primitive-token': 'Primitive tokens are not for use outside MotifSpec. Use the semantic token that aliases it.',
23
- 'unknown-token': 'Not a MotifSpec token. Check the name in dist/tokens.json.',
24
- 'deprecated-token': 'This token is deprecated (see CHANGELOG.md). Use its replacement.',
25
- 'unknown-class': 'Not a MotifSpec class. Copy the markup from docs/components/*.md; don\'t invent cx- classes.',
26
- 'unknown-icon': 'Not an approved icon. Use a key from icons/manifest.json, for its listed role.',
27
- separator: 'Separate inline metadata with <span class="cx-sep" aria-hidden="true">|</span>, never "·".',
28
- };
29
-
30
- const read = (...p) => readFileSync(join(ROOT, ...p), 'utf8');
31
- let data;
32
- function load() {
33
- if (data) return data;
34
- const tokens = JSON.parse(read('dist', 'tokens.json')).tokens;
35
- const sizes = JSON.parse(read('tokens', 'size.json'));
36
- data = {
37
- // Classes the stylesheet styles, plus hooks the shipped scripts look for (such as cx-code__copy-label).
38
- classes: new Set([
39
- ...read('dist', 'codex.css').match(/\.cx-[a-z0-9_-]+/g).map((c) => c.slice(1)),
40
- ...['codex.js', 'plate.js'].flatMap((f) => read('dist', f).match(/(?<=[.'"\s])cx-[a-z0-9_-]+/g) ?? []),
41
- ]),
42
- icons: new Set(Object.keys(JSON.parse(read('icons', 'manifest.json')).icons)),
43
- tokens: new Map(tokens.map((t) => [t.css, t])),
44
- breakpoints: new Set(Object.entries(sizes.primitive.size.breakpoint).filter(([k]) => !k.startsWith('$')).map(([, t]) => `${t.$value.value}${t.$value.unit}`)),
45
- };
46
- return data;
47
- }
48
-
49
- // A hex colour that isn't part of a fragment link such as href="#cx-icon-add" or an HTML entity.
50
- const RAW_COLOR = /(?<![\w&/."-])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/g;
51
- // A number with a length or time unit. Zero is allowed (0px is not a size).
52
- const RAW_UNIT = /(?<![\w.#-])-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|svh|dvh|lvh|cqi|cqw|pt|pc|cm|mm|in|ms|s)\b/g;
53
- const STYLE_ATTR = /\sstyle\s*=/g;
54
- const TOKEN_REF = /--cx-[a-z0-9-]+/g;
55
- const CLASS_ATTR = /\s(?:class|className)\s*=\s*["']([^"']*)["']/g;
56
- const ICON_REF = /#cx-icon-([a-z0-9-]*)/g;
57
- const TEMPLATE = /\{\{.*?\}\}/g; // Hugo actions; replaced by a placeholder so a class or icon built from them is skipped
58
-
59
- /**
60
- * Check one file's text. kind: "html" (templates and markup), "md" (content) or "css".
61
- * Returns [{ file, line, rule, match, message }].
62
- */
63
- export function checkText(text, { file = '<input>', kind = 'html' } = {}) {
64
- const { classes, icons, tokens, breakpoints } = load();
65
- const problems = [];
66
- const add = (line, rule, match) => problems.push({ file, line, rule, match, message: RULES[rule] });
67
- text.split('\n').forEach((raw, i) => {
68
- const n = i + 1;
69
- // In CSS, a breakpoint value in an @media condition is the build's own output and allowed.
70
- const line = kind === 'css' && /@media/.test(raw) ? raw.replace(/(\d+(?:\.\d+)?)(rem|px|em)\b/g, (v) => (breakpoints.has(v) ? 'breakpoint' : v)) : raw;
71
- if (kind !== 'css') for (const m of line.matchAll(STYLE_ATTR)) add(n, 'style-attribute', m[0].trim());
72
- for (const m of line.matchAll(RAW_COLOR)) add(n, 'raw-colour', m[0]);
73
- for (const m of line.matchAll(RAW_UNIT)) if (!/^-?0+(?:\.0+)?[a-z]+$/.test(m[0])) add(n, 'raw-unit', m[0]);
74
- for (const [name] of line.matchAll(TOKEN_REF)) {
75
- if (name.startsWith('--cx-primitive-')) add(n, 'primitive-token', name);
76
- else if (!tokens.has(name)) add(n, 'unknown-token', name);
77
- else if (tokens.get(name).deprecated) add(n, 'deprecated-token', name);
78
- }
79
- if (kind !== 'css') {
80
- if (line.includes('·')) add(n, 'separator', '·');
81
- const plain = line.replace(TEMPLATE, '§');
82
- for (const m of plain.matchAll(CLASS_ATTR)) {
83
- for (const c of m[1].split(/\s+/)) if (c.startsWith('cx-') && !c.includes('§') && !classes.has(c)) add(n, 'unknown-class', c);
84
- }
85
- for (const m of plain.matchAll(ICON_REF)) {
86
- if (m[1] && plain[m.index + m[0].length] !== '§' && !icons.has(m[1])) add(n, 'unknown-icon', `cx-icon-${m[1]}`);
87
- }
88
- }
89
- });
90
- return problems;
91
- }
92
-
93
- const KIND = {
94
- '.html': 'html', '.xml': 'html', '.jsx': 'html', '.tsx': 'html', '.vue': 'html', '.svelte': 'html', '.astro': 'html',
95
- '.txt': 'md', '.md': 'md', '.mdx': 'md', '.json': 'md', '.css': 'css',
96
- };
97
- // Build output and dependencies, which contain MotifSpec's own CSS.
98
- const SKIP = new Set(['.git', 'public', 'resources', 'node_modules', 'dist', 'build', '.next', '.astro', '.svelte-kit']);
99
- function* walk(dir) {
100
- for (const name of readdirSync(dir)) {
101
- if (SKIP.has(name)) continue;
102
- const p = join(dir, name);
103
- if (statSync(p).isDirectory()) yield* walk(p); else yield p;
104
- }
105
- }
106
-
107
- /** Check a project that uses MotifSpec: no stylesheets anywhere, and every file in `dirs` checked by checkText. */
108
- export function checkProject(root, { dirs = ['layouts', 'content'] } = {}) {
109
- const problems = [];
110
- for (const file of walk(root)) {
111
- if (['.css', '.scss', '.sass', '.less'].includes(extname(file))) {
112
- problems.push({ file: relative(root, file), line: 0, rule: 'stylesheet', match: extname(file), message: RULES.stylesheet });
113
- }
114
- }
115
- for (const dir of dirs) {
116
- const abs = join(root, dir);
117
- if (!existsSync(abs)) continue;
118
- for (const file of walk(abs)) {
119
- const kind = KIND[extname(file)];
120
- if (kind && kind !== 'css') problems.push(...checkText(readFileSync(file, 'utf8'), { file: relative(root, file), kind }));
121
- }
122
- }
123
- return problems;
124
- }
125
-
126
- export function format(p) {
127
- return `${p.file}${p.line ? `:${p.line}` : ''}: [${p.rule}] "${p.match}". ${p.message}`;
128
- }
129
-
130
- // Run as a command: directly, or through npm's bin symlink (node_modules/.bin/motifspec-check).
131
- const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
132
- if (isMain) {
133
- const args = process.argv.slice(2);
134
- const at = args.indexOf('--dirs');
135
- const dirs = at >= 0 ? args[at + 1]?.split(',').filter(Boolean) : undefined;
136
- if (at >= 0 && !dirs?.length) { console.error('--dirs needs a comma-separated list, e.g. --dirs src,app'); process.exit(2); }
137
- const root = resolve(args.find((a, i) => !a.startsWith('--') && (at < 0 || i !== at + 1)) ?? '.');
138
- if (!existsSync(root)) { console.error(`No such folder: ${root}`); process.exit(2); }
139
- const problems = checkProject(root, dirs ? { dirs } : undefined);
140
- const version = JSON.parse(read('dist', 'tokens.json')).version;
141
- if (args.includes('--json')) {
142
- console.log(JSON.stringify({ version, problems }, null, 2));
143
- } else if (problems.length) {
144
- for (const p of problems) {
145
- console.error(`✗ ${format(p)}`);
146
- if (process.env.GITHUB_ACTIONS) console.log(`::error file=${p.file},line=${p.line || 1},title=MotifSpec ${p.rule}::${p.message}`);
147
- }
148
- console.error(`${problems.length} problem(s), checked against MotifSpec ${version}.`);
149
- } else {
150
- console.log(`No problems in ${relative(process.cwd(), root) || '.'}: checked against MotifSpec ${version}.`);
151
- }
152
- process.exit(problems.length ? 1 : 0);
153
- }
1
+ #!/usr/bin/env node
2
+ // The MotifSpec rule checker: one implementation for this repo's templates (npm run audit), for sites that use
3
+ // MotifSpec (their CI runs this file from the pinned module) and for any tool that imports checkText.
4
+ //
5
+ // node scripts/check.mjs [project-root] check a consuming project's layouts/ and content/
6
+ // node scripts/check.mjs [project-root] --dirs src,app check other folders instead (a non-Hugo project)
7
+ // node scripts/check.mjs [project-root] --json print the problems as JSON
8
+ // npx motifspec-check [same options] the same, from the npm package
9
+ //
10
+ // Rules come from the files of this release: class names from dist/codex.css, tokens from dist/tokens.json, icons
11
+ // from icons/manifest.json. A site therefore checks against the version it has pinned, not against main.
12
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
13
+ import { extname, join, relative, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { ROOT } from './lib.mjs';
16
+
17
+ export const RULES = {
18
+ stylesheet: 'Styles come only from MotifSpec. Remove the stylesheet; use components and tokens, or propose an addition to MotifSpec.',
19
+ 'style-attribute': 'No style attributes. Use a component class from docs/components/*.md.',
20
+ 'raw-colour': 'No raw colours. Use a semantic colour token through a component class, or var(--cx-color-…) in CSS.',
21
+ 'raw-unit': 'No raw sizes or durations. Use a semantic space, size, radius, border or motion token.',
22
+ 'primitive-token': 'Primitive tokens are not for use outside MotifSpec. Use the semantic token that aliases it.',
23
+ 'unknown-token': 'Not a MotifSpec token. Check the name in dist/tokens.json.',
24
+ 'deprecated-token': 'This token is deprecated (see CHANGELOG.md). Use its replacement.',
25
+ 'unknown-class': 'Not a MotifSpec class. Copy the markup from docs/components/*.md; don\'t invent cx- classes.',
26
+ 'unknown-icon': 'Not an approved icon. Use a key from icons/manifest.json, for its listed role.',
27
+ separator: 'Separate inline metadata with <span class="cx-sep" aria-hidden="true">|</span>, never "·".',
28
+ };
29
+
30
+ const read = (...p) => readFileSync(join(ROOT, ...p), 'utf8');
31
+ let data;
32
+ function load() {
33
+ if (data) return data;
34
+ const tokens = JSON.parse(read('dist', 'tokens.json')).tokens;
35
+ const sizes = JSON.parse(read('tokens', 'size.json'));
36
+ data = {
37
+ // Classes the stylesheet styles, plus hooks the shipped scripts look for (such as cx-code__copy-label).
38
+ classes: new Set([
39
+ ...read('dist', 'codex.css').match(/\.cx-[a-z0-9_-]+/g).map((c) => c.slice(1)),
40
+ ...['codex.js', 'plate.js'].flatMap((f) => read('dist', f).match(/(?<=[.'"\s])cx-[a-z0-9_-]+/g) ?? []),
41
+ ]),
42
+ icons: new Set(Object.keys(JSON.parse(read('icons', 'manifest.json')).icons)),
43
+ tokens: new Map(tokens.map((t) => [t.css, t])),
44
+ breakpoints: new Set(Object.entries(sizes.primitive.size.breakpoint).filter(([k]) => !k.startsWith('$')).map(([, t]) => `${t.$value.value}${t.$value.unit}`)),
45
+ };
46
+ return data;
47
+ }
48
+
49
+ // A hex colour that isn't part of a fragment link such as href="#cx-icon-add" or an HTML entity.
50
+ const RAW_COLOR = /(?<![\w&/."-])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/g;
51
+ // A number with a length or time unit. Zero is allowed (0px is not a size).
52
+ const RAW_UNIT = /(?<![\w.#-])-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|svh|dvh|lvh|cqi|cqw|pt|pc|cm|mm|in|ms|s)\b/g;
53
+ const STYLE_ATTR = /\sstyle\s*=/g;
54
+ const TOKEN_REF = /--cx-[a-z0-9-]+/g;
55
+ const CLASS_ATTR = /\s(?:class|className)\s*=\s*["']([^"']*)["']/g;
56
+ const ICON_REF = /#cx-icon-([a-z0-9-]*)/g;
57
+ const TEMPLATE = /\{\{.*?\}\}/g; // Hugo actions; replaced by a placeholder so a class or icon built from them is skipped
58
+
59
+ /**
60
+ * Check one file's text. kind: "html" (templates and markup), "md" (content) or "css".
61
+ * Returns [{ file, line, rule, match, message }].
62
+ */
63
+ export function checkText(text, { file = '<input>', kind = 'html' } = {}) {
64
+ const { classes, icons, tokens, breakpoints } = load();
65
+ const problems = [];
66
+ const add = (line, rule, match) => problems.push({ file, line, rule, match, message: RULES[rule] });
67
+ text.split('\n').forEach((raw, i) => {
68
+ const n = i + 1;
69
+ // In CSS, a breakpoint value in an @media condition is the build's own output and allowed.
70
+ const line = kind === 'css' && /@media/.test(raw) ? raw.replace(/(\d+(?:\.\d+)?)(rem|px|em)\b/g, (v) => (breakpoints.has(v) ? 'breakpoint' : v)) : raw;
71
+ if (kind !== 'css') for (const m of line.matchAll(STYLE_ATTR)) add(n, 'style-attribute', m[0].trim());
72
+ for (const m of line.matchAll(RAW_COLOR)) add(n, 'raw-colour', m[0]);
73
+ for (const m of line.matchAll(RAW_UNIT)) if (!/^-?0+(?:\.0+)?[a-z]+$/.test(m[0])) add(n, 'raw-unit', m[0]);
74
+ for (const [name] of line.matchAll(TOKEN_REF)) {
75
+ if (name.startsWith('--cx-primitive-')) add(n, 'primitive-token', name);
76
+ else if (!tokens.has(name)) add(n, 'unknown-token', name);
77
+ else if (tokens.get(name).deprecated) add(n, 'deprecated-token', name);
78
+ }
79
+ if (kind !== 'css') {
80
+ if (line.includes('·')) add(n, 'separator', '·');
81
+ const plain = line.replace(TEMPLATE, '§');
82
+ for (const m of plain.matchAll(CLASS_ATTR)) {
83
+ for (const c of m[1].split(/\s+/)) if (c.startsWith('cx-') && !c.includes('§') && !classes.has(c)) add(n, 'unknown-class', c);
84
+ }
85
+ for (const m of plain.matchAll(ICON_REF)) {
86
+ if (m[1] && plain[m.index + m[0].length] !== '§' && !icons.has(m[1])) add(n, 'unknown-icon', `cx-icon-${m[1]}`);
87
+ }
88
+ }
89
+ });
90
+ return problems;
91
+ }
92
+
93
+ const KIND = {
94
+ '.html': 'html', '.xml': 'html', '.jsx': 'html', '.tsx': 'html', '.vue': 'html', '.svelte': 'html', '.astro': 'html',
95
+ '.txt': 'md', '.md': 'md', '.mdx': 'md', '.json': 'md', '.css': 'css',
96
+ };
97
+ // Build output and dependencies, which contain MotifSpec's own CSS.
98
+ const SKIP = new Set(['.git', 'public', 'resources', 'node_modules', 'dist', 'build', '.next', '.astro', '.svelte-kit']);
99
+ function* walk(dir) {
100
+ for (const name of readdirSync(dir)) {
101
+ if (SKIP.has(name)) continue;
102
+ const p = join(dir, name);
103
+ if (statSync(p).isDirectory()) yield* walk(p); else yield p;
104
+ }
105
+ }
106
+
107
+ /** Check a project that uses MotifSpec: no stylesheets anywhere, and every file in `dirs` checked by checkText. */
108
+ export function checkProject(root, { dirs = ['layouts', 'content'] } = {}) {
109
+ const problems = [];
110
+ for (const file of walk(root)) {
111
+ if (['.css', '.scss', '.sass', '.less'].includes(extname(file))) {
112
+ problems.push({ file: relative(root, file), line: 0, rule: 'stylesheet', match: extname(file), message: RULES.stylesheet });
113
+ }
114
+ }
115
+ for (const dir of dirs) {
116
+ const abs = join(root, dir);
117
+ if (!existsSync(abs)) continue;
118
+ for (const file of walk(abs)) {
119
+ const kind = KIND[extname(file)];
120
+ if (kind && kind !== 'css') problems.push(...checkText(readFileSync(file, 'utf8'), { file: relative(root, file), kind }));
121
+ }
122
+ }
123
+ return problems;
124
+ }
125
+
126
+ export function format(p) {
127
+ return `${p.file}${p.line ? `:${p.line}` : ''}: [${p.rule}] "${p.match}". ${p.message}`;
128
+ }
129
+
130
+ // Run as a command: directly, or through npm's bin symlink (node_modules/.bin/motifspec-check).
131
+ const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
132
+ if (isMain) {
133
+ const args = process.argv.slice(2);
134
+ const at = args.indexOf('--dirs');
135
+ const dirs = at >= 0 ? args[at + 1]?.split(',').filter(Boolean) : undefined;
136
+ if (at >= 0 && !dirs?.length) { console.error('--dirs needs a comma-separated list, e.g. --dirs src,app'); process.exit(2); }
137
+ const root = resolve(args.find((a, i) => !a.startsWith('--') && (at < 0 || i !== at + 1)) ?? '.');
138
+ if (!existsSync(root)) { console.error(`No such folder: ${root}`); process.exit(2); }
139
+ const problems = checkProject(root, dirs ? { dirs } : undefined);
140
+ const version = JSON.parse(read('dist', 'tokens.json')).version;
141
+ if (args.includes('--json')) {
142
+ console.log(JSON.stringify({ version, problems }, null, 2));
143
+ } else if (problems.length) {
144
+ for (const p of problems) {
145
+ console.error(`✗ ${format(p)}`);
146
+ if (process.env.GITHUB_ACTIONS) console.log(`::error file=${p.file},line=${p.line || 1},title=MotifSpec ${p.rule}::${p.message}`);
147
+ }
148
+ console.error(`${problems.length} problem(s), checked against MotifSpec ${version}.`);
149
+ } else {
150
+ console.log(`No problems in ${relative(process.cwd(), root) || '.'}: checked against MotifSpec ${version}.`);
151
+ }
152
+ process.exit(problems.length ? 1 : 0);
153
+ }