@ng-solid/mcp 22.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.
Files changed (47) hide show
  1. package/GUIDE.md +292 -0
  2. package/README.md +159 -0
  3. package/data/design-system.json +24204 -0
  4. package/data/icons.json +1 -0
  5. package/data/skills/ns-solid-components/SKILL.md +140 -0
  6. package/data/skills/ns-solid-forms/SKILL.md +183 -0
  7. package/data/skills/ns-solid-icons/SKILL.md +95 -0
  8. package/data/skills/ns-solid-install/SKILL.md +120 -0
  9. package/data/skills/ns-solid-style-contract/SKILL.md +84 -0
  10. package/data/skills/ns-solid-theming/SKILL.md +123 -0
  11. package/index.d.ts +3 -0
  12. package/index.js +38 -0
  13. package/lib/data.d.ts +26 -0
  14. package/lib/data.js +51 -0
  15. package/lib/install-skills.d.ts +1 -0
  16. package/lib/install-skills.js +159 -0
  17. package/lib/preset-validator.d.ts +44 -0
  18. package/lib/preset-validator.js +172 -0
  19. package/lib/prompts.d.ts +2 -0
  20. package/lib/prompts.js +173 -0
  21. package/lib/resources.d.ts +2 -0
  22. package/lib/resources.js +328 -0
  23. package/lib/server.d.ts +3 -0
  24. package/lib/server.js +31 -0
  25. package/lib/style-contract.d.ts +37 -0
  26. package/lib/style-contract.js +167 -0
  27. package/lib/template-parser.d.ts +29 -0
  28. package/lib/template-parser.js +230 -0
  29. package/lib/tools.components.d.ts +44 -0
  30. package/lib/tools.components.js +836 -0
  31. package/lib/tools.d.ts +9 -0
  32. package/lib/tools.guides.d.ts +15 -0
  33. package/lib/tools.guides.js +103 -0
  34. package/lib/tools.icons.d.ts +7 -0
  35. package/lib/tools.icons.js +106 -0
  36. package/lib/tools.js +18 -0
  37. package/lib/tools.tokens.d.ts +7 -0
  38. package/lib/tools.tokens.js +278 -0
  39. package/lib/types.d.ts +349 -0
  40. package/lib/types.js +11 -0
  41. package/package.json +52 -0
  42. package/skills/ns-solid-components/SKILL.md +77 -0
  43. package/skills/ns-solid-forms/SKILL.md +166 -0
  44. package/skills/ns-solid-icons/SKILL.md +95 -0
  45. package/skills/ns-solid-install/SKILL.md +119 -0
  46. package/skills/ns-solid-style-contract/SKILL.md +84 -0
  47. package/skills/ns-solid-theming/SKILL.md +123 -0
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: ns-solid-theming
3
+ description: Work with the Arvisfy three-tier token architecture in @ng-solid/design-system — extending the preset, validating tokens and switching color scheme at runtime.
4
+ ---
5
+
6
+ # Theming con Arvisfy
7
+
8
+ ## Los tres niveles de tokens
9
+
10
+ `packages/design-system/src/lib/theme/presets/arvisfy.preset.ts` define el
11
+ preset en tres tiers, cada uno sólo puede referenciar al tier anterior:
12
+
13
+ 1. **`primitive`** — únicos valores crudos permitidos (hex, `rem`, pesos de
14
+ fuente, escalas numéricas). Ejemplo real: `primitive.color.klein.500 =
15
+ '#3665F3'`.
16
+ 2. **`semantic`** — referencia primitivos con la sintaxis `{primitive.…}`, y
17
+ se divide en `common` (independiente de esquema) y
18
+ `colorScheme.light` / `colorScheme.dark` (un valor distinto por esquema).
19
+ 3. **`components`** — referencia semánticos con `{semantic.…}`. Es el único
20
+ tier que las hojas de estilo de componente pueden consumir, y sólo a
21
+ través de la variable CSS resultante.
22
+
23
+ Conteo actual del preset Arvisfy:
24
+
25
+ 157 primitivos, 265 semánticos, 693 de componente en 43 grupos.
26
+
27
+ ## Regla de oro del SCSS de componente
28
+
29
+ El SCSS de un componente **sólo** puede leer variables `--ns-component-*`.
30
+ Nunca `--ns-primitive-*` ni `--ns-semantic-*` directamente, y nunca un valor
31
+ crudo (hex, `rgb()`, `px`/`rem`/`ms` literal) — eso lo enforcea
32
+ `pnpm quality:styles` (ver la skill `ns-solid-style-contract`).
33
+
34
+ La conversión de un path de token a variable CSS es determinista
35
+ (`tokenPathToCssVariable` en `theme.utils.ts`): camelCase se separa en
36
+ kebab-case y los puntos se vuelven guiones, con el tier como prefijo:
37
+
38
+ ```
39
+ tier: 'component', path: 'button.background'
40
+ -> --ns-component-button-background
41
+ ```
42
+
43
+ ## Extender el preset — nunca copiarlo
44
+
45
+ ```ts
46
+ import { Arvisfy, defineNsPreset } from '@ng-solid/design-system';
47
+
48
+ export const ProductTheme = defineNsPreset(Arvisfy, {
49
+ name: 'product',
50
+ primitive: {
51
+ color: { brand: { 500: '#0052ff' } }
52
+ },
53
+ semantic: {
54
+ colorScheme: {
55
+ light: { color: { primary: '{primitive.color.brand.500}' } },
56
+ dark: { color: { primary: '{primitive.color.brand.500}' } }
57
+ }
58
+ },
59
+ components: {
60
+ button: { borderColor: '{semantic.color.primary}' }
61
+ }
62
+ });
63
+ ```
64
+
65
+ `defineNsPreset(base, overrides)` hace un merge profundo (`overrides` gana
66
+ por clave) y llama a `validateNsPreset` sobre el resultado antes de
67
+ devolverlo. `NsThemeService` también valida el preset activo en su
68
+ constructor y en cada `setPreset`.
69
+
70
+ ## Reglas que aplica `validateNsPreset`
71
+
72
+ Extraídas de `packages/design-system/src/lib/theme/utils/theme.utils.ts`:
73
+
74
+ - **Nombre del preset**: debe matchear `/^[a-z][a-z0-9-]*$/i` (falla si no).
75
+ - **Claves de token**: cada segmento del path debe matchear
76
+ `/^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/` — sin guiones ni mayúsculas iniciales.
77
+ - **Valores primitivos seguros**: ningún string primitivo puede contener
78
+ `;`, `{` o `}`, ni estar vacío.
79
+ - **Referencias `semantic.*`**: todo valor de `semantic.common` /
80
+ `semantic.colorScheme.{light,dark}` debe tener la forma exacta
81
+ `{primitive.<path>}` y ese `<path>` debe existir en `primitive`.
82
+ - **Referencias `components.*`**: todo valor debe tener la forma
83
+ `{semantic.<path>}`, y ese `<path>` debe existir en `semantic.common`, o
84
+ en **ambos** `colorScheme.light` y `colorScheme.dark` a la vez — un token
85
+ de componente que sólo resuelve en un esquema es un preset inválido.
86
+
87
+ Cualquier violación lanza una excepción síncrona (`throw new Error(...)`),
88
+ no un warning: un preset inválido rompe el arranque de la app.
89
+
90
+ ## Cambiar de esquema en runtime
91
+
92
+ ```ts
93
+ import { inject } from '@angular/core';
94
+ import { NsThemeService } from '@ng-solid/design-system';
95
+
96
+ const theme = inject(NsThemeService);
97
+ theme.setScheme('dark'); // 'light' | 'dark' | 'system'
98
+ ```
99
+
100
+ - `'system'` delega en `prefers-color-scheme` y reacciona a cambios en vivo
101
+ vía `matchMedia('(prefers-color-scheme: dark)')`.
102
+ - Una elección explícita (`'light'` / `'dark'`) se persiste en
103
+ `localStorage` bajo la clave `ns-color-scheme` (configurable con
104
+ `provideNsTheme({ storageKey: '...' })`, o desactivable con
105
+ `storageKey: false`).
106
+ - `NsThemeService` expone `preset`, `colorScheme`, `resolvedColorScheme`
107
+ (siempre `'light' | 'dark'`, nunca `'system'`) y `scheme` como signals de
108
+ sólo lectura.
109
+ - Al aplicar el tema, el servicio escribe todas las custom properties en
110
+ `documentElement.style` y además `dataset.nsPreset` / `dataset.nsColorScheme`
111
+ y `style.colorScheme`, para que el navegador ajuste scrollbars/controles
112
+ nativos.
113
+
114
+ ## Configurar el provider
115
+
116
+ ```ts
117
+ provideNsTheme({
118
+ preset: Arvisfy,
119
+ colorScheme: 'system', // default
120
+ storageKey: 'ns-color-scheme', // default; false para desactivar persistencia
121
+ toast: { placement: 'top-end' } // default de NsToastService
122
+ });
123
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export { createNsSolidMcpServer, runNsSolidMcpServer } from './lib/server.js';
3
+ export type * from './lib/types.js';
package/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ export { createNsSolidMcpServer, runNsSolidMcpServer } from './lib/server.js';
3
+ import { realpathSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runNsSolidMcpServer } from './lib/server.js';
6
+ function realpathOrUndefined(path) {
7
+ if (!path)
8
+ return undefined;
9
+ try {
10
+ return realpathSync(path);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ async function main() {
17
+ const command = process.argv[2];
18
+ if (!command || command === 'serve') {
19
+ await runNsSolidMcpServer();
20
+ return;
21
+ }
22
+ if (command === 'install-skills') {
23
+ const { installSkills } = await import('./lib/install-skills.js');
24
+ await installSkills(process.argv.slice(3));
25
+ return;
26
+ }
27
+ console.error(`Unknown command "${command}". Usage: ng-solid-mcp [serve|install-skills]`);
28
+ process.exitCode = 1;
29
+ }
30
+ const executedPath = realpathOrUndefined(process.argv[1]);
31
+ const modulePath = realpathOrUndefined(fileURLToPath(import.meta.url));
32
+ if (executedPath && modulePath && executedPath === modulePath) {
33
+ main().catch((error) => {
34
+ console.error('Fatal error in ng-solid MCP server:', error);
35
+ process.exit(1);
36
+ });
37
+ }
38
+ //# sourceMappingURL=index.js.map
package/lib/data.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { IconsSnapshot, NsSnapshot } from './types.js';
2
+ /**
3
+ * Resolves the on-disk location of a data file (`design-system.json` / `icons.json`).
4
+ *
5
+ * Fallback chain (first `existsSync` wins):
6
+ * 1. `process.env.NG_SOLID_MCP_DATA` (directory override, useful for tests/dev)
7
+ * 2. `../data/<fileName>` relative to this file — matches the published layout,
8
+ * `dist/packages/mcp/lib/data.js` -> `dist/packages/mcp/data/<fileName>`
9
+ * 3. `../../data/<fileName>` relative to this file — matches the dev layout,
10
+ * `packages/mcp/src/lib/data.ts` -> `packages/mcp/data/<fileName>`
11
+ */
12
+ export declare function resolveSnapshotPath(fileName: string): string;
13
+ export declare function loadSnapshot(): NsSnapshot;
14
+ export declare function loadIcons(): IconsSnapshot;
15
+ export declare function textResult(text: string): {
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ };
21
+ export declare function jsonResult(value: unknown): {
22
+ content: {
23
+ type: "text";
24
+ text: string;
25
+ }[];
26
+ };
package/lib/data.js ADDED
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * Resolves the on-disk location of a data file (`design-system.json` / `icons.json`).
6
+ *
7
+ * Fallback chain (first `existsSync` wins):
8
+ * 1. `process.env.NG_SOLID_MCP_DATA` (directory override, useful for tests/dev)
9
+ * 2. `../data/<fileName>` relative to this file — matches the published layout,
10
+ * `dist/packages/mcp/lib/data.js` -> `dist/packages/mcp/data/<fileName>`
11
+ * 3. `../../data/<fileName>` relative to this file — matches the dev layout,
12
+ * `packages/mcp/src/lib/data.ts` -> `packages/mcp/data/<fileName>`
13
+ */
14
+ export function resolveSnapshotPath(fileName) {
15
+ const candidates = [];
16
+ const envDir = process.env.NG_SOLID_MCP_DATA;
17
+ if (envDir) {
18
+ candidates.push(join(envDir, fileName));
19
+ }
20
+ candidates.push(fileURLToPath(new URL(`../data/${fileName}`, import.meta.url)));
21
+ candidates.push(fileURLToPath(new URL(`../../data/${fileName}`, import.meta.url)));
22
+ for (const candidate of candidates) {
23
+ if (existsSync(candidate)) {
24
+ return candidate;
25
+ }
26
+ }
27
+ throw new Error(`Could not resolve "${fileName}". Tried:\n${candidates.map((path) => ` - ${path}`).join('\n')}`);
28
+ }
29
+ let cachedSnapshot;
30
+ let cachedIcons;
31
+ export function loadSnapshot() {
32
+ if (!cachedSnapshot) {
33
+ const path = resolveSnapshotPath('design-system.json');
34
+ cachedSnapshot = JSON.parse(readFileSync(path, 'utf8'));
35
+ }
36
+ return cachedSnapshot;
37
+ }
38
+ export function loadIcons() {
39
+ if (!cachedIcons) {
40
+ const path = resolveSnapshotPath('icons.json');
41
+ cachedIcons = JSON.parse(readFileSync(path, 'utf8'));
42
+ }
43
+ return cachedIcons;
44
+ }
45
+ export function textResult(text) {
46
+ return { content: [{ type: 'text', text }] };
47
+ }
48
+ export function jsonResult(value) {
49
+ return textResult(JSON.stringify(value, null, 2));
50
+ }
51
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ export declare function installSkills(args: string[]): Promise<void>;
@@ -0,0 +1,159 @@
1
+ // Copies the resolved skill files (`data/skills/<name>/SKILL.md` -- markers
2
+ // already interpolated, see `scripts/lib/extract-guides.mjs`) into a
3
+ // consumer's own skills directory, defaulting to `./.agents/skills/`.
4
+ //
5
+ // `packages/mcp/skills/<name>/SKILL.md` is the *authored* source; this
6
+ // module never reads from it directly. It always distributes the resolved
7
+ // copy so a consumer never sees an unresolved `<!-- ns:... -->` marker.
8
+ import { existsSync } from 'node:fs';
9
+ import { cp, mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ const DEFAULT_TARGET_DIR = join('.', '.agents', 'skills');
13
+ function printUsage() {
14
+ console.error([
15
+ 'Usage: ng-solid-mcp install-skills [--dir <path>] [--list] [--force]',
16
+ '',
17
+ ' --dir <path> Target directory to copy skills into (default: ./.agents/skills)',
18
+ ' --list List the skills bundled with this package without copying',
19
+ ' --force Overwrite skill directories that already exist at the target'
20
+ ].join('\n'));
21
+ }
22
+ function parseArgs(args) {
23
+ const options = {
24
+ dir: DEFAULT_TARGET_DIR,
25
+ list: false,
26
+ force: false
27
+ };
28
+ for (let i = 0; i < args.length; i += 1) {
29
+ const arg = args[i];
30
+ if (arg === '--dir') {
31
+ const value = args[i + 1];
32
+ if (!value) {
33
+ console.error('install-skills: --dir requires a path argument');
34
+ return null;
35
+ }
36
+ options.dir = value;
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (arg === '--list') {
41
+ options.list = true;
42
+ continue;
43
+ }
44
+ if (arg === '--force') {
45
+ options.force = true;
46
+ continue;
47
+ }
48
+ if (arg === '--help' || arg === '-h') {
49
+ printUsage();
50
+ return null;
51
+ }
52
+ console.error(`install-skills: unknown argument "${arg}"`);
53
+ return null;
54
+ }
55
+ return options;
56
+ }
57
+ /**
58
+ * Resolves the on-disk location of the resolved skills directory.
59
+ *
60
+ * Fallback chain (first `existsSync` wins), mirroring `resolveSnapshotPath`
61
+ * in `data.ts`:
62
+ * 1. `process.env.NG_SOLID_MCP_DATA` (directory override)
63
+ * 2. `../data/skills` relative to this file -- published layout,
64
+ * `dist/packages/mcp/lib/install-skills.js` -> `dist/packages/mcp/data/skills`
65
+ * 3. `../../data/skills` relative to this file -- dev layout,
66
+ * `packages/mcp/src/lib/install-skills.ts` -> `packages/mcp/data/skills`
67
+ */
68
+ function resolveSkillsSourceDir() {
69
+ const candidates = [];
70
+ const envDir = process.env.NG_SOLID_MCP_DATA;
71
+ if (envDir) {
72
+ candidates.push(join(envDir, 'skills'));
73
+ }
74
+ candidates.push(fileURLToPath(new URL('../data/skills', import.meta.url)));
75
+ candidates.push(fileURLToPath(new URL('../../data/skills', import.meta.url)));
76
+ for (const candidate of candidates) {
77
+ if (existsSync(candidate)) {
78
+ return candidate;
79
+ }
80
+ }
81
+ throw new Error(`Could not resolve the bundled skills directory. Tried:\n${candidates
82
+ .map((path) => ` - ${path}`)
83
+ .join('\n')}`);
84
+ }
85
+ async function listSkillNames(skillsSourceDir) {
86
+ const entries = await readdir(skillsSourceDir, { withFileTypes: true });
87
+ return entries
88
+ .filter((entry) => entry.isDirectory())
89
+ .map((entry) => entry.name)
90
+ .sort((a, b) => a.localeCompare(b));
91
+ }
92
+ async function readDescription(skillsSourceDir, name) {
93
+ try {
94
+ const content = await readFile(join(skillsSourceDir, name, 'SKILL.md'), 'utf8');
95
+ const match = /^description:\s*(.*)$/m.exec(content);
96
+ return match ? match[1].trim() : '';
97
+ }
98
+ catch {
99
+ return '';
100
+ }
101
+ }
102
+ async function pathExists(path) {
103
+ try {
104
+ await stat(path);
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ export async function installSkills(args) {
112
+ const options = parseArgs(args);
113
+ if (!options) {
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+ let skillsSourceDir;
118
+ try {
119
+ skillsSourceDir = resolveSkillsSourceDir();
120
+ }
121
+ catch (error) {
122
+ console.error(`install-skills: ${error.message}`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ const names = await listSkillNames(skillsSourceDir);
127
+ if (options.list) {
128
+ console.log(`Available skills (${names.length}):`);
129
+ for (const name of names) {
130
+ const description = await readDescription(skillsSourceDir, name);
131
+ console.log(description ? ` ${name} -- ${description}` : ` ${name}`);
132
+ }
133
+ return;
134
+ }
135
+ await mkdir(options.dir, { recursive: true });
136
+ let copied = 0;
137
+ let skipped = 0;
138
+ for (const name of names) {
139
+ const source = join(skillsSourceDir, name);
140
+ const target = join(options.dir, name);
141
+ const targetExists = await pathExists(target);
142
+ if (targetExists && !options.force) {
143
+ console.warn(`install-skills: "${target}" already exists, skipping (use --force to overwrite)`);
144
+ skipped += 1;
145
+ continue;
146
+ }
147
+ if (targetExists) {
148
+ await rm(target, { recursive: true, force: true });
149
+ }
150
+ await cp(source, target, { recursive: true });
151
+ console.log(`install-skills: copied ${name} -> ${target}`);
152
+ copied += 1;
153
+ }
154
+ console.log(`install-skills: ${copied} skill(s) copied, ${skipped} skipped, target "${options.dir}"`);
155
+ if (copied === 0 && skipped === 0) {
156
+ process.exitCode = 1;
157
+ }
158
+ }
159
+ //# sourceMappingURL=install-skills.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Angular-free port of `validateNsPreset`
3
+ * (`packages/design-system/src/lib/theme/utils/theme.utils.ts`).
4
+ *
5
+ * The real function throws on the first violation and requires the live `NsThemePreset`
6
+ * TypeScript types. This port instead walks a plain JS object (typically parsed from JSON an
7
+ * agent pastes) and collects every violation, reusing the rule ids/messages/patterns already
8
+ * captured in `snapshot.theme.rules` so the two never drift apart.
9
+ */
10
+ import type { ThemeRuleDoc } from './types.js';
11
+ export type PresetValidationMode = 'full' | 'overrides';
12
+ export interface PresetViolation {
13
+ ruleId: string;
14
+ severity: 'error' | 'warning';
15
+ path: string;
16
+ message: string;
17
+ }
18
+ export interface PresetValidationResult {
19
+ valid: boolean;
20
+ violations: PresetViolation[];
21
+ }
22
+ export interface PresetLike {
23
+ name?: unknown;
24
+ primitive?: unknown;
25
+ semantic?: {
26
+ common?: unknown;
27
+ colorScheme?: {
28
+ light?: unknown;
29
+ dark?: unknown;
30
+ };
31
+ };
32
+ components?: unknown;
33
+ }
34
+ /**
35
+ * Validates a plain-object preset (or a partial override tree) against the ng-solid theme rules.
36
+ *
37
+ * `mode: 'full'` expects the complete three-tier shape (`primitive` / `semantic.common` /
38
+ * `semantic.colorScheme.{light,dark}` / `components`) and validates every rule, including
39
+ * cross-tier reference resolution. `mode: 'overrides'` treats the input as a
40
+ * `NsThemePresetOverrides`-shaped partial tree: only present tiers are checked, `preset-name`
41
+ * is skipped when `name` is absent, and reference checks are skipped for tiers the override
42
+ * doesn't touch (an override can legitimately reference a base-preset token it doesn't redefine).
43
+ */
44
+ export declare function validatePreset(preset: PresetLike, rules: ThemeRuleDoc[], mode?: PresetValidationMode): PresetValidationResult;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Angular-free port of `validateNsPreset`
3
+ * (`packages/design-system/src/lib/theme/utils/theme.utils.ts`).
4
+ *
5
+ * The real function throws on the first violation and requires the live `NsThemePreset`
6
+ * TypeScript types. This port instead walks a plain JS object (typically parsed from JSON an
7
+ * agent pastes) and collects every violation, reusing the rule ids/messages/patterns already
8
+ * captured in `snapshot.theme.rules` so the two never drift apart.
9
+ */
10
+ function parseRegexLiteral(literal) {
11
+ const match = /^\/(.*)\/([a-z]*)$/.exec(literal);
12
+ if (!match) {
13
+ throw new Error(`Not a serialized RegExp literal: ${literal}`);
14
+ }
15
+ return new RegExp(match[1], match[2]);
16
+ }
17
+ function ruleFrom(rules, id) {
18
+ const rule = rules.find((candidate) => candidate.id === id);
19
+ if (!rule) {
20
+ throw new Error(`theme rule snapshot is missing rule "${id}"`);
21
+ }
22
+ return rule;
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ }
27
+ /**
28
+ * Same recursive flatten as the real `flattenTokens` -- map keys are the bare dotted path
29
+ * within this tier (e.g. `"color.white"`, never `"primitive.color.white"`), because that is
30
+ * exactly what a `{primitive.color.white}` / `{semantic.x}` reference target must match.
31
+ * `reportPrefix` (e.g. `"primitive."`) is used only to build human-readable violation paths.
32
+ */
33
+ function flattenTokens(tree, tokenKeyPattern, tokenKeyRule, reportPrefix, violations) {
34
+ const tokens = new Map();
35
+ if (!isRecord(tree))
36
+ return tokens;
37
+ const visit = (value, path) => {
38
+ for (const [key, token] of Object.entries(value)) {
39
+ if (!tokenKeyPattern.test(key)) {
40
+ violations.push({
41
+ ruleId: tokenKeyRule.id,
42
+ severity: tokenKeyRule.severity,
43
+ path: `${reportPrefix}${path}${key}`,
44
+ message: `${tokenKeyRule.message} -- got "${key}".`
45
+ });
46
+ continue;
47
+ }
48
+ const nextPath = path ? `${path}${key}` : key;
49
+ if (isRecord(token)) {
50
+ visit(token, `${nextPath}.`);
51
+ }
52
+ else {
53
+ tokens.set(nextPath, token);
54
+ }
55
+ }
56
+ };
57
+ visit(tree, '');
58
+ return tokens;
59
+ }
60
+ function validateReferences(tokens, pattern, targets, rule, reportPrefix, violations) {
61
+ for (const [path, value] of tokens) {
62
+ const reportPath = `${reportPrefix}${path}`;
63
+ if (typeof value !== 'string') {
64
+ violations.push({
65
+ ruleId: rule.id,
66
+ severity: rule.severity,
67
+ path: reportPath,
68
+ message: `${rule.message} -- "${reportPath}" is not a string reference.`
69
+ });
70
+ continue;
71
+ }
72
+ const target = pattern.exec(value)?.[1];
73
+ if (!target || !targets.has(target)) {
74
+ violations.push({
75
+ ruleId: rule.id,
76
+ severity: rule.severity,
77
+ path: reportPath,
78
+ message: `${rule.message} -- invalid reference at "${reportPath}": ${value}.`
79
+ });
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Validates a plain-object preset (or a partial override tree) against the ng-solid theme rules.
85
+ *
86
+ * `mode: 'full'` expects the complete three-tier shape (`primitive` / `semantic.common` /
87
+ * `semantic.colorScheme.{light,dark}` / `components`) and validates every rule, including
88
+ * cross-tier reference resolution. `mode: 'overrides'` treats the input as a
89
+ * `NsThemePresetOverrides`-shaped partial tree: only present tiers are checked, `preset-name`
90
+ * is skipped when `name` is absent, and reference checks are skipped for tiers the override
91
+ * doesn't touch (an override can legitimately reference a base-preset token it doesn't redefine).
92
+ */
93
+ export function validatePreset(preset, rules, mode = 'full') {
94
+ const violations = [];
95
+ const tokenKeyRule = ruleFrom(rules, 'token-key');
96
+ const presetNameRule = ruleFrom(rules, 'preset-name');
97
+ const unsafeValueRule = ruleFrom(rules, 'unsafe-css-value');
98
+ const primitiveRefRule = ruleFrom(rules, 'primitive-reference');
99
+ const semanticRefRule = ruleFrom(rules, 'semantic-reference');
100
+ const bothSchemesRule = ruleFrom(rules, 'component-token-both-schemes');
101
+ if (mode === 'full' || preset.name !== undefined) {
102
+ const namePattern = parseRegexLiteral(presetNameRule.pattern ?? '/^[a-z][a-z0-9-]*$/i');
103
+ if (typeof preset.name !== 'string' || !namePattern.test(preset.name)) {
104
+ violations.push({
105
+ ruleId: presetNameRule.id,
106
+ severity: presetNameRule.severity,
107
+ path: 'name',
108
+ message: `${presetNameRule.message} -- got ${JSON.stringify(preset.name ?? null)}.`
109
+ });
110
+ }
111
+ }
112
+ const unsafePattern = parseRegexLiteral(unsafeValueRule.pattern ?? '/[;{}]/');
113
+ const primitives = flattenTokens(preset.primitive, /^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/, tokenKeyRule, 'primitive.', violations);
114
+ for (const [path, value] of primitives) {
115
+ if (typeof value === 'string' &&
116
+ (unsafePattern.test(value) || value.trim().length === 0)) {
117
+ violations.push({
118
+ ruleId: unsafeValueRule.id,
119
+ severity: unsafeValueRule.severity,
120
+ path,
121
+ message: `${unsafeValueRule.message} -- got ${JSON.stringify(value)}.`
122
+ });
123
+ }
124
+ }
125
+ const semanticCommon = flattenTokens(preset.semantic?.common, /^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/, tokenKeyRule, 'semantic.', violations);
126
+ const semanticLight = flattenTokens(preset.semantic?.colorScheme?.light, /^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/, tokenKeyRule, 'semantic.', violations);
127
+ const semanticDark = flattenTokens(preset.semantic?.colorScheme?.dark, /^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/, tokenKeyRule, 'semantic.', violations);
128
+ const primitiveRefPattern = parseRegexLiteral(primitiveRefRule.pattern ?? '/^\\{primitive\\.([a-zA-Z0-9.]+)\\}$/');
129
+ const havePrimitives = mode === 'full' || primitives.size > 0;
130
+ if (havePrimitives) {
131
+ for (const tokens of [semanticCommon, semanticLight, semanticDark]) {
132
+ if (tokens.size > 0) {
133
+ validateReferences(tokens, primitiveRefPattern, primitives, primitiveRefRule, 'semantic.', violations);
134
+ }
135
+ }
136
+ }
137
+ const components = flattenTokens(preset.components, /^(?:[a-z][a-zA-Z0-9]*|[0-9]+)$/, tokenKeyRule, 'components.', violations);
138
+ const semanticPaths = new Map([
139
+ ...semanticCommon,
140
+ ...semanticLight,
141
+ ...semanticDark
142
+ ]);
143
+ const semanticRefPattern = parseRegexLiteral(semanticRefRule.pattern ?? '/^\\{semantic\\.([a-zA-Z0-9.]+)\\}$/');
144
+ const haveSemantics = mode === 'full' || semanticPaths.size > 0;
145
+ if (haveSemantics && components.size > 0) {
146
+ validateReferences(components, semanticRefPattern, semanticPaths, semanticRefRule, 'components.', violations);
147
+ for (const [path, value] of components) {
148
+ if (typeof value !== 'string')
149
+ continue;
150
+ const semanticPath = semanticRefPattern.exec(value)?.[1];
151
+ if (!semanticPath)
152
+ continue;
153
+ const inCommon = semanticCommon.has(semanticPath);
154
+ const inBothSchemes = semanticLight.has(semanticPath) && semanticDark.has(semanticPath);
155
+ if (!inCommon &&
156
+ !inBothSchemes &&
157
+ (mode === 'full' || semanticPaths.size > 0)) {
158
+ violations.push({
159
+ ruleId: bothSchemesRule.id,
160
+ severity: bothSchemesRule.severity,
161
+ path: `components.${path}`,
162
+ message: `${bothSchemesRule.message} -- "components.${path}" references "${semanticPath}".`
163
+ });
164
+ }
165
+ }
166
+ }
167
+ return {
168
+ valid: violations.every((violation) => violation.severity !== 'error'),
169
+ violations
170
+ };
171
+ }
172
+ //# sourceMappingURL=preset-validator.js.map
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerPrompts(server: McpServer): void;