@agiflowai/style-system 0.0.1

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.
@@ -0,0 +1,3099 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import path from "node:path";
4
+ import { TemplatesManagerService, log } from "@agiflowai/aicode-utils";
5
+ import { promises } from "node:fs";
6
+ import yaml from "js-yaml";
7
+ import postcss from "postcss";
8
+ import { glob } from "glob";
9
+ import { createHash } from "node:crypto";
10
+ import { loadCsf } from "@storybook/csf-tools";
11
+ import tailwindcss from "@tailwindcss/vite";
12
+ import { viteSingleFile } from "vite-plugin-singlefile";
13
+ import os from "node:os";
14
+ import { chromium, firefox, webkit } from "playwright";
15
+ import sharp from "sharp";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+
18
+ //#region src/config.ts
19
+ /**
20
+ * Default tags for identifying shared/design system components
21
+ */
22
+ const DEFAULT_SHARED_COMPONENT_TAGS = ["style-system"];
23
+ /**
24
+ * Default configuration for apps without style-system config
25
+ */
26
+ const DEFAULT_CONFIG = {
27
+ type: "tailwind",
28
+ themeProvider: "@agimonai/web-ui",
29
+ sharedComponentTags: DEFAULT_SHARED_COMPONENT_TAGS
30
+ };
31
+ /**
32
+ * Validate style-system configuration from project.json.
33
+ * Ensures required fields are present and have correct types.
34
+ *
35
+ * @param config - The config object to validate
36
+ * @param projectName - Project name for error messages
37
+ * @returns Validated DesignSystemConfig
38
+ * @throws Error if validation fails
39
+ */
40
+ function validateDesignSystemConfig(config, projectName) {
41
+ if (typeof config !== "object" || config === null) throw new Error(`[${projectName}] style-system config must be an object`);
42
+ const cfg = config;
43
+ if (!cfg.type || cfg.type !== "tailwind" && cfg.type !== "shadcn") throw new Error(`[${projectName}] style-system.type must be 'tailwind' or 'shadcn'`);
44
+ if (!cfg.themeProvider || typeof cfg.themeProvider !== "string") throw new Error(`[${projectName}] style-system.themeProvider is required and must be a string`);
45
+ if (cfg.tailwindConfig !== void 0 && typeof cfg.tailwindConfig !== "string") throw new Error(`[${projectName}] style-system.tailwindConfig must be a string`);
46
+ if (cfg.rootComponent !== void 0 && typeof cfg.rootComponent !== "string") throw new Error(`[${projectName}] style-system.rootComponent must be a string`);
47
+ if (cfg.cssFiles !== void 0) {
48
+ if (!Array.isArray(cfg.cssFiles) || !cfg.cssFiles.every((f) => typeof f === "string")) throw new Error(`[${projectName}] style-system.cssFiles must be an array of strings`);
49
+ }
50
+ if (cfg.componentLibrary !== void 0 && typeof cfg.componentLibrary !== "string") throw new Error(`[${projectName}] style-system.componentLibrary must be a string`);
51
+ if (cfg.themePath !== void 0 && typeof cfg.themePath !== "string") throw new Error(`[${projectName}] style-system.themePath must be a string`);
52
+ if (cfg.sharedComponentTags !== void 0) {
53
+ if (!Array.isArray(cfg.sharedComponentTags) || !cfg.sharedComponentTags.every((t) => typeof t === "string")) throw new Error(`[${projectName}] style-system.sharedComponentTags must be an array of strings`);
54
+ }
55
+ return config;
56
+ }
57
+ /**
58
+ * Read design system configuration from an app's project.json.
59
+ *
60
+ * @param appPath - Path to the app directory (relative or absolute)
61
+ * @returns Validated DesignSystemConfig
62
+ * @throws Error if appPath is invalid, project.json cannot be read, or config validation fails
63
+ */
64
+ async function getAppDesignSystemConfig(appPath) {
65
+ if (!appPath || typeof appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
66
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
67
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath);
68
+ const projectJsonPath = path.join(resolvedAppPath, "project.json");
69
+ try {
70
+ const content = await promises.readFile(projectJsonPath, "utf-8");
71
+ let projectJson;
72
+ try {
73
+ projectJson = JSON.parse(content);
74
+ } catch (parseError) {
75
+ throw new Error(`Invalid JSON in ${projectJsonPath}: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
76
+ }
77
+ if (typeof projectJson !== "object" || projectJson === null) throw new Error(`${projectJsonPath} must contain a JSON object`);
78
+ const project = projectJson;
79
+ const projectName = typeof project.name === "string" ? project.name : path.basename(resolvedAppPath);
80
+ if (project["style-system"]) {
81
+ const validatedConfig = validateDesignSystemConfig(project["style-system"], projectName);
82
+ log.info(`[Config] Loaded and validated style-system config for ${projectName}`);
83
+ return validatedConfig;
84
+ }
85
+ log.info(`[Config] No style-system config found for ${projectName}, using defaults`);
86
+ return DEFAULT_CONFIG;
87
+ } catch (error) {
88
+ throw new Error(`Failed to read style-system config from ${projectJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
89
+ }
90
+ }
91
+ /**
92
+ * Get shared component tags from toolkit.yaml or use defaults.
93
+ *
94
+ * Reads configuration from toolkit.yaml at workspace root.
95
+ * Falls back to DEFAULT_SHARED_COMPONENT_TAGS if not configured.
96
+ *
97
+ * @returns Array of tag names that identify shared components
98
+ */
99
+ async function getSharedComponentTags() {
100
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
101
+ const toolkitYamlPath = path.join(monorepoRoot, "toolkit.yaml");
102
+ try {
103
+ const content = await promises.readFile(toolkitYamlPath, "utf-8");
104
+ const config = yaml.load(content);
105
+ if (config?.["style-system"]?.sharedComponentTags?.length) {
106
+ const tags = config["style-system"].sharedComponentTags;
107
+ if (Array.isArray(tags) && tags.every((tag) => typeof tag === "string")) {
108
+ log.info(`[Config] Loaded sharedComponentTags from toolkit.yaml: ${tags.join(", ")}`);
109
+ return tags;
110
+ }
111
+ log.warn("[Config] sharedComponentTags in toolkit.yaml is not a valid string array, using defaults");
112
+ }
113
+ } catch (error) {
114
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
115
+ }
116
+ log.info(`[Config] Using default sharedComponentTags: ${DEFAULT_SHARED_COMPONENT_TAGS.join(", ")}`);
117
+ return DEFAULT_SHARED_COMPONENT_TAGS;
118
+ }
119
+ /**
120
+ * Get getCssClasses tool configuration from toolkit.yaml.
121
+ *
122
+ * Reads configuration from toolkit.yaml at workspace root under
123
+ * style-system.getCssClasses key.
124
+ *
125
+ * @returns GetCssClassesConfig or undefined if not configured
126
+ */
127
+ async function getGetCssClassesConfig() {
128
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
129
+ const toolkitYamlPath = path.join(monorepoRoot, "toolkit.yaml");
130
+ try {
131
+ const content = await promises.readFile(toolkitYamlPath, "utf-8");
132
+ const config = yaml.load(content);
133
+ if (config?.["style-system"]?.getCssClasses) {
134
+ const getCssClassesConfig = config["style-system"].getCssClasses;
135
+ if (getCssClassesConfig.customService !== void 0 && typeof getCssClassesConfig.customService !== "string") {
136
+ log.warn("[Config] style-system.getCssClasses.customService must be a string, ignoring");
137
+ return;
138
+ }
139
+ log.info(`[Config] Loaded getCssClasses config from toolkit.yaml`);
140
+ return getCssClassesConfig;
141
+ }
142
+ } catch (error) {
143
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
144
+ }
145
+ }
146
+ /**
147
+ * Get bundler service configuration from toolkit.yaml.
148
+ *
149
+ * Reads configuration from toolkit.yaml at workspace root under
150
+ * style-system.bundler key.
151
+ *
152
+ * @returns BundlerConfig or undefined if not configured
153
+ */
154
+ async function getBundlerConfig() {
155
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
156
+ const toolkitYamlPath = path.join(monorepoRoot, "toolkit.yaml");
157
+ try {
158
+ const content = await promises.readFile(toolkitYamlPath, "utf-8");
159
+ const config = yaml.load(content);
160
+ if (config?.["style-system"]?.bundler) {
161
+ const bundlerConfig = config["style-system"].bundler;
162
+ if (bundlerConfig.customService !== void 0 && typeof bundlerConfig.customService !== "string") {
163
+ log.warn("[Config] style-system.bundler.customService must be a string, ignoring");
164
+ return;
165
+ }
166
+ log.info(`[Config] Loaded bundler config from toolkit.yaml`);
167
+ return bundlerConfig;
168
+ }
169
+ } catch (error) {
170
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`);
171
+ }
172
+ }
173
+
174
+ //#endregion
175
+ //#region src/services/CssClasses/BaseCSSClassesService.ts
176
+ /**
177
+ * Abstract base class for CSS class extraction services.
178
+ *
179
+ * Subclasses must implement the `extractClasses` method to provide
180
+ * framework-specific CSS class extraction logic.
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * class MyCustomCSSService extends BaseCSSClassesService {
185
+ * async extractClasses(category: CSSClassCategory, themePath: string): Promise<CSSClassesResult> {
186
+ * // Custom extraction logic
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ var BaseCSSClassesService = class {
192
+ config;
193
+ /**
194
+ * Creates a new CSS classes service instance
195
+ * @param config - Style system configuration from toolkit.yaml
196
+ */
197
+ constructor(config) {
198
+ this.config = config;
199
+ }
200
+ /**
201
+ * Validate that the theme path exists and is readable.
202
+ * Can be overridden by subclasses for custom validation.
203
+ *
204
+ * @param themePath - Path to validate
205
+ * @throws Error if path is invalid or unreadable
206
+ */
207
+ async validateThemePath(themePath) {
208
+ try {
209
+ await promises.access(themePath);
210
+ } catch {
211
+ throw new Error(`Theme file not found or not readable: ${themePath}`);
212
+ }
213
+ }
214
+ };
215
+
216
+ //#endregion
217
+ //#region src/services/CssClasses/TailwindCSSClassesService.ts
218
+ /**
219
+ * Tailwind CSS class extraction service.
220
+ *
221
+ * Extracts CSS classes from Tailwind theme files by parsing CSS variables
222
+ * using postcss AST and generating corresponding utility class names.
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * const service = new TailwindCSSClassesService(config);
227
+ * const result = await service.extractClasses('colors', '/path/to/theme.css');
228
+ * console.log(result.classes.colors); // Array of color utility classes
229
+ * ```
230
+ */
231
+ var TailwindCSSClassesService = class extends BaseCSSClassesService {
232
+ /**
233
+ * Creates a new TailwindCSSClassesService instance
234
+ * @param config - Style system configuration from toolkit.yaml
235
+ */
236
+ constructor(config) {
237
+ super(config);
238
+ }
239
+ /**
240
+ * Get the CSS framework identifier
241
+ * @returns Framework identifier string 'tailwind'
242
+ */
243
+ getFrameworkId() {
244
+ return "tailwind";
245
+ }
246
+ /**
247
+ * Extract Tailwind CSS classes from a theme file.
248
+ *
249
+ * Uses postcss to parse the CSS AST and safely extract variable declarations,
250
+ * then generates corresponding Tailwind utility classes (e.g., bg-*, text-*, border-*).
251
+ *
252
+ * Note: If an unrecognized category is passed, the method returns a result with
253
+ * empty classes object. Use valid categories: 'colors', 'typography', 'spacing', 'effects', 'all'.
254
+ *
255
+ * @param category - Category filter ('colors', 'typography', 'spacing', 'effects', 'all')
256
+ * @param themePath - Absolute path to the theme CSS file
257
+ * @returns Promise resolving to extracted CSS classes organized by category
258
+ * @throws Error if theme file cannot be read or parsed
259
+ */
260
+ async extractClasses(category, themePath) {
261
+ try {
262
+ await this.validateThemePath(themePath);
263
+ const resolvedThemePath = path.resolve(themePath);
264
+ const themeContent = await promises.readFile(resolvedThemePath, "utf-8");
265
+ const variables = await this.extractVariablesWithPostCSS(themeContent);
266
+ return this.generateClassesFromVariables(variables, category);
267
+ } catch (error) {
268
+ throw new Error(`Failed to extract classes from theme file ${themePath}: ${error instanceof Error ? error.message : String(error)}`);
269
+ }
270
+ }
271
+ /**
272
+ * Extract CSS variables with their values from theme content using postcss AST.
273
+ *
274
+ * Walks the CSS AST to find all custom property declarations (--*),
275
+ * handling multi-line values, comments, and any CSS formatting.
276
+ *
277
+ * @param themeContent - Raw CSS content from theme file
278
+ * @returns Promise resolving to Map of variable names (without --) to their values
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * // Handles standard declarations
283
+ * // --color-primary: #3b82f6;
284
+ *
285
+ * // Handles multi-line declarations
286
+ * // --shadow-lg:
287
+ * // 0 10px 15px -3px rgba(0, 0, 0, 0.1),
288
+ * // 0 4px 6px -4px rgba(0, 0, 0, 0.1);
289
+ *
290
+ * // Handles compressed CSS
291
+ * // --color-primary:#3b82f6;--color-secondary:#10b981;
292
+ * ```
293
+ */
294
+ async extractVariablesWithPostCSS(themeContent) {
295
+ const variables = /* @__PURE__ */ new Map();
296
+ try {
297
+ postcss.parse(themeContent).walkDecls((decl) => {
298
+ if (decl.prop.startsWith("--")) {
299
+ const varName = decl.prop.slice(2);
300
+ const varValue = decl.value.trim();
301
+ if (!variables.has(varName)) variables.set(varName, varValue);
302
+ }
303
+ });
304
+ } catch (error) {
305
+ throw new Error(`Failed to parse CSS content: ${error instanceof Error ? error.message : String(error)}`);
306
+ }
307
+ return variables;
308
+ }
309
+ /**
310
+ * Generate utility classes with actual values from CSS variables.
311
+ *
312
+ * Maps CSS variable naming conventions to Tailwind utility classes:
313
+ * - color-* → bg-*, text-*, border-*, ring-*
314
+ * - sidebar* → bg-*, text-*, border-*
315
+ * - text-* → text-* (typography)
316
+ * - font-* → font-* (typography)
317
+ * - space-* → p-*, m-*, gap-*
318
+ * - shadow-* → shadow-*
319
+ *
320
+ * Note: If the variables Map is empty, returns a result with empty arrays
321
+ * for all requested categories.
322
+ *
323
+ * @param variables - Map of CSS variable names to values
324
+ * @param category - Category filter for which classes to generate
325
+ * @returns CSSClassesResult with organized classes by category
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * const variables = new Map([
330
+ * ['color-primary', '#3b82f6'],
331
+ * ['shadow-md', '0 4px 6px rgba(0,0,0,0.1)']
332
+ * ]);
333
+ * const result = generateClassesFromVariables(variables, 'colors');
334
+ * // Returns:
335
+ * // {
336
+ * // category: 'colors',
337
+ * // classes: {
338
+ * // colors: [
339
+ * // { class: 'bg-primary', value: '#3b82f6' },
340
+ * // { class: 'text-primary', value: '#3b82f6' },
341
+ * // { class: 'border-primary', value: '#3b82f6' },
342
+ * // { class: 'ring-primary', value: '#3b82f6' }
343
+ * // ]
344
+ * // },
345
+ * // totalClasses: 4
346
+ * // }
347
+ * ```
348
+ */
349
+ generateClassesFromVariables(variables, category) {
350
+ const colorClasses = [];
351
+ const typographyClasses = [];
352
+ const spacingClasses = [];
353
+ const effectsClasses = [];
354
+ for (const [varName, varValue] of variables.entries()) {
355
+ if (category === "all" || category === "colors") {
356
+ if (varName.startsWith("color-")) {
357
+ const colorName = varName.replace("color-", "");
358
+ colorClasses.push({
359
+ class: `bg-${colorName}`,
360
+ value: varValue
361
+ }, {
362
+ class: `text-${colorName}`,
363
+ value: varValue
364
+ }, {
365
+ class: `border-${colorName}`,
366
+ value: varValue
367
+ }, {
368
+ class: `ring-${colorName}`,
369
+ value: varValue
370
+ });
371
+ } else if (varName.startsWith("sidebar")) colorClasses.push({
372
+ class: `bg-${varName}`,
373
+ value: varValue
374
+ }, {
375
+ class: `text-${varName}`,
376
+ value: varValue
377
+ }, {
378
+ class: `border-${varName}`,
379
+ value: varValue
380
+ });
381
+ }
382
+ if (category === "all" || category === "typography") {
383
+ if (varName.startsWith("text-")) typographyClasses.push({
384
+ class: varName,
385
+ value: varValue
386
+ });
387
+ if (varName.startsWith("font-")) {
388
+ const fontName = varName.replace("font-", "");
389
+ if (fontName.startsWith("weight-")) typographyClasses.push({
390
+ class: `font-${fontName.replace("weight-", "")}`,
391
+ value: varValue
392
+ });
393
+ else typographyClasses.push({
394
+ class: `font-${fontName}`,
395
+ value: varValue
396
+ });
397
+ }
398
+ }
399
+ if (category === "all" || category === "spacing") {
400
+ if (varName.startsWith("space-")) {
401
+ const spaceName = varName.replace("space-", "");
402
+ const calcValue = `calc(var(--${varName}) * 1)`;
403
+ spacingClasses.push({
404
+ class: `p-${spaceName}`,
405
+ value: calcValue
406
+ }, {
407
+ class: `m-${spaceName}`,
408
+ value: calcValue
409
+ }, {
410
+ class: `gap-${spaceName}`,
411
+ value: calcValue
412
+ });
413
+ } else if (varName === "spacing") spacingClasses.push({
414
+ class: "space",
415
+ value: varValue
416
+ });
417
+ }
418
+ if (category === "all" || category === "effects") {
419
+ if (varName.startsWith("shadow-")) {
420
+ const shadowName = varName.replace("shadow-", "");
421
+ effectsClasses.push({
422
+ class: `shadow-${shadowName}`,
423
+ value: varValue
424
+ });
425
+ }
426
+ }
427
+ }
428
+ const result = {
429
+ category,
430
+ classes: {},
431
+ totalClasses: colorClasses.length + typographyClasses.length + spacingClasses.length + effectsClasses.length
432
+ };
433
+ if (category === "all" || category === "colors") result.classes.colors = colorClasses;
434
+ if (category === "all" || category === "typography") result.classes.typography = typographyClasses;
435
+ if (category === "all" || category === "spacing") result.classes.spacing = spacingClasses;
436
+ if (category === "all" || category === "effects") result.classes.effects = effectsClasses;
437
+ return result;
438
+ }
439
+ };
440
+
441
+ //#endregion
442
+ //#region src/services/CssClasses/types.ts
443
+ /**
444
+ * Default configuration values
445
+ */
446
+ const DEFAULT_STYLE_SYSTEM_CONFIG = { cssFramework: "tailwind" };
447
+
448
+ //#endregion
449
+ //#region src/services/CssClasses/CSSClassesServiceFactory.ts
450
+ /** Valid file extensions for custom service modules */
451
+ const VALID_SERVICE_EXTENSIONS$1 = [
452
+ ".ts",
453
+ ".js",
454
+ ".mjs",
455
+ ".cjs"
456
+ ];
457
+ /**
458
+ * Factory for creating CSS classes service instances.
459
+ *
460
+ * Supports built-in frameworks (tailwind) and custom service implementations
461
+ * loaded dynamically from user-specified paths.
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * const factory = new CSSClassesServiceFactory();
466
+ * const service = await factory.createService({ cssFramework: 'tailwind' });
467
+ * const classes = await service.extractClasses('colors', '/path/to/theme.css');
468
+ * ```
469
+ */
470
+ var CSSClassesServiceFactory = class {
471
+ /**
472
+ * Create a CSS classes service based on configuration.
473
+ *
474
+ * @param config - Style system configuration (defaults to tailwind)
475
+ * @returns Promise resolving to a CSS classes service instance
476
+ * @throws Error if framework is unknown or custom service cannot be loaded
477
+ */
478
+ async createService(config = {}) {
479
+ const resolvedConfig = {
480
+ ...DEFAULT_STYLE_SYSTEM_CONFIG,
481
+ ...config
482
+ };
483
+ if (resolvedConfig.customServicePath) return this.loadCustomService(resolvedConfig);
484
+ return this.createBuiltInService(resolvedConfig);
485
+ }
486
+ /**
487
+ * Create a built-in CSS classes service based on framework identifier.
488
+ *
489
+ * @param config - Resolved style system configuration
490
+ * @returns CSS classes service instance
491
+ * @throws Error if framework is not supported
492
+ */
493
+ createBuiltInService(config) {
494
+ switch (config.cssFramework) {
495
+ case "tailwind": return new TailwindCSSClassesService(config);
496
+ default: throw new Error(`Unsupported CSS framework: ${config.cssFramework}. Supported frameworks: tailwind. Use customServicePath to provide a custom implementation.`);
497
+ }
498
+ }
499
+ /**
500
+ * Load a custom CSS classes service from user-specified path.
501
+ *
502
+ * The custom service must export a class that extends BaseCSSClassesService.
503
+ *
504
+ * @param config - Configuration with customServicePath set
505
+ * @returns Promise resolving to custom service instance
506
+ * @throws Error if service cannot be loaded or is invalid
507
+ */
508
+ async loadCustomService(config) {
509
+ const servicePath = config.customServicePath;
510
+ if (!servicePath) throw new Error("customServicePath is required for custom service loading");
511
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
512
+ const resolvedPath = path.resolve(monorepoRoot, servicePath);
513
+ const normalizedWorkspaceRoot = path.resolve(monorepoRoot);
514
+ if (!resolvedPath.startsWith(normalizedWorkspaceRoot + path.sep)) throw new Error(`Security error: customServicePath "${servicePath}" resolves outside workspace root`);
515
+ const ext = path.extname(resolvedPath).toLowerCase();
516
+ if (!VALID_SERVICE_EXTENSIONS$1.includes(ext)) throw new Error(`Invalid file extension "${ext}" for customServicePath. Expected one of: ${VALID_SERVICE_EXTENSIONS$1.join(", ")}`);
517
+ log.info(`[CSSClassesServiceFactory] Loading custom CSS service from: ${resolvedPath}`);
518
+ try {
519
+ const customModule = await import(resolvedPath);
520
+ const ServiceClass = customModule.default || customModule.CSSClassesService || customModule.CustomCSSClassesService;
521
+ if (!ServiceClass) throw new Error(`Custom service module at ${resolvedPath} must export a default class, CSSClassesService, or CustomCSSClassesService that extends BaseCSSClassesService`);
522
+ const instance = new ServiceClass(config);
523
+ if (!(instance instanceof BaseCSSClassesService)) throw new Error(`Custom service at ${resolvedPath} must extend BaseCSSClassesService`);
524
+ log.info(`[CSSClassesServiceFactory] Custom CSS service loaded successfully`);
525
+ return instance;
526
+ } catch (error) {
527
+ if (error instanceof Error && error.message.includes("BaseCSSClassesService")) throw error;
528
+ throw new Error(`Failed to load custom CSS classes service from ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`);
529
+ }
530
+ }
531
+ };
532
+
533
+ //#endregion
534
+ //#region src/tools/GetCSSClassesTool.ts
535
+ /**
536
+ * Valid CSS class category values
537
+ */
538
+ const VALID_CATEGORIES = [
539
+ "colors",
540
+ "typography",
541
+ "spacing",
542
+ "effects",
543
+ "all"
544
+ ];
545
+ /**
546
+ * Type guard to validate category input
547
+ * @param value - Value to check
548
+ * @returns True if value is a valid CSSClassCategory
549
+ */
550
+ function isValidCategory(value) {
551
+ return VALID_CATEGORIES.includes(value);
552
+ }
553
+ /**
554
+ * MCP Tool for extracting CSS classes from theme files.
555
+ *
556
+ * Uses the CSSClassesServiceFactory to create the appropriate service
557
+ * based on configuration, supporting Tailwind and custom CSS frameworks.
558
+ *
559
+ * @example
560
+ * ```typescript
561
+ * const tool = new GetCSSClassesTool();
562
+ * const result = await tool.execute({ category: 'colors' });
563
+ * ```
564
+ */
565
+ var GetCSSClassesTool = class GetCSSClassesTool {
566
+ static TOOL_NAME = "get_css_classes";
567
+ static CSS_REUSE_INSTRUCTION = "IMPORTANT: Always reuse these existing CSS classes from the theme as much as possible instead of creating custom styles. This ensures design consistency and reduces CSS bloat.";
568
+ serviceFactory;
569
+ service = null;
570
+ defaultThemePath;
571
+ /**
572
+ * Creates a new GetCSSClassesTool instance
573
+ * @param defaultThemePath - Default path to theme CSS file (relative to workspace root)
574
+ */
575
+ constructor(defaultThemePath = "packages/frontend/web-theme/src/agimon-theme.css") {
576
+ this.serviceFactory = new CSSClassesServiceFactory();
577
+ this.defaultThemePath = defaultThemePath;
578
+ }
579
+ /**
580
+ * Returns the tool definition for MCP registration
581
+ * @returns Tool definition with name, description, and input schema
582
+ */
583
+ getDefinition() {
584
+ return {
585
+ name: GetCSSClassesTool.TOOL_NAME,
586
+ description: "Extract and return all supported CSS classes from the theme file. Call this tool BEFORE writing any component styles or class names to ensure you use existing theme classes.",
587
+ inputSchema: {
588
+ type: "object",
589
+ properties: {
590
+ category: {
591
+ type: "string",
592
+ enum: [
593
+ "colors",
594
+ "typography",
595
+ "spacing",
596
+ "effects",
597
+ "all"
598
+ ],
599
+ description: "Category filter: 'colors', 'typography', 'spacing', 'effects', 'all' (default)"
600
+ },
601
+ appPath: {
602
+ type: "string",
603
+ description: "Optional app path (relative or absolute) to read theme path from project.json style-system config (e.g., \"apps/agiflow-app\")"
604
+ }
605
+ },
606
+ additionalProperties: false
607
+ }
608
+ };
609
+ }
610
+ /**
611
+ * Executes the CSS class extraction
612
+ * @param input - Tool input parameters
613
+ * @returns CallToolResult with extracted CSS classes or error
614
+ */
615
+ async execute(input) {
616
+ try {
617
+ const category = input.category || "all";
618
+ if (!isValidCategory(category)) throw new Error(`Invalid category: '${category}'. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
619
+ const themePath = await this.resolveThemePath(input.appPath);
620
+ if (!this.service) {
621
+ const toolkitConfig = await getGetCssClassesConfig();
622
+ this.service = await this.serviceFactory.createService({ customServicePath: toolkitConfig?.customService });
623
+ }
624
+ const result = await this.service.extractClasses(category, themePath);
625
+ return { content: [{
626
+ type: "text",
627
+ text: `${GetCSSClassesTool.CSS_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
628
+ }] };
629
+ } catch (error) {
630
+ return {
631
+ content: [{
632
+ type: "text",
633
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
634
+ }],
635
+ isError: true
636
+ };
637
+ }
638
+ }
639
+ /**
640
+ * Resolves the theme file path based on app configuration or defaults.
641
+ *
642
+ * Resolution strategy:
643
+ * 1. If appPath provided, read themePath from project.json style-system config
644
+ * 2. themePath is resolved relative to the app directory (where project.json is)
645
+ * 3. Fall back to default theme path if not configured
646
+ *
647
+ * @param appPath - Optional app path to read config from
648
+ * @returns Absolute path to the theme file
649
+ */
650
+ async resolveThemePath(appPath) {
651
+ const workspaceRoot = TemplatesManagerService.getWorkspaceRootSync();
652
+ if (appPath) {
653
+ const config = await getAppDesignSystemConfig(appPath);
654
+ if (config.themePath) {
655
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(workspaceRoot, appPath);
656
+ return path.resolve(resolvedAppPath, config.themePath);
657
+ }
658
+ }
659
+ return path.resolve(workspaceRoot, this.defaultThemePath);
660
+ }
661
+ };
662
+
663
+ //#endregion
664
+ //#region src/services/StoriesIndexService/StoriesIndexService.ts
665
+ var StoriesIndexService = class {
666
+ componentIndex = /* @__PURE__ */ new Map();
667
+ monorepoRoot;
668
+ initialized = false;
669
+ /** Last initialization result for error reporting */
670
+ lastInitResult = null;
671
+ /**
672
+ * Creates a new StoriesIndexService instance
673
+ */
674
+ constructor() {
675
+ this.monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
676
+ }
677
+ /**
678
+ * Initialize the index by scanning all .stories files.
679
+ * @returns Initialization result with success/failure statistics
680
+ */
681
+ async initialize() {
682
+ if (this.initialized && this.lastInitResult) return this.lastInitResult;
683
+ log.info("[StoriesIndexService] Initializing story index...");
684
+ const storyFiles = await glob("**/*.stories.{ts,tsx}", {
685
+ cwd: this.monorepoRoot,
686
+ ignore: [
687
+ "**/node_modules/**",
688
+ "**/dist/**",
689
+ "**/.next/**",
690
+ "**/build/**"
691
+ ],
692
+ absolute: true
693
+ });
694
+ log.info(`[StoriesIndexService] Found ${storyFiles.length} story files`);
695
+ const failures = [];
696
+ let successCount = 0;
697
+ for (const filePath of storyFiles) try {
698
+ await this.indexStoryFile(filePath);
699
+ successCount++;
700
+ } catch (error) {
701
+ const errorMessage = error instanceof Error ? error.message : String(error);
702
+ log.error(`[StoriesIndexService] Error indexing ${filePath}: ${errorMessage}`);
703
+ failures.push({
704
+ filePath,
705
+ error: errorMessage
706
+ });
707
+ }
708
+ this.initialized = true;
709
+ this.lastInitResult = {
710
+ totalFiles: storyFiles.length,
711
+ successCount,
712
+ failureCount: failures.length,
713
+ failures
714
+ };
715
+ if (failures.length > 0) log.warn(`[StoriesIndexService] Indexed ${successCount}/${storyFiles.length} files successfully. ${failures.length} files failed.`);
716
+ else log.info(`[StoriesIndexService] Indexed ${this.componentIndex.size} components successfully`);
717
+ return this.lastInitResult;
718
+ }
719
+ /**
720
+ * Get the last initialization result.
721
+ * @returns Initialization result or null if not initialized
722
+ */
723
+ getLastInitResult() {
724
+ return this.lastInitResult;
725
+ }
726
+ /**
727
+ * Index a single story file using @storybook/csf-tools.
728
+ *
729
+ * Uses the official Storybook CSF parser which handles all CSF formats
730
+ * including TypeScript satisfies/as expressions.
731
+ */
732
+ async indexStoryFile(filePath) {
733
+ const content = await promises.readFile(filePath, "utf-8");
734
+ const fileHash = this.hashContent(content);
735
+ const csf = loadCsf(content, {
736
+ fileName: filePath,
737
+ makeTitle: (title) => title
738
+ });
739
+ await csf.parse();
740
+ if (!csf.meta?.title) {
741
+ log.warn(`[StoriesIndexService] No valid meta title in ${filePath}`);
742
+ return;
743
+ }
744
+ const stories = csf.stories.map((story) => story.name).filter((name) => !!name);
745
+ const tags = Array.isArray(csf.meta.tags) ? csf.meta.tags.filter((t) => typeof t === "string") : [];
746
+ const description = this.extractDescription(content, csf.meta);
747
+ const meta = {
748
+ title: csf.meta.title,
749
+ tags
750
+ };
751
+ const componentInfo = {
752
+ title: meta.title,
753
+ filePath,
754
+ fileHash,
755
+ tags,
756
+ stories,
757
+ meta,
758
+ description
759
+ };
760
+ this.componentIndex.set(meta.title, componentInfo);
761
+ }
762
+ /**
763
+ * Extract component description from file header JSDoc or meta.parameters.docs.description.
764
+ *
765
+ * Priority:
766
+ * 1. meta.parameters.docs.description.component (Storybook standard)
767
+ * 2. File header JSDoc comment (first block comment in file)
768
+ *
769
+ * @param content - Raw file content
770
+ * @param meta - Parsed meta object from csf-tools
771
+ * @returns Description string or undefined
772
+ */
773
+ extractDescription(content, meta) {
774
+ const docsDescription = (((meta?.parameters)?.docs)?.description)?.component;
775
+ if (typeof docsDescription === "string" && docsDescription.trim()) return docsDescription.trim();
776
+ const jsDocMatch = content.match(/^\s*\/\*\*\s*([\s\S]*?)\s*\*\//);
777
+ if (jsDocMatch?.[1]) {
778
+ const description = jsDocMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter((line) => !line.startsWith("@")).join(" ").replace(/\s+/g, " ").trim();
779
+ if (description) return description;
780
+ }
781
+ }
782
+ /**
783
+ * Hash file content for cache invalidation
784
+ */
785
+ hashContent(content) {
786
+ return createHash("sha256").update(content).digest("hex");
787
+ }
788
+ /**
789
+ * Get all components filtered by tags
790
+ * @param tags - Optional array of tags to filter by
791
+ * @returns Array of matching components
792
+ */
793
+ getComponentsByTags(tags) {
794
+ const components = Array.from(this.componentIndex.values());
795
+ if (!tags || tags.length === 0) return components;
796
+ return components.filter((component) => tags.some((tag) => component.tags.includes(tag)));
797
+ }
798
+ /**
799
+ * Get component by title
800
+ * @param title - Exact title to match (e.g., "Components/Button")
801
+ * @returns Component info or undefined
802
+ */
803
+ getComponentByTitle(title) {
804
+ return this.componentIndex.get(title);
805
+ }
806
+ /**
807
+ * Find component by partial name match
808
+ * @param name - Partial name to search for
809
+ * @returns First matching component or undefined
810
+ */
811
+ findComponentByName(name) {
812
+ const lowerName = name.toLowerCase();
813
+ for (const component of this.componentIndex.values()) if ((component.title.split("/").pop() || component.title).toLowerCase() === lowerName) return component;
814
+ for (const component of this.componentIndex.values()) if ((component.title.split("/").pop() || component.title).toLowerCase().includes(lowerName)) return component;
815
+ }
816
+ /**
817
+ * Refresh a specific file if it has changed
818
+ * @param filePath - Absolute path to story file
819
+ * @returns True if file was updated, false if unchanged
820
+ */
821
+ async refreshFile(filePath) {
822
+ const content = await promises.readFile(filePath, "utf-8");
823
+ const newHash = this.hashContent(content);
824
+ const existingComponent = Array.from(this.componentIndex.values()).find((c) => c.filePath === filePath);
825
+ if (existingComponent && existingComponent.fileHash === newHash) return false;
826
+ await this.indexStoryFile(filePath);
827
+ return true;
828
+ }
829
+ /**
830
+ * Get all indexed components
831
+ * @returns Array of all component info objects
832
+ */
833
+ getAllComponents() {
834
+ return Array.from(this.componentIndex.values());
835
+ }
836
+ /**
837
+ * Clear the index (useful for testing)
838
+ */
839
+ clear() {
840
+ this.componentIndex.clear();
841
+ this.initialized = false;
842
+ this.lastInitResult = null;
843
+ }
844
+ /**
845
+ * Get all unique tags from indexed components
846
+ * @returns Sorted array of unique tag names
847
+ */
848
+ getAllTags() {
849
+ const tagSet = /* @__PURE__ */ new Set();
850
+ for (const component of this.componentIndex.values()) for (const tag of component.tags) tagSet.add(tag);
851
+ return Array.from(tagSet).sort();
852
+ }
853
+ };
854
+
855
+ //#endregion
856
+ //#region src/services/AppComponentsService/types.ts
857
+ /**
858
+ * Default configuration values.
859
+ */
860
+ const DEFAULT_APP_COMPONENTS_CONFIG = { pageSize: 50 };
861
+
862
+ //#endregion
863
+ //#region src/services/AppComponentsService/AppComponentsService.ts
864
+ /**
865
+ * AppComponentsService handles listing app-specific and package components.
866
+ *
867
+ * Detects components by file path (within app directory) and resolves
868
+ * workspace dependencies to find package components.
869
+ *
870
+ * @example
871
+ * ```typescript
872
+ * const service = new AppComponentsService();
873
+ * const result = await service.listComponents({ appPath: 'apps/my-app' });
874
+ * // Returns: { app: 'my-app', appComponents: ['Button'], packageComponents: {...}, pagination: {...} }
875
+ * ```
876
+ */
877
+ var AppComponentsService = class {
878
+ config;
879
+ /**
880
+ * Creates a new AppComponentsService instance.
881
+ * @param config - Service configuration options
882
+ */
883
+ constructor(config = {}) {
884
+ this.config = {
885
+ ...DEFAULT_APP_COMPONENTS_CONFIG,
886
+ ...config
887
+ };
888
+ }
889
+ /**
890
+ * List app-specific and package components for a given application.
891
+ * @param input - Object containing appPath and optional cursor for pagination
892
+ * @returns Promise resolving to paginated component list
893
+ * @throws Error if input validation fails, app path does not exist, or stories index fails to initialize
894
+ */
895
+ async listComponents(input) {
896
+ if (!input.appPath || typeof input.appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
897
+ if (input.cursor !== void 0 && typeof input.cursor !== "string") throw new Error("cursor must be a string");
898
+ const { appPath, cursor } = input;
899
+ const { offset } = cursor ? this.decodeCursor(cursor) : { offset: 0 };
900
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
901
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath);
902
+ try {
903
+ await promises.access(resolvedAppPath);
904
+ } catch {
905
+ throw new Error(`App path does not exist: ${resolvedAppPath}`);
906
+ }
907
+ const appName = await this.getAppName(resolvedAppPath);
908
+ const workspaceDependencies = await this.getWorkspaceDependencies(resolvedAppPath);
909
+ log.info(`[AppComponentsService] Found ${workspaceDependencies.length} workspace dependencies for ${appName}`);
910
+ const packageMap = await this.buildPackageMap(monorepoRoot);
911
+ const storiesIndex = new StoriesIndexService();
912
+ await storiesIndex.initialize();
913
+ const allComponents = storiesIndex.getAllComponents();
914
+ const { appComponentsArray, packageComponents, totalPackageComponents } = this.categorizeComponents(allComponents, resolvedAppPath, workspaceDependencies, packageMap);
915
+ const totalComponents = appComponentsArray.length + totalPackageComponents;
916
+ const { paginatedAppComponents, paginatedPackageComponents, totalReturned } = this.paginateComponents(appComponentsArray, packageComponents, offset);
917
+ const hasMore = offset + totalReturned < totalComponents;
918
+ const result = {
919
+ app: appName,
920
+ appComponents: paginatedAppComponents,
921
+ packageComponents: paginatedPackageComponents,
922
+ pagination: {
923
+ offset,
924
+ pageSize: this.config.pageSize,
925
+ totalComponents,
926
+ hasMore
927
+ }
928
+ };
929
+ if (hasMore) result.nextCursor = this.encodeCursor(offset + totalReturned);
930
+ log.info(`[AppComponentsService] Page ${Math.floor(offset / this.config.pageSize) + 1}: Returned ${totalReturned} of ${totalComponents} total components (hasMore: ${hasMore})`);
931
+ return result;
932
+ }
933
+ /**
934
+ * Get app name from project.json.
935
+ * @param resolvedAppPath - Absolute path to the app directory
936
+ * @returns App name from project.json or directory basename as fallback
937
+ */
938
+ async getAppName(resolvedAppPath) {
939
+ const projectJsonPath = path.join(resolvedAppPath, "project.json");
940
+ let appName = path.basename(resolvedAppPath);
941
+ try {
942
+ appName = JSON.parse(await promises.readFile(projectJsonPath, "utf-8")).name || appName;
943
+ } catch (error) {
944
+ log.warn(`[AppComponentsService] Could not read project.json for ${resolvedAppPath}:`, error);
945
+ }
946
+ return appName;
947
+ }
948
+ /**
949
+ * Get workspace dependencies from package.json.
950
+ * @param resolvedAppPath - Absolute path to the app directory
951
+ * @returns Array of workspace dependency package names
952
+ */
953
+ async getWorkspaceDependencies(resolvedAppPath) {
954
+ const packageJsonPath = path.join(resolvedAppPath, "package.json");
955
+ try {
956
+ const packageJson = JSON.parse(await promises.readFile(packageJsonPath, "utf-8"));
957
+ const allDeps = {
958
+ ...packageJson.dependencies,
959
+ ...packageJson.devDependencies
960
+ };
961
+ return Object.entries(allDeps).filter(([_name, version]) => typeof version === "string" && version.startsWith("workspace:")).map(([name]) => name);
962
+ } catch (error) {
963
+ log.warn(`[AppComponentsService] Could not read package.json for ${resolvedAppPath}:`, error);
964
+ return [];
965
+ }
966
+ }
967
+ /**
968
+ * Find all package.json files in the monorepo and build a map of package name → directory path.
969
+ * @param monorepoRoot - The root directory of the monorepo
970
+ * @returns Promise resolving to a Map where keys are package names and values are directory paths
971
+ * @throws Error if scanning for package.json files fails
972
+ */
973
+ async buildPackageMap(monorepoRoot) {
974
+ const packageMap = /* @__PURE__ */ new Map();
975
+ let packageJsonFiles;
976
+ try {
977
+ packageJsonFiles = await glob("**/package.json", {
978
+ cwd: monorepoRoot,
979
+ ignore: [
980
+ "**/node_modules/**",
981
+ "**/dist/**",
982
+ "**/.next/**",
983
+ "**/build/**"
984
+ ],
985
+ absolute: true
986
+ });
987
+ } catch (error) {
988
+ throw new Error(`Failed to scan for package.json files in ${monorepoRoot}: ${error instanceof Error ? error.message : String(error)}`);
989
+ }
990
+ for (const pkgJsonPath of packageJsonFiles) try {
991
+ const content = await promises.readFile(pkgJsonPath, "utf-8");
992
+ const pkgJson = JSON.parse(content);
993
+ if (pkgJson.name) {
994
+ const pkgDir = path.dirname(pkgJsonPath);
995
+ packageMap.set(pkgJson.name, pkgDir);
996
+ }
997
+ } catch (error) {
998
+ log.debug(`[AppComponentsService] Skipping invalid package.json at ${pkgJsonPath}:`, error);
999
+ }
1000
+ return packageMap;
1001
+ }
1002
+ /**
1003
+ * Categorize components into app-specific and package components.
1004
+ * App components are detected by file path (within app directory).
1005
+ * Package components are matched to workspace dependencies.
1006
+ *
1007
+ * @param allComponents - All components from stories index
1008
+ * @param resolvedAppPath - Absolute path to the app directory
1009
+ * @param workspaceDependencies - List of workspace dependency package names
1010
+ * @param packageMap - Map of package name to directory path
1011
+ * @returns Categorized components with totals
1012
+ */
1013
+ categorizeComponents(allComponents, resolvedAppPath, workspaceDependencies, packageMap) {
1014
+ const appComponentsMap = /* @__PURE__ */ new Map();
1015
+ const packageComponentsMap = {};
1016
+ for (const component of allComponents) {
1017
+ const componentName = component.title.split("/").pop() || component.title;
1018
+ const componentBrief = {
1019
+ name: componentName,
1020
+ ...component.description && { description: component.description }
1021
+ };
1022
+ if (component.filePath.startsWith(resolvedAppPath)) appComponentsMap.set(componentName, componentBrief);
1023
+ for (const dep of workspaceDependencies) {
1024
+ const packageDir = packageMap.get(dep);
1025
+ if (packageDir && component.filePath.startsWith(packageDir)) {
1026
+ if (!packageComponentsMap[dep]) packageComponentsMap[dep] = /* @__PURE__ */ new Map();
1027
+ packageComponentsMap[dep].set(componentName, componentBrief);
1028
+ }
1029
+ }
1030
+ }
1031
+ const packageComponents = {};
1032
+ for (const [pkg, componentsMap] of Object.entries(packageComponentsMap)) packageComponents[pkg] = Array.from(componentsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
1033
+ return {
1034
+ appComponentsArray: Array.from(appComponentsMap.values()).sort((a, b) => a.name.localeCompare(b.name)),
1035
+ packageComponents,
1036
+ totalPackageComponents: Object.values(packageComponents).reduce((sum, arr) => sum + arr.length, 0)
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Apply pagination to component lists.
1041
+ *
1042
+ * Pagination strategy:
1043
+ * 1. First, fill page with app components starting from offset
1044
+ * 2. Then, fill remaining page space with package components in order
1045
+ * 3. Track total returned for cursor calculation
1046
+ *
1047
+ * @param appComponentsArray - Sorted array of app component briefs
1048
+ * @param packageComponents - Record of package name to component briefs
1049
+ * @param offset - Current pagination offset (0-indexed)
1050
+ * @returns Paginated components and total returned count
1051
+ */
1052
+ paginateComponents(appComponentsArray, packageComponents, offset) {
1053
+ const totalPackageComponents = Object.values(packageComponents).reduce((sum, arr) => sum + arr.length, 0);
1054
+ const totalComponents = appComponentsArray.length + totalPackageComponents;
1055
+ const paginatedAppComponents = appComponentsArray.slice(offset, offset + this.config.pageSize);
1056
+ const remainingSpace = this.config.pageSize - paginatedAppComponents.length;
1057
+ const paginatedPackageComponents = {};
1058
+ let packageComponentsConsumed = 0;
1059
+ if (remainingSpace > 0 && offset < totalComponents) {
1060
+ const packageOffset = Math.max(0, offset - appComponentsArray.length);
1061
+ let itemsToTake = remainingSpace;
1062
+ let currentOffset = packageOffset;
1063
+ for (const [pkg, components] of Object.entries(packageComponents)) {
1064
+ if (itemsToTake <= 0) break;
1065
+ if (currentOffset < components.length) {
1066
+ const sliced = components.slice(currentOffset, currentOffset + itemsToTake);
1067
+ paginatedPackageComponents[pkg] = sliced;
1068
+ packageComponentsConsumed += sliced.length;
1069
+ itemsToTake -= sliced.length;
1070
+ currentOffset = 0;
1071
+ } else currentOffset -= components.length;
1072
+ }
1073
+ }
1074
+ return {
1075
+ paginatedAppComponents,
1076
+ paginatedPackageComponents,
1077
+ totalReturned: paginatedAppComponents.length + packageComponentsConsumed
1078
+ };
1079
+ }
1080
+ /**
1081
+ * Encode pagination state into a base64 cursor string.
1082
+ * @param offset - The current offset position in the component list
1083
+ * @returns Base64-encoded cursor string for the next page
1084
+ */
1085
+ encodeCursor(offset) {
1086
+ return Buffer.from(JSON.stringify({ offset })).toString("base64");
1087
+ }
1088
+ /**
1089
+ * Decode cursor string into pagination state.
1090
+ * @param cursor - Base64-encoded cursor string from previous response
1091
+ * @returns Object with offset position; defaults to 0 if cursor is invalid
1092
+ */
1093
+ decodeCursor(cursor) {
1094
+ try {
1095
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
1096
+ return { offset: JSON.parse(decoded).offset || 0 };
1097
+ } catch {
1098
+ log.debug("[AppComponentsService] Invalid cursor, resetting to offset 0");
1099
+ return { offset: 0 };
1100
+ }
1101
+ }
1102
+ };
1103
+
1104
+ //#endregion
1105
+ //#region src/services/BundlerService/BaseBundlerService.ts
1106
+ /**
1107
+ * Abstract base class for bundler service implementations.
1108
+ *
1109
+ * Subclasses must implement the abstract methods to provide
1110
+ * bundler-specific (Vite, Webpack, etc.) and framework-specific
1111
+ * (React, Vue, etc.) component rendering logic.
1112
+ *
1113
+ * @example
1114
+ * ```typescript
1115
+ * class MyCustomBundlerService extends BaseBundlerService {
1116
+ * async startDevServer(appPath: string): Promise<DevServerResult> {
1117
+ * // Custom dev server logic
1118
+ * }
1119
+ * // ... implement other abstract methods
1120
+ * }
1121
+ * ```
1122
+ */
1123
+ var BaseBundlerService = class {
1124
+ config;
1125
+ /**
1126
+ * Creates a new bundler service instance.
1127
+ * @param config - Service configuration options
1128
+ */
1129
+ constructor(config = {}) {
1130
+ this.config = config;
1131
+ }
1132
+ };
1133
+
1134
+ //#endregion
1135
+ //#region src/services/BundlerService/ViteReactBundlerService.ts
1136
+ /**
1137
+ * Maximum age for story configs in milliseconds (5 minutes).
1138
+ * Configs older than this are cleaned up to prevent memory leaks.
1139
+ */
1140
+ const STORY_CONFIG_MAX_AGE_MS = 300 * 1e3;
1141
+ /**
1142
+ * Maximum number of story configs to keep in memory.
1143
+ * Oldest configs are removed when this limit is exceeded.
1144
+ */
1145
+ const STORY_CONFIG_MAX_COUNT = 100;
1146
+ /**
1147
+ * Maximum size for serialized args in bytes (1MB).
1148
+ * Prevents memory issues with extremely large payloads.
1149
+ */
1150
+ const MAX_ARGS_SIZE_BYTES = 1024 * 1024;
1151
+ /**
1152
+ * Valid pattern for story names.
1153
+ * Allows alphanumeric characters, underscores, hyphens, and spaces.
1154
+ */
1155
+ const VALID_STORY_NAME_PATTERN = /^[a-zA-Z0-9_\- ]+$/;
1156
+ /**
1157
+ * Valid pattern for component paths.
1158
+ * Must end with .stories.tsx, .stories.ts, .stories.jsx, or .stories.js
1159
+ */
1160
+ const VALID_COMPONENT_PATH_PATTERN = /\.stories\.(tsx?|jsx?)$/;
1161
+ /**
1162
+ * Validates a story name to prevent code injection.
1163
+ * @param storyName - The story name to validate
1164
+ * @throws Error if the story name contains invalid characters
1165
+ */
1166
+ function validateStoryName(storyName) {
1167
+ if (!storyName || typeof storyName !== "string") throw new Error("Story name is required and must be a string");
1168
+ if (!VALID_STORY_NAME_PATTERN.test(storyName)) throw new Error(`Story name "${storyName}" contains invalid characters. Only alphanumeric characters, underscores, hyphens, and spaces are allowed.`);
1169
+ }
1170
+ /**
1171
+ * Validates a component path to prevent code injection.
1172
+ * @param componentPath - The component path to validate
1173
+ * @throws Error if the component path is invalid
1174
+ */
1175
+ function validateComponentPath(componentPath) {
1176
+ if (!componentPath || typeof componentPath !== "string") throw new Error("Component path is required and must be a string");
1177
+ if (!VALID_COMPONENT_PATH_PATTERN.test(componentPath)) throw new Error(`Component path "${componentPath}" must be a valid Storybook story file (*.stories.tsx, *.stories.ts, *.stories.jsx, or *.stories.js)`);
1178
+ if (componentPath.includes("..")) throw new Error("Component path must not contain path traversal sequences (..)");
1179
+ }
1180
+ /**
1181
+ * Validates args object size to prevent memory issues.
1182
+ * @param args - The args object to validate
1183
+ * @throws Error if args exceed size limit
1184
+ */
1185
+ function validateArgsSize(args) {
1186
+ const serialized = JSON.stringify(args);
1187
+ const sizeBytes = Buffer.byteLength(serialized, "utf-8");
1188
+ if (sizeBytes > MAX_ARGS_SIZE_BYTES) throw new Error(`Args payload too large: ${sizeBytes} bytes exceeds maximum of ${MAX_ARGS_SIZE_BYTES} bytes (1MB)`);
1189
+ }
1190
+ /**
1191
+ * Validates CSS file paths to prevent path traversal attacks.
1192
+ * @param cssFiles - Array of CSS file paths to validate
1193
+ * @param workspaceRoot - The workspace root directory
1194
+ * @throws Error if any path is invalid or escapes workspace
1195
+ */
1196
+ function validateCssFiles(cssFiles, workspaceRoot) {
1197
+ for (const cssFile of cssFiles) {
1198
+ if (typeof cssFile !== "string") throw new Error("CSS file path must be a string");
1199
+ if (cssFile.includes("..")) throw new Error(`CSS file path "${cssFile}" must not contain path traversal sequences (..)`);
1200
+ if (path.isAbsolute(cssFile)) {
1201
+ if (!path.normalize(cssFile).startsWith(workspaceRoot)) throw new Error(`CSS file path "${cssFile}" must be within workspace boundaries`);
1202
+ }
1203
+ const ext = path.extname(cssFile).toLowerCase();
1204
+ if (![
1205
+ ".css",
1206
+ ".scss",
1207
+ ".sass",
1208
+ ".less",
1209
+ ".pcss",
1210
+ ".postcss"
1211
+ ].includes(ext)) throw new Error(`CSS file "${cssFile}" must have a valid CSS extension (.css, .scss, .sass, .less, .pcss)`);
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Helper to create a Vite plugin that serves story entry files from memory.
1216
+ */
1217
+ function createStoryEntryPlugin(getStoryConfig, generateCode) {
1218
+ return {
1219
+ name: "vite-plugin-story-entry",
1220
+ enforce: "pre",
1221
+ resolveId(id) {
1222
+ if (id.includes("virtual:story-entry")) {
1223
+ log.debug(`[vite-plugin-story-entry] resolveId: ${id}`);
1224
+ return `\0${id.replace(/^\//, "")}`;
1225
+ }
1226
+ },
1227
+ load(id) {
1228
+ if (id.includes("virtual:story-entry")) {
1229
+ log.debug(`[vite-plugin-story-entry] load: ${id}`);
1230
+ const storyId = id.match(/id=([^&]+)/)?.[1];
1231
+ if (storyId) {
1232
+ const config = getStoryConfig(storyId);
1233
+ if (config) return generateCode(config);
1234
+ }
1235
+ throw new Error(`[Vite] Story config not found for id: ${storyId}`);
1236
+ }
1237
+ }
1238
+ };
1239
+ }
1240
+ /**
1241
+ * ViteReactBundlerService provides Vite + React bundling for component rendering.
1242
+ *
1243
+ * This is the default implementation of BaseBundlerService that uses Vite
1244
+ * as the bundler and React as the framework for rendering components.
1245
+ *
1246
+ * @example
1247
+ * ```typescript
1248
+ * const service = ViteReactBundlerService.getInstance();
1249
+ * await service.startDevServer('apps/my-app');
1250
+ * const { url } = await service.serveComponent({
1251
+ * componentPath: '/path/to/Button.stories.tsx',
1252
+ * storyName: 'Primary',
1253
+ * appPath: 'apps/my-app'
1254
+ * });
1255
+ * ```
1256
+ */
1257
+ var ViteReactBundlerService = class ViteReactBundlerService extends BaseBundlerService {
1258
+ static instance = null;
1259
+ server = null;
1260
+ monorepoRoot;
1261
+ serverUrl = null;
1262
+ serverPort = null;
1263
+ currentAppPath = null;
1264
+ storyConfigs = /* @__PURE__ */ new Map();
1265
+ /** Timestamps for when each story config was created, used for cleanup */
1266
+ storyConfigTimestamps = /* @__PURE__ */ new Map();
1267
+ /** Promise that resolves when server startup completes, prevents race conditions */
1268
+ serverStartPromise = null;
1269
+ /**
1270
+ * Creates a new ViteReactBundlerService instance.
1271
+ * Use getInstance() for singleton access.
1272
+ * @param config - Service configuration options
1273
+ */
1274
+ constructor(config = {}) {
1275
+ super(config);
1276
+ this.monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
1277
+ }
1278
+ /**
1279
+ * Get the singleton instance of ViteReactBundlerService.
1280
+ * Singleton pattern ensures only one dev server runs at a time,
1281
+ * preventing port conflicts and resource duplication.
1282
+ * @returns The singleton ViteReactBundlerService instance
1283
+ */
1284
+ static getInstance() {
1285
+ if (!ViteReactBundlerService.instance) ViteReactBundlerService.instance = new ViteReactBundlerService();
1286
+ return ViteReactBundlerService.instance;
1287
+ }
1288
+ /**
1289
+ * Reset the singleton instance.
1290
+ * This is primarily used in testing to ensure a fresh instance.
1291
+ * @example
1292
+ * ```typescript
1293
+ * afterEach(() => {
1294
+ * ViteReactBundlerService.resetInstance();
1295
+ * });
1296
+ * ```
1297
+ */
1298
+ static resetInstance() {
1299
+ ViteReactBundlerService.instance = null;
1300
+ }
1301
+ /**
1302
+ * Get the bundler identifier.
1303
+ * @returns The bundler ID string ('vite')
1304
+ */
1305
+ getBundlerId() {
1306
+ return "vite";
1307
+ }
1308
+ /**
1309
+ * Get the framework identifier.
1310
+ * @returns The framework ID string ('react')
1311
+ */
1312
+ getFrameworkId() {
1313
+ return "react";
1314
+ }
1315
+ /**
1316
+ * Get the current server URL.
1317
+ * @returns Server URL or null if not running
1318
+ */
1319
+ getServerUrl() {
1320
+ return this.serverUrl;
1321
+ }
1322
+ /**
1323
+ * Get the current server port.
1324
+ * @returns Server port or null if not running
1325
+ */
1326
+ getServerPort() {
1327
+ return this.serverPort;
1328
+ }
1329
+ /**
1330
+ * Check if the dev server is running.
1331
+ * @returns True if server is running
1332
+ */
1333
+ isServerRunning() {
1334
+ return this.server !== null && this.serverUrl !== null;
1335
+ }
1336
+ /**
1337
+ * Get the current app path being served.
1338
+ * @returns App path or null if not running
1339
+ */
1340
+ getCurrentAppPath() {
1341
+ return this.currentAppPath;
1342
+ }
1343
+ /**
1344
+ * Start a Vite dev server for hot reload and caching.
1345
+ * Handles concurrent calls by returning the same promise if server is already starting.
1346
+ * @param appPath - Absolute or relative path to the app directory
1347
+ * @returns Promise resolving to server URL and port
1348
+ * @throws Error if server fails to start
1349
+ */
1350
+ async startDevServer(appPath) {
1351
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(this.monorepoRoot, appPath);
1352
+ if (this.isServerRunning() && this.currentAppPath === resolvedAppPath) {
1353
+ log.info(`[ViteReactBundlerService] Server already running for ${resolvedAppPath}`);
1354
+ return {
1355
+ url: this.serverUrl,
1356
+ port: this.serverPort
1357
+ };
1358
+ }
1359
+ if (this.serverStartPromise) {
1360
+ log.info("[ViteReactBundlerService] Server start already in progress, waiting...");
1361
+ return this.serverStartPromise;
1362
+ }
1363
+ this.serverStartPromise = this.doStartDevServer(resolvedAppPath);
1364
+ try {
1365
+ return await this.serverStartPromise;
1366
+ } finally {
1367
+ this.serverStartPromise = null;
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Internal method that performs the actual server startup.
1372
+ * @param resolvedAppPath - Resolved absolute path to the app directory
1373
+ * @returns Promise resolving to server URL and port
1374
+ */
1375
+ async doStartDevServer(resolvedAppPath) {
1376
+ const tmpDir = path.join(resolvedAppPath, ".tmp");
1377
+ try {
1378
+ await promises.mkdir(tmpDir, { recursive: true });
1379
+ const { createServer: createServer$1 } = await import("vite");
1380
+ this.server = await createServer$1({
1381
+ root: tmpDir,
1382
+ base: "/",
1383
+ configFile: false,
1384
+ plugins: [tailwindcss(), createStoryEntryPlugin((id) => this.storyConfigs.get(id), (opts) => this.generateEntryFile(opts))],
1385
+ resolve: { alias: { "@": path.join(resolvedAppPath, "src") } },
1386
+ esbuild: {
1387
+ jsx: "automatic",
1388
+ jsxImportSource: "react"
1389
+ },
1390
+ server: {
1391
+ strictPort: false,
1392
+ open: false
1393
+ }
1394
+ });
1395
+ this.server.middlewares.use(async (req, res, next) => {
1396
+ const url$1 = req.url || "/";
1397
+ const match = url$1.match(/^\/preview\/([^/?]+)/);
1398
+ if (match) {
1399
+ const storyId = match[1];
1400
+ const config = this.storyConfigs.get(storyId);
1401
+ if (config) try {
1402
+ const htmlTemplate = this.generateHtmlTemplate(`@virtual:story-entry?id=${storyId}`, config.darkMode);
1403
+ const transformedHtml = await this.server.transformIndexHtml(url$1, htmlTemplate);
1404
+ res.statusCode = 200;
1405
+ res.setHeader("Content-Type", "text/html");
1406
+ res.end(transformedHtml);
1407
+ return;
1408
+ } catch (e) {
1409
+ const err = e;
1410
+ log.error(`[ViteMiddleware] Error serving preview: ${err.message}`);
1411
+ next(err);
1412
+ return;
1413
+ }
1414
+ }
1415
+ next();
1416
+ });
1417
+ await this.server.listen();
1418
+ const address = this.server.httpServer?.address();
1419
+ if (!address || typeof address === "string") throw new Error("Failed to start Vite dev server. Ensure no other process is using the port.");
1420
+ const port = address.port;
1421
+ const url = `http://localhost:${port}`;
1422
+ this.serverUrl = url;
1423
+ this.serverPort = port;
1424
+ this.currentAppPath = resolvedAppPath;
1425
+ log.info(`[ViteReactBundlerService] Vite dev server started at ${url}`);
1426
+ return {
1427
+ url,
1428
+ port
1429
+ };
1430
+ } catch (error) {
1431
+ const err = /* @__PURE__ */ new Error(`Failed to start dev server for ${resolvedAppPath}: ${error instanceof Error ? error.message : String(error)}`);
1432
+ err.cause = error;
1433
+ throw err;
1434
+ }
1435
+ }
1436
+ /**
1437
+ * Serve a component dynamically through the dev server.
1438
+ * @param options - Component rendering options
1439
+ * @returns Promise resolving to the component URL and HTML file path
1440
+ * @throws Error if dev server is not running or file operations fail
1441
+ */
1442
+ async serveComponent(options) {
1443
+ if (!this.isServerRunning()) throw new Error("Dev server is not running. Start it first using startDevServer().");
1444
+ const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent } = options;
1445
+ validateStoryName(storyName);
1446
+ validateComponentPath(componentPath);
1447
+ validateArgsSize(args);
1448
+ validateCssFiles(cssFiles, this.monorepoRoot);
1449
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(this.monorepoRoot, appPath);
1450
+ if (this.currentAppPath !== resolvedAppPath) throw new Error(`Dev server is running for ${this.currentAppPath} but requested ${resolvedAppPath}.`);
1451
+ const tmpDir = path.join(resolvedAppPath, ".tmp");
1452
+ try {
1453
+ this.cleanupStaleStoryConfigs();
1454
+ await promises.mkdir(tmpDir, { recursive: true });
1455
+ const wrapperCssPath = path.join(tmpDir, "tailwind-wrapper.css");
1456
+ const wrapperCssContent = this.generateWrapperCss(resolvedAppPath, cssFiles);
1457
+ await promises.writeFile(wrapperCssPath, wrapperCssContent, "utf-8");
1458
+ const timestamp = Date.now();
1459
+ const storyId = `${timestamp}-${Math.random().toString(36).slice(2)}`;
1460
+ this.storyConfigs.set(storyId, {
1461
+ componentPath,
1462
+ storyName,
1463
+ args,
1464
+ appPath: resolvedAppPath,
1465
+ darkMode,
1466
+ cssFiles,
1467
+ rootComponent,
1468
+ tmpDir
1469
+ });
1470
+ this.storyConfigTimestamps.set(storyId, timestamp);
1471
+ const url = `${this.serverUrl}/preview/${storyId}`;
1472
+ const htmlContent = this.generateHtmlTemplate(`@virtual:story-entry?id=${storyId}`, darkMode);
1473
+ log.info(`[ViteReactBundlerService] Component served at: ${url}`);
1474
+ return {
1475
+ url,
1476
+ htmlContent
1477
+ };
1478
+ } catch (error) {
1479
+ const err = /* @__PURE__ */ new Error(`Failed to serve component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
1480
+ err.cause = error;
1481
+ throw err;
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Pre-render a component to a static HTML file.
1486
+ * @param options - Component rendering options
1487
+ * @returns Promise resolving to the HTML file path
1488
+ * @throws Error if build fails
1489
+ */
1490
+ async prerenderComponent(options) {
1491
+ const { componentPath, storyName, args = {}, darkMode = false, appPath, cssFiles = [], rootComponent } = options;
1492
+ validateStoryName(storyName);
1493
+ validateComponentPath(componentPath);
1494
+ validateArgsSize(args);
1495
+ validateCssFiles(cssFiles, this.monorepoRoot);
1496
+ const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(this.monorepoRoot, appPath);
1497
+ const tmpDir = path.join(resolvedAppPath, ".tmp");
1498
+ try {
1499
+ await promises.mkdir(tmpDir, { recursive: true });
1500
+ return { htmlFilePath: await this.buildComponent({
1501
+ componentPath,
1502
+ storyName,
1503
+ args,
1504
+ appPath: resolvedAppPath,
1505
+ darkMode,
1506
+ cssFiles,
1507
+ rootComponent,
1508
+ tmpDir
1509
+ }) };
1510
+ } catch (error) {
1511
+ const err = /* @__PURE__ */ new Error(`Failed to prerender component ${storyName}: ${error instanceof Error ? error.message : String(error)}`);
1512
+ err.cause = error;
1513
+ throw err;
1514
+ }
1515
+ }
1516
+ /**
1517
+ * Clean up server resources and reset state.
1518
+ * Closes the Vite dev server if running.
1519
+ *
1520
+ * Note: Errors during server close are intentionally logged but not re-thrown.
1521
+ * This ensures cleanup always completes and state is reset, even if the server
1522
+ * is in an unexpected state. Callers should not depend on cleanup failure detection.
1523
+ */
1524
+ async cleanup() {
1525
+ if (this.server) {
1526
+ log.info("[ViteReactBundlerService] Closing Vite dev server...");
1527
+ try {
1528
+ await this.server.close();
1529
+ } catch (error) {
1530
+ log.error(`[ViteReactBundlerService] Error closing server: ${error instanceof Error ? error.message : String(error)}`);
1531
+ }
1532
+ this.server = null;
1533
+ this.serverUrl = null;
1534
+ this.serverPort = null;
1535
+ this.currentAppPath = null;
1536
+ this.serverStartPromise = null;
1537
+ this.storyConfigs.clear();
1538
+ this.storyConfigTimestamps.clear();
1539
+ log.info("[ViteReactBundlerService] Vite dev server closed");
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Clean up stale story configs to prevent memory leaks.
1544
+ * Removes configs that are older than STORY_CONFIG_MAX_AGE_MS or
1545
+ * when the number of configs exceeds STORY_CONFIG_MAX_COUNT.
1546
+ */
1547
+ cleanupStaleStoryConfigs() {
1548
+ const now = Date.now();
1549
+ const entriesToDelete = [];
1550
+ for (const [storyId, timestamp] of this.storyConfigTimestamps) if (now - timestamp > STORY_CONFIG_MAX_AGE_MS) entriesToDelete.push(storyId);
1551
+ for (const storyId of entriesToDelete) {
1552
+ this.storyConfigs.delete(storyId);
1553
+ this.storyConfigTimestamps.delete(storyId);
1554
+ }
1555
+ if (entriesToDelete.length > 0) log.debug(`[ViteReactBundlerService] Cleaned up ${entriesToDelete.length} stale story configs`);
1556
+ if (this.storyConfigs.size > STORY_CONFIG_MAX_COUNT) {
1557
+ const toRemove = Array.from(this.storyConfigTimestamps.entries()).sort((a, b) => a[1] - b[1]).slice(0, this.storyConfigs.size - STORY_CONFIG_MAX_COUNT);
1558
+ for (const [storyId] of toRemove) {
1559
+ this.storyConfigs.delete(storyId);
1560
+ this.storyConfigTimestamps.delete(storyId);
1561
+ }
1562
+ log.debug(`[ViteReactBundlerService] Removed ${toRemove.length} oldest story configs to stay under limit`);
1563
+ }
1564
+ }
1565
+ /**
1566
+ * Generate a wrapper CSS file with @source directive for Tailwind v4.
1567
+ * This tells Tailwind where to scan for class names when building from .tmp directory.
1568
+ * @param appPath - Absolute path to the app directory
1569
+ * @param cssFiles - Array of CSS file paths to import
1570
+ * @returns Generated CSS content with @source directive
1571
+ */
1572
+ generateWrapperCss(appPath, cssFiles) {
1573
+ const cssImportStatements = cssFiles.map((cssFile) => {
1574
+ if (cssFile.startsWith("@") || cssFile.startsWith("tailwindcss/")) return `@import '${cssFile}';`;
1575
+ if (cssFile.startsWith("packages/") || cssFile.startsWith("apps/")) return `@import '${path.join(this.monorepoRoot, cssFile)}';`;
1576
+ return `@import '${path.join(appPath, cssFile)}';`;
1577
+ }).join("\n");
1578
+ return `/* Tailwind v4 source configuration for component scanning */
1579
+ @source "${path.join(appPath, "src")}";
1580
+
1581
+ ${cssImportStatements}
1582
+ `;
1583
+ }
1584
+ /**
1585
+ * Generate the React entry file content for rendering a story.
1586
+ * @param options - Build options including component path and story name
1587
+ * @returns Generated TypeScript/JSX entry file content
1588
+ */
1589
+ generateEntryFile(options) {
1590
+ const { componentPath, storyName, args, appPath, darkMode, rootComponent, tmpDir } = options;
1591
+ const argsJson = JSON.stringify(args, null, 2);
1592
+ return `${`import '${path.join(tmpDir, "tailwind-wrapper.css").replace(/\\/g, "/")}';`}
1593
+
1594
+ import React from 'react';
1595
+ import { createRoot } from 'react-dom/client';
1596
+ import * as Stories from '${componentPath}';
1597
+ ${rootComponent ? `import { RootDocument } from '${path.join(appPath, rootComponent).replace(/\\/g, "/")}';` : ""}
1598
+
1599
+ // Extract story metadata and the specific story to render
1600
+ const meta = Stories.default;
1601
+ const Story = Stories['${storyName}'];
1602
+ const storyArgs = Story?.args || {};
1603
+ // Merge story's default args with any custom args passed in
1604
+ const args = { ...storyArgs, ...${argsJson} };
1605
+
1606
+ // Create the story element - Storybook stories can define rendering in two ways:
1607
+ // 1. A render() function that receives args and context
1608
+ // 2. A component reference in meta.component
1609
+ let element;
1610
+ if (Story?.render) {
1611
+ // Story has custom render function - call it with args and minimal context
1612
+ const RenderComponent = () => Story.render(args, { loaded: {}, args });
1613
+ element = React.createElement(RenderComponent);
1614
+ } else {
1615
+ // Story uses component from meta - render with merged args as props
1616
+ const Component = meta.component;
1617
+ element = React.createElement(Component, args);
1618
+ }
1619
+
1620
+ // Wrap element with root component (theme provider, etc.) if specified
1621
+ const Wrapper = ${rootComponent ? "RootDocument" : "React.Fragment"};
1622
+ const wrappedElement = React.createElement(Wrapper, ${rootComponent ? `{ darkMode: ${darkMode} }` : "{}"}, element);
1623
+
1624
+ // Mount to DOM
1625
+ const rootEl = document.getElementById('root');
1626
+ if (!rootEl) throw new Error('Root element not found');
1627
+ const root = createRoot(rootEl);
1628
+ root.render(wrappedElement);
1629
+ `;
1630
+ }
1631
+ /**
1632
+ * Generate the HTML template for component preview.
1633
+ * @param entryFileName - Name of the entry file to include
1634
+ * @param darkMode - Whether to add dark mode class to HTML
1635
+ * @returns Generated HTML template string
1636
+ */
1637
+ generateHtmlTemplate(entryFileName, darkMode) {
1638
+ return `<!DOCTYPE html>
1639
+ <html class="${darkMode ? "dark" : ""}">
1640
+ <head>
1641
+ <meta charset="UTF-8">
1642
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1643
+ <title>Component Preview</title>
1644
+ <style>
1645
+ body { margin: 0; padding: 0; }
1646
+ #root { display: inline-block; }
1647
+ </style>
1648
+ </head>
1649
+ <body>
1650
+ <div id="root"></div>
1651
+ <script type="module" src="/${entryFileName}"><\/script>
1652
+ </body>
1653
+ </html>`;
1654
+ }
1655
+ async buildComponent(options) {
1656
+ const { appPath, darkMode, tmpDir, cssFiles = [] } = options;
1657
+ const timestamp = Date.now();
1658
+ try {
1659
+ const wrapperCssPath = path.join(tmpDir, "tailwind-wrapper.css");
1660
+ const wrapperCssContent = this.generateWrapperCss(appPath, cssFiles);
1661
+ await promises.writeFile(wrapperCssPath, wrapperCssContent, "utf-8");
1662
+ const storyId = `build-${timestamp}`;
1663
+ const virtualModuleId = `@virtual:story-entry?id=${storyId}`;
1664
+ const htmlTemplate = this.generateHtmlTemplate(virtualModuleId, darkMode);
1665
+ const htmlTemplateFileName = `index-${timestamp}.html`;
1666
+ const htmlTemplatePath = path.join(tmpDir, htmlTemplateFileName);
1667
+ await promises.writeFile(htmlTemplatePath, htmlTemplate, "utf-8");
1668
+ const { build } = await import("vite");
1669
+ const outDir = path.join(tmpDir, `dist-${timestamp}`);
1670
+ const buildStoryConfigs = /* @__PURE__ */ new Map();
1671
+ buildStoryConfigs.set(storyId, options);
1672
+ await build({
1673
+ root: tmpDir,
1674
+ base: "./",
1675
+ configFile: false,
1676
+ plugins: [
1677
+ tailwindcss(),
1678
+ viteSingleFile(),
1679
+ createStoryEntryPlugin((id) => buildStoryConfigs.get(id), (opts) => this.generateEntryFile(opts))
1680
+ ],
1681
+ resolve: { alias: { "@": path.join(appPath, "src") } },
1682
+ esbuild: {
1683
+ jsx: "automatic",
1684
+ jsxImportSource: "react"
1685
+ },
1686
+ build: {
1687
+ outDir,
1688
+ emptyOutDir: false,
1689
+ cssCodeSplit: false,
1690
+ rollupOptions: {
1691
+ input: htmlTemplatePath,
1692
+ output: { inlineDynamicImports: true }
1693
+ }
1694
+ }
1695
+ });
1696
+ const builtHtmlPath = path.join(outDir, htmlTemplateFileName);
1697
+ log.info(`[ViteReactBundlerService] Component built to: ${builtHtmlPath}`);
1698
+ await promises.unlink(htmlTemplatePath).catch((err) => log.debug(`[ViteReactBundlerService] Failed to cleanup temp file: ${err.message}`));
1699
+ return builtHtmlPath;
1700
+ } catch (error) {
1701
+ const err = /* @__PURE__ */ new Error(`Failed to build component: ${error instanceof Error ? error.message : String(error)}`);
1702
+ err.cause = error;
1703
+ throw err;
1704
+ }
1705
+ }
1706
+ };
1707
+
1708
+ //#endregion
1709
+ //#region src/services/BundlerService/BundlerServiceFactory.ts
1710
+ /** Valid file extensions for custom service modules */
1711
+ const VALID_SERVICE_EXTENSIONS = [
1712
+ ".ts",
1713
+ ".js",
1714
+ ".mjs",
1715
+ ".cjs"
1716
+ ];
1717
+ /**
1718
+ * Default factory that creates a ViteReactBundlerService instance.
1719
+ * Uses the singleton pattern to ensure only one dev server runs at a time.
1720
+ *
1721
+ * @returns The singleton ViteReactBundlerService instance
1722
+ *
1723
+ * @example
1724
+ * ```typescript
1725
+ * import { createDefaultBundlerService } from './BundlerServiceFactory';
1726
+ *
1727
+ * const bundler = createDefaultBundlerService();
1728
+ * await bundler.startDevServer('apps/my-app');
1729
+ * ```
1730
+ */
1731
+ function createDefaultBundlerService() {
1732
+ return ViteReactBundlerService.getInstance();
1733
+ }
1734
+ /**
1735
+ * Registry of available bundler service factories.
1736
+ * Allows registration of custom bundler implementations by key.
1737
+ *
1738
+ * @example
1739
+ * ```typescript
1740
+ * // Register a custom bundler
1741
+ * bundlerRegistry.set('webpack-react', () => new WebpackReactBundlerService());
1742
+ *
1743
+ * // Get a bundler by key
1744
+ * const factory = bundlerRegistry.get('webpack-react');
1745
+ * const bundler = factory?.() ?? createDefaultBundlerService();
1746
+ * ```
1747
+ */
1748
+ const bundlerRegistry = /* @__PURE__ */ new Map();
1749
+ bundlerRegistry.set("vite-react", createDefaultBundlerService);
1750
+ /** Cached bundler service instance loaded from config */
1751
+ let cachedBundlerService = null;
1752
+ /**
1753
+ * Get bundler service based on toolkit.yaml configuration.
1754
+ *
1755
+ * If a custom service is configured in toolkit.yaml under style-system.bundler.customService,
1756
+ * it will be dynamically loaded. Otherwise, returns the default ViteReactBundlerService.
1757
+ *
1758
+ * The custom service module must:
1759
+ * - Export a class that extends BaseBundlerService as default export, OR
1760
+ * - Export an instance of BaseBundlerService as default export, OR
1761
+ * - Export a getInstance() function that returns a BaseBundlerService
1762
+ *
1763
+ * @returns Promise resolving to a bundler service instance
1764
+ *
1765
+ * @example
1766
+ * ```typescript
1767
+ * // In toolkit.yaml:
1768
+ * // style-system:
1769
+ * // bundler:
1770
+ * // customService: packages/my-app/src/bundler/CustomBundlerService.ts
1771
+ *
1772
+ * const bundler = await getBundlerServiceFromConfig();
1773
+ * await bundler.startDevServer('apps/my-app');
1774
+ * ```
1775
+ */
1776
+ async function getBundlerServiceFromConfig() {
1777
+ if (cachedBundlerService) return cachedBundlerService;
1778
+ const config = await getBundlerConfig();
1779
+ if (!config?.customService) {
1780
+ cachedBundlerService = createDefaultBundlerService();
1781
+ return cachedBundlerService;
1782
+ }
1783
+ const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
1784
+ const customServicePath = path.resolve(monorepoRoot, config.customService);
1785
+ const normalizedWorkspaceRoot = path.resolve(monorepoRoot);
1786
+ if (!customServicePath.startsWith(normalizedWorkspaceRoot + path.sep)) {
1787
+ log.error(`[BundlerServiceFactory] Security error: customService path "${config.customService}" resolves outside workspace root`);
1788
+ cachedBundlerService = createDefaultBundlerService();
1789
+ return cachedBundlerService;
1790
+ }
1791
+ const ext = path.extname(customServicePath).toLowerCase();
1792
+ if (!VALID_SERVICE_EXTENSIONS.includes(ext)) {
1793
+ log.error(`[BundlerServiceFactory] Invalid file extension "${ext}" for customService. Expected one of: ${VALID_SERVICE_EXTENSIONS.join(", ")}`);
1794
+ cachedBundlerService = createDefaultBundlerService();
1795
+ return cachedBundlerService;
1796
+ }
1797
+ try {
1798
+ log.info(`[BundlerServiceFactory] Loading custom bundler service from: ${customServicePath}`);
1799
+ const module = await import(customServicePath);
1800
+ if (module.default) {
1801
+ if (typeof module.default === "function" && module.default.prototype) if (typeof module.default.getInstance === "function") cachedBundlerService = module.default.getInstance();
1802
+ else cachedBundlerService = new module.default();
1803
+ else if (typeof module.default === "object") cachedBundlerService = module.default;
1804
+ } else if (typeof module.getInstance === "function") cachedBundlerService = module.getInstance();
1805
+ if (!cachedBundlerService) throw new Error("Custom bundler service module must export a class extending BaseBundlerService as default, an instance as default, or a getInstance() function");
1806
+ const missingMethods = [
1807
+ "getBundlerId",
1808
+ "getFrameworkId",
1809
+ "startDevServer",
1810
+ "serveComponent",
1811
+ "prerenderComponent",
1812
+ "isServerRunning",
1813
+ "getServerUrl",
1814
+ "getServerPort",
1815
+ "getCurrentAppPath",
1816
+ "cleanup"
1817
+ ].filter((method) => typeof cachedBundlerService[method] !== "function");
1818
+ if (missingMethods.length > 0) throw new Error(`Custom bundler service must implement BaseBundlerService interface. Missing methods: ${missingMethods.join(", ")}`);
1819
+ log.info(`[BundlerServiceFactory] Custom bundler service loaded successfully`);
1820
+ return cachedBundlerService;
1821
+ } catch (error) {
1822
+ const message = error instanceof Error ? error.message : String(error);
1823
+ log.error(`[BundlerServiceFactory] Failed to load custom bundler service: ${message}`);
1824
+ log.info("[BundlerServiceFactory] Falling back to default ViteReactBundlerService");
1825
+ cachedBundlerService = createDefaultBundlerService();
1826
+ return cachedBundlerService;
1827
+ }
1828
+ }
1829
+
1830
+ //#endregion
1831
+ //#region src/utils/screenshot.ts
1832
+ const browsers = {
1833
+ chromium,
1834
+ firefox,
1835
+ webkit
1836
+ };
1837
+ /**
1838
+ * Browser launch configurations to try in order of preference.
1839
+ * Prefers system-installed Chrome before falling back to Playwright's bundled browsers.
1840
+ */
1841
+ const browserLaunchConfigs = [
1842
+ {
1843
+ browserType: chromium,
1844
+ channel: "chrome",
1845
+ name: "System Chrome"
1846
+ },
1847
+ {
1848
+ browserType: chromium,
1849
+ channel: void 0,
1850
+ name: "Playwright Chromium"
1851
+ },
1852
+ {
1853
+ browserType: firefox,
1854
+ channel: void 0,
1855
+ name: "Playwright Firefox"
1856
+ }
1857
+ ];
1858
+ /**
1859
+ * Attempts to launch a browser, trying multiple configurations in order of preference.
1860
+ * @returns Launched browser instance
1861
+ * @throws Error if no browser can be launched
1862
+ */
1863
+ async function launchBrowserWithFallback() {
1864
+ const errors = [];
1865
+ for (const config of browserLaunchConfigs) try {
1866
+ const browser = await config.browserType.launch({
1867
+ headless: true,
1868
+ channel: config.channel
1869
+ });
1870
+ console.log(`[screenshot] Using ${config.name}`);
1871
+ return browser;
1872
+ } catch (error) {
1873
+ const message = error instanceof Error ? error.message : String(error);
1874
+ errors.push(`${config.name}: ${message}`);
1875
+ }
1876
+ throw new Error(`No browser available. Tried:\n${errors.join("\n")}\n\nPlease install Chrome, or run 'npx playwright install chromium'`);
1877
+ }
1878
+ async function takeScreenshot(options) {
1879
+ const { url, output, width = 1280, height = 800, fullPage = false, browser: browserName = "chromium", waitTime = 1e3, darkMode = false, mobile = false, generateThumbnail = false, thumbnailWidth = 400, thumbnailQuality = 80, base64 = false } = options;
1880
+ let browser = null;
1881
+ try {
1882
+ if (browserName === "chromium") browser = await launchBrowserWithFallback();
1883
+ else {
1884
+ const browserType = browsers[browserName];
1885
+ if (!browserType) throw new Error(`Unsupported browser: ${browserName}`);
1886
+ browser = await browserType.launch({ headless: true });
1887
+ }
1888
+ const page = await (await browser.newContext({
1889
+ viewport: {
1890
+ width,
1891
+ height
1892
+ },
1893
+ colorScheme: darkMode ? "dark" : "light",
1894
+ isMobile: mobile
1895
+ })).newPage();
1896
+ await page.goto(url, { waitUntil: "networkidle" });
1897
+ if (waitTime > 0) await page.waitForTimeout(waitTime);
1898
+ const screenshotBuffer = await page.screenshot({
1899
+ path: output,
1900
+ fullPage
1901
+ });
1902
+ const result = { imagePath: output };
1903
+ if (generateThumbnail) {
1904
+ const ext = path.extname(output);
1905
+ const thumbnailPath = output.replace(ext, `-thumb${ext}`);
1906
+ await sharp(screenshotBuffer).resize(thumbnailWidth).jpeg({ quality: thumbnailQuality }).toFile(thumbnailPath.replace(ext, ".jpg"));
1907
+ result.thumbnailPath = thumbnailPath.replace(ext, ".jpg");
1908
+ }
1909
+ if (base64) result.base64 = screenshotBuffer.toString("base64");
1910
+ return result;
1911
+ } finally {
1912
+ if (browser) await browser.close();
1913
+ }
1914
+ }
1915
+
1916
+ //#endregion
1917
+ //#region src/services/ThemeService/BaseThemeService.ts
1918
+ /**
1919
+ * Abstract base class for theme listing services.
1920
+ *
1921
+ * Subclasses must implement the `listThemes` method to provide
1922
+ * source-specific theme extraction logic (CSS files, JSON configs, etc.).
1923
+ *
1924
+ * @example
1925
+ * ```typescript
1926
+ * class MyCustomThemeService extends BaseThemeService {
1927
+ * async listThemes(): Promise<AvailableThemesResult> {
1928
+ * // Custom theme extraction logic
1929
+ * }
1930
+ * }
1931
+ * ```
1932
+ */
1933
+ var BaseThemeService = class {
1934
+ config;
1935
+ /**
1936
+ * Creates a new theme service instance
1937
+ * @param config - Theme service configuration
1938
+ */
1939
+ constructor(config) {
1940
+ this.config = config;
1941
+ }
1942
+ /**
1943
+ * Validate that a file path exists and is readable.
1944
+ * Can be overridden by subclasses for custom validation.
1945
+ *
1946
+ * @param filePath - Path to validate
1947
+ * @throws Error if path is invalid or unreadable
1948
+ */
1949
+ async validatePath(filePath) {
1950
+ try {
1951
+ await promises.access(filePath);
1952
+ } catch {
1953
+ throw new Error(`File not found or not readable: ${filePath}`);
1954
+ }
1955
+ }
1956
+ };
1957
+
1958
+ //#endregion
1959
+ //#region src/services/ThemeService/CSSThemeService.ts
1960
+ /**
1961
+ * CSS-based theme service implementation.
1962
+ *
1963
+ * Extracts themes from CSS files by parsing class selectors that contain
1964
+ * CSS custom properties (variables). Each top-level class selector with
1965
+ * color variables is treated as a theme.
1966
+ *
1967
+ * @example
1968
+ * ```typescript
1969
+ * const service = new CSSThemeService({ themePath: 'apps/my-app/src/styles/colors.css' });
1970
+ * const result = await service.listThemes();
1971
+ * console.log(result.themes); // [{ name: 'slate', ... }, { name: 'blue', ... }]
1972
+ * ```
1973
+ */
1974
+ var CSSThemeService = class extends BaseThemeService {
1975
+ monorepoRoot;
1976
+ /**
1977
+ * Creates a new CSSThemeService instance
1978
+ * @param config - Theme service configuration with themePath or cssFiles
1979
+ */
1980
+ constructor(config) {
1981
+ super(config);
1982
+ this.monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
1983
+ }
1984
+ /**
1985
+ * Get the source identifier for this service
1986
+ * @returns 'css-file' identifier
1987
+ */
1988
+ getSourceId() {
1989
+ return "css-file";
1990
+ }
1991
+ /**
1992
+ * List available themes by parsing CSS files.
1993
+ *
1994
+ * Scans the configured CSS files for class selectors that define
1995
+ * CSS custom properties (--color-*, etc.) and returns them as themes.
1996
+ *
1997
+ * @returns Promise resolving to available themes result
1998
+ * @throws Error if no theme files are configured or files cannot be read
1999
+ */
2000
+ async listThemes() {
2001
+ const cssFilePaths = this.resolveCSSFilePaths();
2002
+ if (cssFilePaths.length === 0) throw new Error("No theme CSS files configured. Set themePath or cssFiles in project.json style-system config.");
2003
+ const themes = [];
2004
+ for (const cssFilePath of cssFilePaths) try {
2005
+ await this.validatePath(cssFilePath);
2006
+ const fileThemes = await this.extractThemesFromCSS(cssFilePath);
2007
+ themes.push(...fileThemes);
2008
+ } catch (error) {
2009
+ log.warn(`[CSSThemeService] Could not process ${cssFilePath}:`, error);
2010
+ }
2011
+ const uniqueThemes = this.deduplicateThemes(themes);
2012
+ return {
2013
+ themes: uniqueThemes.sort((a, b) => a.name.localeCompare(b.name)),
2014
+ activeTheme: uniqueThemes.length > 0 ? uniqueThemes[0].name : void 0,
2015
+ source: "css-file"
2016
+ };
2017
+ }
2018
+ /**
2019
+ * Resolve configured CSS file paths to absolute paths.
2020
+ * @returns Array of absolute CSS file paths
2021
+ */
2022
+ resolveCSSFilePaths() {
2023
+ const paths = [];
2024
+ if (this.config.themePath) {
2025
+ const themePath = path.isAbsolute(this.config.themePath) ? this.config.themePath : path.join(this.monorepoRoot, this.config.themePath);
2026
+ paths.push(themePath);
2027
+ }
2028
+ if (this.config.cssFiles && this.config.cssFiles.length > 0) for (const cssFile of this.config.cssFiles) {
2029
+ const cssPath = path.isAbsolute(cssFile) ? cssFile : path.join(this.monorepoRoot, cssFile);
2030
+ if (!paths.includes(cssPath)) paths.push(cssPath);
2031
+ }
2032
+ return paths;
2033
+ }
2034
+ /**
2035
+ * Extract themes from a CSS file by parsing class selectors.
2036
+ *
2037
+ * Identifies class selectors that contain CSS custom property declarations
2038
+ * (--color-*, --primary-*, etc.) and treats each as a theme definition.
2039
+ *
2040
+ * @param cssFilePath - Absolute path to CSS file
2041
+ * @returns Array of ThemeInfo objects
2042
+ */
2043
+ async extractThemesFromCSS(cssFilePath) {
2044
+ const content = await promises.readFile(cssFilePath, "utf-8");
2045
+ const fileName = path.basename(cssFilePath);
2046
+ const themes = [];
2047
+ try {
2048
+ postcss.parse(content).walkRules((rule) => {
2049
+ const selector = rule.selector.trim();
2050
+ if (!selector.startsWith(".") || selector.includes(" ") || selector.includes(",")) return;
2051
+ const className = selector.slice(1);
2052
+ const colorVariables = {};
2053
+ let hasColorVariables = false;
2054
+ rule.walkDecls((decl) => {
2055
+ if (decl.prop.startsWith("--color-") || decl.prop.startsWith("--primary-")) {
2056
+ colorVariables[decl.prop] = decl.value;
2057
+ hasColorVariables = true;
2058
+ }
2059
+ });
2060
+ if (hasColorVariables) themes.push({
2061
+ name: className,
2062
+ fileName,
2063
+ path: cssFilePath,
2064
+ colorVariables,
2065
+ shadeCount: Object.keys(colorVariables).length
2066
+ });
2067
+ });
2068
+ } catch (error) {
2069
+ throw new Error(`Failed to parse CSS file ${cssFilePath}: ${error instanceof Error ? error.message : String(error)}`);
2070
+ }
2071
+ log.info(`[CSSThemeService] Found ${themes.length} themes in ${fileName}`);
2072
+ return themes;
2073
+ }
2074
+ /**
2075
+ * Deduplicate themes by name, keeping the first occurrence.
2076
+ * @param themes - Array of themes to deduplicate
2077
+ * @returns Deduplicated array of themes
2078
+ */
2079
+ deduplicateThemes(themes) {
2080
+ const seen = /* @__PURE__ */ new Set();
2081
+ return themes.filter((theme) => {
2082
+ if (seen.has(theme.name)) return false;
2083
+ seen.add(theme.name);
2084
+ return true;
2085
+ });
2086
+ }
2087
+ };
2088
+
2089
+ //#endregion
2090
+ //#region src/services/ThemeService/ThemeService.ts
2091
+ /**
2092
+ * ThemeService handles theme configuration and CSS generation.
2093
+ *
2094
+ * Provides methods for accessing theme CSS, generating theme wrappers,
2095
+ * and listing available theme configurations.
2096
+ *
2097
+ * @example
2098
+ * ```typescript
2099
+ * const service = new ThemeService(designConfig);
2100
+ * const cssFiles = await service.getThemeCSS();
2101
+ * const themes = await service.listAvailableThemes();
2102
+ * ```
2103
+ */
2104
+ var ThemeService = class {
2105
+ monorepoRoot;
2106
+ config;
2107
+ /**
2108
+ * Creates a new ThemeService instance
2109
+ * @param config - Design system configuration
2110
+ */
2111
+ constructor(config) {
2112
+ this.monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
2113
+ this.config = config;
2114
+ log.info(`[ThemeService] Using theme provider: ${this.config.themeProvider}`);
2115
+ log.info(`[ThemeService] Design system type: ${this.config.type}`);
2116
+ }
2117
+ /**
2118
+ * Get design system configuration
2119
+ * @returns Current design system configuration
2120
+ */
2121
+ getConfig() {
2122
+ return this.config;
2123
+ }
2124
+ /**
2125
+ * Get theme CSS imports from config or common locations
2126
+ * @returns Array of absolute paths to CSS files
2127
+ */
2128
+ async getThemeCSS() {
2129
+ if (this.config.cssFiles && this.config.cssFiles.length > 0) return this.config.cssFiles.map((cssFile) => path.isAbsolute(cssFile) ? cssFile : path.join(this.monorepoRoot, cssFile));
2130
+ const cssPatterns = ["**/packages/frontend/web-theme/src/**/*.css", "**/packages/frontend/shared-theme/**/*.css"];
2131
+ const cssFiles = [];
2132
+ for (const pattern of cssPatterns) {
2133
+ const files = await glob(pattern, {
2134
+ cwd: this.monorepoRoot,
2135
+ absolute: true,
2136
+ ignore: ["**/node_modules/**", "**/dist/**"]
2137
+ });
2138
+ cssFiles.push(...files);
2139
+ }
2140
+ return cssFiles;
2141
+ }
2142
+ /**
2143
+ * Generate theme provider wrapper code
2144
+ * Uses default export as specified in config
2145
+ * @param componentCode - Component code to wrap
2146
+ * @param darkMode - Whether to use dark mode theme
2147
+ * @returns Generated wrapper code string
2148
+ */
2149
+ generateThemeWrapper(componentCode, darkMode = false) {
2150
+ const { themeProvider } = this.config;
2151
+ return `
2152
+ import React from 'react';
2153
+ import ThemeProvider from '${themeProvider}';
2154
+
2155
+ const WrappedComponent = () => {
2156
+ return React.createElement(
2157
+ ThemeProvider,
2158
+ { theme: ${darkMode ? "'dark'" : "'light'"} },
2159
+ ${componentCode}
2160
+ );
2161
+ };
2162
+
2163
+ export default WrappedComponent;
2164
+ `.trim();
2165
+ }
2166
+ /**
2167
+ * Get inline theme styles for SSR
2168
+ * @param darkMode - Whether to use dark mode styles
2169
+ * @returns Combined CSS content as string
2170
+ */
2171
+ async getInlineStyles(darkMode = false) {
2172
+ const cssFiles = await this.getThemeCSS();
2173
+ let styles = "";
2174
+ for (const cssFile of cssFiles) try {
2175
+ const content = await promises.readFile(cssFile, "utf-8");
2176
+ styles += content + "\n";
2177
+ } catch (error) {
2178
+ log.warn(`[ThemeService] Could not read CSS file ${cssFile}:`, error);
2179
+ }
2180
+ if (darkMode && styles) styles = `.dark { ${styles} }`;
2181
+ return styles;
2182
+ }
2183
+ /**
2184
+ * Get Tailwind CSS classes for theming
2185
+ * @param darkMode - Whether to use dark mode classes
2186
+ * @returns Array of Tailwind class names
2187
+ */
2188
+ getTailwindClasses(darkMode = false) {
2189
+ const baseClasses = ["font-sans", "antialiased"];
2190
+ if (darkMode) return [
2191
+ ...baseClasses,
2192
+ "dark",
2193
+ "bg-gray-900",
2194
+ "text-white"
2195
+ ];
2196
+ return [
2197
+ ...baseClasses,
2198
+ "bg-white",
2199
+ "text-gray-900"
2200
+ ];
2201
+ }
2202
+ /**
2203
+ * Validate that the theme provider path exists
2204
+ * @returns True if theme provider is valid
2205
+ */
2206
+ async validateThemeProvider() {
2207
+ try {
2208
+ if (this.config.themeProvider.startsWith("@") || !this.config.themeProvider.startsWith("/")) return true;
2209
+ return (await promises.stat(this.config.themeProvider)).isFile();
2210
+ } catch {
2211
+ return false;
2212
+ }
2213
+ }
2214
+ /**
2215
+ * List all available theme configurations
2216
+ * @returns Object containing themes array and active brand
2217
+ * @throws Error if themes directory cannot be read
2218
+ */
2219
+ async listAvailableThemes() {
2220
+ const configsPath = path.join(this.monorepoRoot, "packages/frontend/shared-theme/configs");
2221
+ try {
2222
+ const themeFiles = (await promises.readdir(configsPath)).filter((file) => file.endsWith(".json"));
2223
+ const themes = [];
2224
+ let activeBrand;
2225
+ for (const file of themeFiles) {
2226
+ const filePath = path.join(configsPath, file);
2227
+ const content = await promises.readFile(filePath, "utf-8");
2228
+ const themeData = JSON.parse(content);
2229
+ const themeName = file.replace(".json", "");
2230
+ themes.push({
2231
+ name: themeName,
2232
+ fileName: file,
2233
+ path: filePath,
2234
+ colors: themeData.colors || themeData
2235
+ });
2236
+ if (themeName === "lightTheme" || themeName === "agimonTheme") activeBrand = themeName;
2237
+ }
2238
+ return {
2239
+ themes: themes.sort((a, b) => a.name.localeCompare(b.name)),
2240
+ activeBrand
2241
+ };
2242
+ } catch (error) {
2243
+ throw new Error(`Failed to list themes: ${error instanceof Error ? error.message : String(error)}`);
2244
+ }
2245
+ }
2246
+ };
2247
+
2248
+ //#endregion
2249
+ //#region src/services/ThemeService/types.ts
2250
+ /**
2251
+ * Default theme service configuration
2252
+ */
2253
+ const DEFAULT_THEME_SERVICE_CONFIG = {
2254
+ themePath: void 0,
2255
+ cssFiles: [],
2256
+ customServicePath: void 0
2257
+ };
2258
+
2259
+ //#endregion
2260
+ //#region src/services/ThemeService/ThemeServiceFactory.ts
2261
+ /**
2262
+ * Factory for creating theme service instances.
2263
+ *
2264
+ * Supports the built-in CSSThemeService and custom service implementations
2265
+ * loaded dynamically from user-specified paths.
2266
+ *
2267
+ * @example
2268
+ * ```typescript
2269
+ * const factory = new ThemeServiceFactory();
2270
+ *
2271
+ * // Create default CSS-based service
2272
+ * const service = await factory.createService({
2273
+ * themePath: 'apps/my-app/src/styles/colors.css'
2274
+ * });
2275
+ *
2276
+ * // Create with custom service override
2277
+ * const customService = await factory.createService({
2278
+ * customServicePath: './my-custom-theme-service.ts'
2279
+ * });
2280
+ *
2281
+ * const themes = await service.listThemes();
2282
+ * ```
2283
+ */
2284
+ var ThemeServiceFactory = class {
2285
+ /**
2286
+ * Create a theme service based on configuration.
2287
+ *
2288
+ * Resolution order:
2289
+ * 1. If customServicePath is provided, load custom service dynamically
2290
+ * 2. Otherwise, create the default CSSThemeService
2291
+ *
2292
+ * @param config - Theme service configuration
2293
+ * @returns Promise resolving to a theme service instance
2294
+ * @throws Error if custom service cannot be loaded or is invalid
2295
+ */
2296
+ async createService(config = {}) {
2297
+ const resolvedConfig = {
2298
+ ...DEFAULT_THEME_SERVICE_CONFIG,
2299
+ ...config
2300
+ };
2301
+ if (resolvedConfig.customServicePath) return this.loadCustomService(resolvedConfig);
2302
+ return new CSSThemeService(resolvedConfig);
2303
+ }
2304
+ /**
2305
+ * Load a custom theme service from user-specified path.
2306
+ *
2307
+ * The custom service must export a class that extends BaseThemeService.
2308
+ *
2309
+ * @param config - Configuration with customServicePath set
2310
+ * @returns Promise resolving to custom service instance
2311
+ * @throws Error if service cannot be loaded or is invalid
2312
+ */
2313
+ async loadCustomService(config) {
2314
+ const servicePath = config.customServicePath;
2315
+ if (!servicePath) throw new Error("customServicePath is required for custom service loading");
2316
+ try {
2317
+ const customModule = await import(servicePath);
2318
+ const ServiceClass = customModule.default || customModule.ThemeService || customModule.CustomThemeService;
2319
+ if (!ServiceClass) throw new Error(`Custom service module at ${servicePath} must export a default class, ThemeService, or CustomThemeService that extends BaseThemeService`);
2320
+ const instance = new ServiceClass(config);
2321
+ if (!(instance instanceof BaseThemeService)) throw new Error(`Custom service at ${servicePath} must extend BaseThemeService`);
2322
+ return instance;
2323
+ } catch (error) {
2324
+ if (error instanceof Error && error.message.includes("BaseThemeService")) throw error;
2325
+ throw new Error(`Failed to load custom theme service from ${servicePath}: ${error instanceof Error ? error.message : String(error)}`);
2326
+ }
2327
+ }
2328
+ };
2329
+
2330
+ //#endregion
2331
+ //#region src/services/ComponentRendererService/ComponentRendererService.ts
2332
+ /**
2333
+ * ComponentRendererService handles rendering React components to images.
2334
+ *
2335
+ * Uses BundlerService for building/serving components and ThemeService
2336
+ * for theme configuration. Supports both dev server (fast) and static
2337
+ * build (fallback) rendering modes.
2338
+ *
2339
+ * The bundler service can be customized by providing a bundlerFactory
2340
+ * to support different bundlers (Vite, Webpack) and frameworks (React, Vue).
2341
+ *
2342
+ * @example
2343
+ * ```typescript
2344
+ * // Using default bundler (Vite + React)
2345
+ * const service = new ComponentRendererService(designConfig, 'apps/my-app');
2346
+ *
2347
+ * // Using custom bundler
2348
+ * const service = new ComponentRendererService(
2349
+ * designConfig,
2350
+ * 'apps/my-app',
2351
+ * { bundlerFactory: () => new MyCustomBundlerService() }
2352
+ * );
2353
+ *
2354
+ * const result = await service.renderComponent(componentInfo, {
2355
+ * storyName: 'Primary',
2356
+ * darkMode: true,
2357
+ * width: 1280,
2358
+ * height: 800
2359
+ * });
2360
+ * console.log(result.imagePath);
2361
+ * ```
2362
+ */
2363
+ var ComponentRendererService = class ComponentRendererService {
2364
+ monorepoRoot;
2365
+ tmpDir;
2366
+ themeService;
2367
+ appPath;
2368
+ bundlerFactory;
2369
+ /**
2370
+ * Creates a new ComponentRendererService instance
2371
+ * @param designSystemConfig - Design system configuration
2372
+ * @param appPath - Path to the app directory (relative or absolute)
2373
+ * @param options - Optional configuration including custom bundler factory
2374
+ */
2375
+ constructor(designSystemConfig, appPath, options = {}) {
2376
+ if (!appPath) throw new Error("appPath is required for ComponentRendererService");
2377
+ this.monorepoRoot = TemplatesManagerService.getWorkspaceRootSync();
2378
+ this.appPath = appPath;
2379
+ this.tmpDir = path.join(os.tmpdir(), "style-system");
2380
+ this.themeService = new ThemeService(designSystemConfig);
2381
+ this.bundlerFactory = options.bundlerFactory ?? createDefaultBundlerService;
2382
+ }
2383
+ /**
2384
+ * Get the bundler service instance.
2385
+ * Uses the factory to create/retrieve the bundler.
2386
+ * @returns The bundler service instance
2387
+ */
2388
+ getBundlerService() {
2389
+ return this.bundlerFactory();
2390
+ }
2391
+ /**
2392
+ * Render a component to an image
2393
+ * @param componentInfo - Component metadata from StoriesIndexService
2394
+ * @param options - Render options (story name, args, dimensions, etc.)
2395
+ * @returns Rendered image path, HTML content, and component info
2396
+ * @throws Error if rendering fails
2397
+ */
2398
+ async renderComponent(componentInfo, options = {}) {
2399
+ const { storyName = componentInfo.stories[0] || "Default", args = {}, darkMode = false, width = 1280, height = 800 } = options;
2400
+ try {
2401
+ log.info(`[ComponentRendererService] Rendering ${componentInfo.title} - ${storyName}`);
2402
+ await promises.mkdir(this.tmpDir, { recursive: true });
2403
+ const designSystemConfig = this.themeService.getConfig();
2404
+ log.info(`[ComponentRendererService] Using theme provider: ${designSystemConfig.themeProvider}`);
2405
+ log.info(`[ComponentRendererService] Design system type: ${designSystemConfig.type}`);
2406
+ if (!await this.themeService.validateThemeProvider()) log.warn(`[ComponentRendererService] Theme provider path may not exist: ${designSystemConfig.themeProvider}`);
2407
+ const bundlerService = this.getBundlerService();
2408
+ let componentUrl;
2409
+ let htmlFilePath;
2410
+ if (bundlerService.isServerRunning()) {
2411
+ log.info("[ComponentRendererService] Using dev server for fast rendering");
2412
+ try {
2413
+ const result = await bundlerService.serveComponent({
2414
+ componentPath: componentInfo.filePath,
2415
+ storyName,
2416
+ args,
2417
+ themePath: designSystemConfig.themeProvider,
2418
+ darkMode,
2419
+ appPath: this.appPath,
2420
+ cssFiles: designSystemConfig.cssFiles || [],
2421
+ rootComponent: designSystemConfig.rootComponent
2422
+ });
2423
+ componentUrl = result.url;
2424
+ htmlFilePath = result.htmlFilePath;
2425
+ log.info(`[ComponentRendererService] Component served at: ${componentUrl}`);
2426
+ } catch (devServerError) {
2427
+ log.warn(`[ComponentRendererService] Dev server failed, falling back to static build: ${devServerError instanceof Error ? devServerError.message : String(devServerError)}`);
2428
+ const result = await bundlerService.prerenderComponent({
2429
+ componentPath: componentInfo.filePath,
2430
+ storyName,
2431
+ args,
2432
+ themePath: designSystemConfig.themeProvider,
2433
+ darkMode,
2434
+ appPath: this.appPath,
2435
+ cssFiles: designSystemConfig.cssFiles || [],
2436
+ rootComponent: designSystemConfig.rootComponent
2437
+ });
2438
+ componentUrl = `file://${result.htmlFilePath}`;
2439
+ htmlFilePath = result.htmlFilePath;
2440
+ log.info(`[ComponentRendererService] HTML built to: ${htmlFilePath}`);
2441
+ }
2442
+ } else {
2443
+ log.info("[ComponentRendererService] No dev server running, using static build");
2444
+ const result = await bundlerService.prerenderComponent({
2445
+ componentPath: componentInfo.filePath,
2446
+ storyName,
2447
+ args,
2448
+ themePath: designSystemConfig.themeProvider,
2449
+ darkMode,
2450
+ appPath: this.appPath,
2451
+ cssFiles: designSystemConfig.cssFiles || [],
2452
+ rootComponent: designSystemConfig.rootComponent
2453
+ });
2454
+ componentUrl = `file://${result.htmlFilePath}`;
2455
+ htmlFilePath = result.htmlFilePath;
2456
+ log.info(`[ComponentRendererService] HTML built to: ${htmlFilePath}`);
2457
+ }
2458
+ const timestamp = Date.now();
2459
+ const imageName = `component-${componentInfo.title.split("/").pop()}-${timestamp}.png`;
2460
+ const imagePath = path.join(this.tmpDir, imageName);
2461
+ await takeScreenshot({
2462
+ url: componentUrl,
2463
+ output: imagePath,
2464
+ width,
2465
+ height,
2466
+ fullPage: true,
2467
+ browser: "chromium",
2468
+ waitTime: 2e3,
2469
+ darkMode,
2470
+ mobile: false,
2471
+ generateThumbnail: true,
2472
+ thumbnailWidth: 900,
2473
+ thumbnailQuality: 80,
2474
+ base64: false
2475
+ });
2476
+ log.info(`[ComponentRendererService] Screenshot saved to: ${imagePath}`);
2477
+ let html = "";
2478
+ if (htmlFilePath) {
2479
+ html = await promises.readFile(htmlFilePath, "utf-8");
2480
+ log.info(`[ComponentRendererService] HTML file kept at: ${htmlFilePath}`);
2481
+ } else log.warn("[ComponentRendererService] No HTML file path available, returning empty HTML content");
2482
+ return {
2483
+ imagePath,
2484
+ html,
2485
+ componentInfo
2486
+ };
2487
+ } catch (error) {
2488
+ const errorMessage = error instanceof Error ? error.message : String(error);
2489
+ throw new Error(`Failed to render component ${componentInfo.title} (${storyName}): ${errorMessage}`);
2490
+ }
2491
+ }
2492
+ /**
2493
+ * Maximum number of screenshot files to keep in temp directory.
2494
+ * Prevents disk space issues on long-running servers.
2495
+ */
2496
+ static MAX_TEMP_FILES = 100;
2497
+ /**
2498
+ * Clean up old rendered files.
2499
+ * Removes files older than the specified duration and enforces a max file count.
2500
+ * @param olderThanMs - Remove files older than this duration (default: 1 hour)
2501
+ */
2502
+ async cleanup(olderThanMs = 36e5) {
2503
+ try {
2504
+ const files = await promises.readdir(this.tmpDir);
2505
+ const now = Date.now();
2506
+ const componentFiles = [];
2507
+ for (const file of files) if (file.startsWith("component-")) {
2508
+ const filePath = path.join(this.tmpDir, file);
2509
+ try {
2510
+ const stats = await promises.stat(filePath);
2511
+ componentFiles.push({
2512
+ name: file,
2513
+ path: filePath,
2514
+ mtime: stats.mtimeMs
2515
+ });
2516
+ } catch {}
2517
+ }
2518
+ let deletedCount = 0;
2519
+ for (const file of componentFiles) if (now - file.mtime > olderThanMs) {
2520
+ await promises.unlink(file.path).catch((err) => log.warn("[ComponentRendererService] Failed to delete file:", file.path, err));
2521
+ deletedCount++;
2522
+ }
2523
+ const remainingFiles = componentFiles.filter((f) => now - f.mtime <= olderThanMs);
2524
+ if (remainingFiles.length > ComponentRendererService.MAX_TEMP_FILES) {
2525
+ remainingFiles.sort((a, b) => a.mtime - b.mtime);
2526
+ const toDelete = remainingFiles.slice(0, remainingFiles.length - ComponentRendererService.MAX_TEMP_FILES);
2527
+ for (const file of toDelete) {
2528
+ await promises.unlink(file.path).catch((err) => log.warn("[ComponentRendererService] Failed to delete file:", file.path, err));
2529
+ deletedCount++;
2530
+ }
2531
+ }
2532
+ if (deletedCount > 0) log.info(`[ComponentRendererService] Cleaned up ${deletedCount} temp files`);
2533
+ } catch (error) {
2534
+ log.error("[ComponentRendererService] Cleanup error:", error);
2535
+ }
2536
+ }
2537
+ /**
2538
+ * Cleanup bundler server and temp files.
2539
+ * Called on service shutdown.
2540
+ */
2541
+ async dispose() {
2542
+ try {
2543
+ await this.cleanup(0);
2544
+ const bundlerService = this.getBundlerService();
2545
+ if (!bundlerService.isServerRunning()) await bundlerService.cleanup();
2546
+ } catch (error) {
2547
+ log.error("[ComponentRendererService] Dispose error:", error);
2548
+ }
2549
+ }
2550
+ };
2551
+
2552
+ //#endregion
2553
+ //#region src/services/GetUiComponentService/types.ts
2554
+ /**
2555
+ * Default configuration values.
2556
+ */
2557
+ const DEFAULT_GET_UI_COMPONENT_CONFIG = {
2558
+ defaultStoryName: "Playground",
2559
+ defaultDarkMode: true,
2560
+ defaultWidth: 1280,
2561
+ defaultHeight: 800
2562
+ };
2563
+
2564
+ //#endregion
2565
+ //#region src/services/GetUiComponentService/GetUiComponentService.ts
2566
+ /**
2567
+ * GetUiComponentService handles rendering UI component previews.
2568
+ *
2569
+ * Locates components in the stories index, renders them with app-specific
2570
+ * design system configuration, and returns screenshot results.
2571
+ *
2572
+ * @example
2573
+ * ```typescript
2574
+ * const service = new GetUiComponentService();
2575
+ * const result = await service.getComponent({
2576
+ * componentName: 'Button',
2577
+ * appPath: 'apps/my-app',
2578
+ * });
2579
+ * console.log(result.imagePath);
2580
+ * ```
2581
+ */
2582
+ var GetUiComponentService = class {
2583
+ config;
2584
+ storiesIndexFactory;
2585
+ rendererFactory;
2586
+ /**
2587
+ * Creates a new GetUiComponentService instance.
2588
+ * @param config - Service configuration options
2589
+ * @param storiesIndexFactory - Factory for creating StoriesIndexService (for DI/testing)
2590
+ * @param rendererFactory - Factory for creating ComponentRendererService (for DI/testing)
2591
+ */
2592
+ constructor(config = {}, storiesIndexFactory = () => new StoriesIndexService(), rendererFactory = (c, p) => new ComponentRendererService(c, p)) {
2593
+ this.config = {
2594
+ ...DEFAULT_GET_UI_COMPONENT_CONFIG,
2595
+ ...config
2596
+ };
2597
+ this.storiesIndexFactory = storiesIndexFactory;
2598
+ this.rendererFactory = rendererFactory;
2599
+ }
2600
+ /**
2601
+ * Get a UI component preview image.
2602
+ *
2603
+ * @param input - Component input parameters
2604
+ * @returns Result with image path and component metadata
2605
+ * @throws Error if input validation fails, component not found, or rendering fails
2606
+ */
2607
+ async getComponent(input) {
2608
+ if (!input.componentName || typeof input.componentName !== "string") throw new Error("componentName is required and must be a non-empty string");
2609
+ if (!input.appPath || typeof input.appPath !== "string") throw new Error("appPath is required and must be a non-empty string");
2610
+ if (input.storyName !== void 0 && typeof input.storyName !== "string") throw new Error("storyName must be a string");
2611
+ if (input.darkMode !== void 0 && typeof input.darkMode !== "boolean") throw new Error("darkMode must be a boolean");
2612
+ if (input.selector !== void 0) {
2613
+ if (typeof input.selector !== "string") throw new Error("selector must be a string");
2614
+ if (!/^[a-zA-Z0-9_\-#.\[\]=":' ]+$/.test(input.selector)) throw new Error("selector contains invalid characters");
2615
+ if (/javascript:|expression\(|url\(/i.test(input.selector)) throw new Error("selector contains potentially malicious content");
2616
+ }
2617
+ const { componentName, appPath, storyName = this.config.defaultStoryName, darkMode = this.config.defaultDarkMode } = input;
2618
+ log.info(`[GetUiComponentService] Starting for component: ${componentName}, appPath: ${appPath}, storyName: ${storyName}`);
2619
+ const storiesIndex = this.storiesIndexFactory();
2620
+ try {
2621
+ await storiesIndex.initialize();
2622
+ } catch (error) {
2623
+ throw new Error(`Failed to initialize stories index: ${error instanceof Error ? error.message : String(error)}`);
2624
+ }
2625
+ const componentInfo = storiesIndex.findComponentByName(componentName);
2626
+ if (!componentInfo) throw new Error(`Component "${componentName}" not found in stories index. Ensure the component has a .stories.tsx file and has been indexed.`);
2627
+ log.info(`[GetUiComponentService] Found component: ${componentInfo.title}`);
2628
+ const validStoryName = this.resolveStoryName(storyName, componentInfo.stories);
2629
+ const designSystemConfig = await getAppDesignSystemConfig(appPath);
2630
+ log.info(`[GetUiComponentService] Using theme provider: ${designSystemConfig.themeProvider}`);
2631
+ log.info(`[GetUiComponentService] Design system type: ${designSystemConfig.type}`);
2632
+ const renderer = this.rendererFactory(designSystemConfig, appPath);
2633
+ try {
2634
+ const renderResult = await renderer.renderComponent(componentInfo, {
2635
+ storyName: validStoryName,
2636
+ width: this.config.defaultWidth,
2637
+ height: this.config.defaultHeight,
2638
+ darkMode
2639
+ });
2640
+ log.info(`[GetUiComponentService] Component rendered to: ${renderResult.imagePath}`);
2641
+ const storyFileContent = await this.readStoryFile(componentInfo.filePath);
2642
+ log.info("[GetUiComponentService] Completed successfully");
2643
+ return {
2644
+ imagePath: renderResult.imagePath,
2645
+ format: "png",
2646
+ dimensions: "900px width, 80% quality",
2647
+ storyFilePath: componentInfo.filePath,
2648
+ storyFileContent,
2649
+ componentTitle: componentInfo.title,
2650
+ availableStories: componentInfo.stories,
2651
+ renderedStory: validStoryName
2652
+ };
2653
+ } finally {
2654
+ await renderer.dispose();
2655
+ }
2656
+ }
2657
+ /**
2658
+ * Resolve and validate the story name.
2659
+ *
2660
+ * @param requestedStory - The requested story name
2661
+ * @param availableStories - List of available stories for the component
2662
+ * @returns Valid story name (requested or fallback)
2663
+ */
2664
+ resolveStoryName(requestedStory, availableStories) {
2665
+ if (availableStories.includes(requestedStory)) return requestedStory;
2666
+ log.warn(`[GetUiComponentService] Story "${requestedStory}" not found, available stories: ${availableStories.join(", ")}`);
2667
+ const fallbackStory = availableStories[0] || "Default";
2668
+ log.info(`[GetUiComponentService] Using fallback story: ${fallbackStory}`);
2669
+ return fallbackStory;
2670
+ }
2671
+ /**
2672
+ * Read the story file content.
2673
+ *
2674
+ * @param filePath - Path to the story file
2675
+ * @returns File content or error message
2676
+ */
2677
+ async readStoryFile(filePath) {
2678
+ try {
2679
+ const content = await promises.readFile(filePath, "utf-8");
2680
+ log.info(`[GetUiComponentService] Story file read successfully (${content.length} chars)`);
2681
+ return content;
2682
+ } catch (error) {
2683
+ log.warn(`[GetUiComponentService] Warning: Could not read story file: ${error instanceof Error ? error.message : String(error)}`);
2684
+ return `// Could not read file: ${filePath}\n// Error: ${error instanceof Error ? error.message : String(error)}`;
2685
+ }
2686
+ }
2687
+ };
2688
+
2689
+ //#endregion
2690
+ //#region src/tools/GetComponentVisualTool.ts
2691
+ /**
2692
+ * Template for visual review instructions returned with component screenshots.
2693
+ * Guides the LLM to verify visual correctness of the rendered component.
2694
+ * @param imagePath - Path to the component screenshot image
2695
+ * @returns Formatted markdown string with visual review instructions
2696
+ */
2697
+ const REVIEW_INSTRUCTIONS_TEMPLATE = (imagePath) => `
2698
+ ## Visual Review Instructions
2699
+
2700
+ Please review the component screenshot and story code to verify visual correctness:
2701
+
2702
+ 1. **Read the screenshot** at: ${imagePath}
2703
+ 2. **Compare with the story code** to ensure the rendered output matches expectations
2704
+ 3. **Check for visual issues**:
2705
+ - Are all variants rendering correctly?
2706
+ - Are colors, spacing, and typography as expected?
2707
+ - Are interactive states (hover, disabled, loading) properly styled?
2708
+ - Is the layout and alignment correct?
2709
+
2710
+ If you notice any visual discrepancies between the code and the rendered output, please report them.
2711
+ `;
2712
+ var GetComponentVisualTool = class GetComponentVisualTool {
2713
+ static TOOL_NAME = "get_component_visual";
2714
+ service;
2715
+ /**
2716
+ * Creates a new GetComponentVisualTool instance.
2717
+ * @param service - Optional service instance for dependency injection (useful for testing)
2718
+ */
2719
+ constructor(service) {
2720
+ this.service = service ?? new GetUiComponentService();
2721
+ }
2722
+ /**
2723
+ * Returns the tool definition including name, description, and input schema.
2724
+ * @returns Tool definition with JSON Schema for input validation
2725
+ */
2726
+ getDefinition() {
2727
+ return {
2728
+ name: GetComponentVisualTool.TOOL_NAME,
2729
+ description: "Get a image preview of a UI component with app-specific design system configuration. Useful when work on the frontend design to review the UI quickly without running the full app.",
2730
+ inputSchema: {
2731
+ type: "object",
2732
+ properties: {
2733
+ componentName: {
2734
+ type: "string",
2735
+ minLength: 1,
2736
+ description: "The name of the component to capture (e.g., \"Button\", \"Card\", etc.)"
2737
+ },
2738
+ appPath: {
2739
+ type: "string",
2740
+ minLength: 1,
2741
+ description: "The app path (relative or absolute) to load design system configuration from (e.g., \"apps/agiflow-app\"). The design system config is read from {appPath}/project.json"
2742
+ },
2743
+ storyName: {
2744
+ type: "string",
2745
+ minLength: 1,
2746
+ description: "The story name to render (e.g., \"Playground\", \"Default\"). Defaults to \"Playground\"."
2747
+ },
2748
+ darkMode: {
2749
+ type: "boolean",
2750
+ description: "Whether to render the component in dark mode. Defaults to true."
2751
+ },
2752
+ selector: {
2753
+ type: "string",
2754
+ minLength: 1,
2755
+ description: "CSS selector to target specific element for screenshot. When provided, screenshot will auto-resize to element dimensions. Defaults to \"#root\"."
2756
+ }
2757
+ },
2758
+ required: ["componentName", "appPath"],
2759
+ additionalProperties: false
2760
+ }
2761
+ };
2762
+ }
2763
+ /**
2764
+ * Executes the tool to get a UI component preview.
2765
+ * @param input - The input parameters for getting the component
2766
+ * @returns Promise resolving to CallToolResult with component info or error
2767
+ */
2768
+ async execute(input) {
2769
+ try {
2770
+ const result = await this.service.getComponent(input);
2771
+ const reviewInstructions = REVIEW_INSTRUCTIONS_TEMPLATE(result.imagePath);
2772
+ return { content: [{
2773
+ type: "text",
2774
+ text: JSON.stringify(result, null, 2)
2775
+ }, {
2776
+ type: "text",
2777
+ text: reviewInstructions
2778
+ }] };
2779
+ } catch (error) {
2780
+ return {
2781
+ content: [{
2782
+ type: "text",
2783
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
2784
+ }],
2785
+ isError: true
2786
+ };
2787
+ }
2788
+ }
2789
+ };
2790
+
2791
+ //#endregion
2792
+ //#region src/tools/ListAppComponentsTool.ts
2793
+ /**
2794
+ * Tool to list app-specific components and package components used by an app.
2795
+ *
2796
+ * Detects app components by file path (within app directory) and resolves
2797
+ * workspace dependencies to find package components from Storybook stories.
2798
+ *
2799
+ * @example
2800
+ * ```typescript
2801
+ * const tool = new ListAppComponentsTool();
2802
+ * const result = await tool.execute({ appPath: 'apps/my-app' });
2803
+ * // Returns: { app: 'my-app', appComponents: ['Button'], packageComponents: {...}, pagination: {...} }
2804
+ * ```
2805
+ */
2806
+ var ListAppComponentsTool = class ListAppComponentsTool {
2807
+ static TOOL_NAME = "list_app_components";
2808
+ static COMPONENT_REUSE_INSTRUCTION = "IMPORTANT: Before creating new components, check if a similar component already exists in the list above. Always reuse existing components to maintain consistency and reduce code duplication.";
2809
+ service;
2810
+ constructor() {
2811
+ this.service = new AppComponentsService();
2812
+ }
2813
+ /**
2814
+ * Gets the tool definition including name, description, and input schema.
2815
+ * @returns Tool definition for MCP registration
2816
+ */
2817
+ getDefinition() {
2818
+ return {
2819
+ name: ListAppComponentsTool.TOOL_NAME,
2820
+ description: "List app-specific components and package components used by an app. Call this tool BEFORE creating new components to check if a similar component already exists. Reads the app's package.json to find workspace dependencies and returns components from both the app and its dependent packages.",
2821
+ inputSchema: {
2822
+ type: "object",
2823
+ properties: {
2824
+ appPath: {
2825
+ type: "string",
2826
+ description: "The app path (relative or absolute) to list components for (e.g., \"apps/my-app\")",
2827
+ minLength: 1
2828
+ },
2829
+ cursor: {
2830
+ type: "string",
2831
+ description: "Optional pagination cursor to fetch the next page of results. Omit to fetch the first page."
2832
+ }
2833
+ },
2834
+ required: ["appPath"],
2835
+ additionalProperties: false
2836
+ }
2837
+ };
2838
+ }
2839
+ /**
2840
+ * Lists app-specific and package components for a given application.
2841
+ * @param input - Object containing appPath and optional cursor for pagination
2842
+ * @returns CallToolResult with component list or error
2843
+ */
2844
+ async execute(input) {
2845
+ try {
2846
+ const result = await this.service.listComponents({
2847
+ appPath: input.appPath,
2848
+ cursor: input.cursor
2849
+ });
2850
+ return { content: [{
2851
+ type: "text",
2852
+ text: `${ListAppComponentsTool.COMPONENT_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
2853
+ }] };
2854
+ } catch (error) {
2855
+ return {
2856
+ content: [{
2857
+ type: "text",
2858
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
2859
+ }],
2860
+ isError: true
2861
+ };
2862
+ }
2863
+ }
2864
+ };
2865
+
2866
+ //#endregion
2867
+ //#region src/tools/ListThemesTool.ts
2868
+ /**
2869
+ * Tool to list all available theme configurations.
2870
+ *
2871
+ * Reads themes from CSS files configured in the app's project.json
2872
+ * style-system config. Themes are extracted by parsing CSS class selectors
2873
+ * that contain color variable definitions.
2874
+ *
2875
+ * @example
2876
+ * ```typescript
2877
+ * const tool = new ListThemesTool();
2878
+ * const result = await tool.execute({ appPath: 'apps/my-app' });
2879
+ * // Returns: { themes: [{ name: 'slate', ... }, { name: 'blue', ... }], source: 'css-file' }
2880
+ * ```
2881
+ */
2882
+ var ListThemesTool = class ListThemesTool {
2883
+ static TOOL_NAME = "list_themes";
2884
+ serviceFactory;
2885
+ constructor() {
2886
+ this.serviceFactory = new ThemeServiceFactory();
2887
+ }
2888
+ getDefinition() {
2889
+ return {
2890
+ name: ListThemesTool.TOOL_NAME,
2891
+ description: "List all available theme configurations. Reads themes from CSS files configured in the app's project.json style-system config.",
2892
+ inputSchema: {
2893
+ type: "object",
2894
+ properties: { appPath: {
2895
+ type: "string",
2896
+ description: "App path (relative or absolute) to read theme config from project.json (e.g., \"apps/my-app\")"
2897
+ } },
2898
+ additionalProperties: false
2899
+ }
2900
+ };
2901
+ }
2902
+ async execute(input) {
2903
+ try {
2904
+ let themePath;
2905
+ let cssFiles;
2906
+ if (input.appPath) {
2907
+ const appConfig = await getAppDesignSystemConfig(input.appPath);
2908
+ themePath = appConfig.themePath;
2909
+ cssFiles = appConfig.cssFiles;
2910
+ }
2911
+ const result = await (await this.serviceFactory.createService({
2912
+ themePath,
2913
+ cssFiles
2914
+ })).listThemes();
2915
+ return { content: [{
2916
+ type: "text",
2917
+ text: JSON.stringify(result, null, 2)
2918
+ }] };
2919
+ } catch (error) {
2920
+ return {
2921
+ content: [{
2922
+ type: "text",
2923
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
2924
+ }],
2925
+ isError: true
2926
+ };
2927
+ }
2928
+ }
2929
+ };
2930
+
2931
+ //#endregion
2932
+ //#region src/tools/ListSharedComponentsTool.ts
2933
+ var ListSharedComponentsTool = class ListSharedComponentsTool {
2934
+ static TOOL_NAME = "list_shared_components";
2935
+ static PAGE_SIZE = 50;
2936
+ static COMPONENT_REUSE_INSTRUCTION = "IMPORTANT: Before creating new components, check if a similar component already exists in the list above. Always reuse existing shared components to maintain consistency and reduce code duplication.";
2937
+ /**
2938
+ * Encode pagination state into an opaque cursor string
2939
+ * @param offset - The current offset in the component list
2940
+ * @returns Base64 encoded cursor string
2941
+ */
2942
+ encodeCursor(offset) {
2943
+ return Buffer.from(JSON.stringify({ offset })).toString("base64");
2944
+ }
2945
+ /**
2946
+ * Decode cursor string into pagination state
2947
+ * @param cursor - Base64 encoded cursor string
2948
+ * @returns Object containing the offset
2949
+ */
2950
+ decodeCursor(cursor) {
2951
+ try {
2952
+ const decoded = Buffer.from(cursor, "base64").toString("utf-8");
2953
+ return { offset: JSON.parse(decoded).offset || 0 };
2954
+ } catch {
2955
+ return { offset: 0 };
2956
+ }
2957
+ }
2958
+ /**
2959
+ * Gets the tool definition including name, description, and input schema.
2960
+ * @returns Tool definition for MCP registration
2961
+ */
2962
+ getDefinition() {
2963
+ return {
2964
+ name: ListSharedComponentsTool.TOOL_NAME,
2965
+ description: "List shared UI components from the design system. Call this tool BEFORE creating new components to check if a similar component already exists. Use the \"tags\" parameter to filter by specific tags. If no tags provided, uses configured sharedComponentTags from toolkit.yaml (default: style-system). The response includes \"availableTags\" showing all tags in the codebase for filtering.",
2966
+ inputSchema: {
2967
+ type: "object",
2968
+ properties: {
2969
+ tags: {
2970
+ type: "array",
2971
+ items: { type: "string" },
2972
+ description: "Optional array of tags to filter components by. If not provided, uses configured sharedComponentTags from toolkit.yaml. Pass an empty array [] to list ALL components."
2973
+ },
2974
+ cursor: {
2975
+ type: "string",
2976
+ description: "Optional pagination cursor to fetch the next page of results. Omit to fetch the first page."
2977
+ }
2978
+ },
2979
+ additionalProperties: false
2980
+ }
2981
+ };
2982
+ }
2983
+ /**
2984
+ * Lists shared UI components from the design system.
2985
+ * @param input - Object containing optional tags filter and cursor for pagination
2986
+ * @returns CallToolResult with component list or error
2987
+ */
2988
+ async execute(input) {
2989
+ try {
2990
+ const { cursor, tags: inputTags } = input;
2991
+ const { offset } = cursor ? this.decodeCursor(cursor) : { offset: 0 };
2992
+ const storiesIndex = new StoriesIndexService();
2993
+ await storiesIndex.initialize();
2994
+ const availableTags = storiesIndex.getAllTags();
2995
+ let filterTags;
2996
+ if (inputTags !== void 0) filterTags = inputTags;
2997
+ else filterTags = await getSharedComponentTags();
2998
+ const allComponentNames = storiesIndex.getComponentsByTags(filterTags.length > 0 ? filterTags : void 0).map((component) => component.title.split("/").pop() || component.title).filter((name, index, self) => self.indexOf(name) === index).sort();
2999
+ const totalComponents = allComponentNames.length;
3000
+ const paginatedComponents = allComponentNames.slice(offset, offset + ListSharedComponentsTool.PAGE_SIZE);
3001
+ const hasMore = offset + paginatedComponents.length < totalComponents;
3002
+ const result = {
3003
+ filteredByTags: filterTags,
3004
+ availableTags,
3005
+ components: paginatedComponents,
3006
+ pagination: {
3007
+ offset,
3008
+ pageSize: ListSharedComponentsTool.PAGE_SIZE,
3009
+ totalComponents,
3010
+ hasMore
3011
+ }
3012
+ };
3013
+ if (hasMore) result.nextCursor = this.encodeCursor(offset + paginatedComponents.length);
3014
+ log.info(`[ListSharedComponentsTool] Tags: [${filterTags.join(", ")}], Page ${Math.floor(offset / ListSharedComponentsTool.PAGE_SIZE) + 1}: Returned ${paginatedComponents.length} of ${totalComponents} total components (hasMore: ${hasMore})`);
3015
+ return { content: [{
3016
+ type: "text",
3017
+ text: `${ListSharedComponentsTool.COMPONENT_REUSE_INSTRUCTION}\n\n${JSON.stringify(result, null, 2)}`
3018
+ }] };
3019
+ } catch (error) {
3020
+ return {
3021
+ content: [{
3022
+ type: "text",
3023
+ text: `Failed to list shared components: ${error instanceof Error ? error.message : "Unknown error"}`
3024
+ }],
3025
+ isError: true
3026
+ };
3027
+ }
3028
+ }
3029
+ };
3030
+
3031
+ //#endregion
3032
+ //#region src/server/index.ts
3033
+ function createServer(themePath = "packages/frontend/web-theme/src/agimon-theme.css") {
3034
+ const server = new Server({
3035
+ name: "style-system-mcp",
3036
+ version: "0.1.0"
3037
+ }, {
3038
+ capabilities: { tools: {} },
3039
+ instructions: "Use this MCP when you work on the frontend or design. You can use this to list supported themes, get the correct tailwind classes supported by the repo, list design system components, and get visual + detailed implementation of components."
3040
+ });
3041
+ const listThemesTool = new ListThemesTool();
3042
+ const getCSSClassesTool = new GetCSSClassesTool(themePath);
3043
+ const getComponentVisualTool = new GetComponentVisualTool();
3044
+ const listSharedComponentsTool = new ListSharedComponentsTool();
3045
+ const listAppComponentsTool = new ListAppComponentsTool();
3046
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
3047
+ return { tools: [
3048
+ listThemesTool.getDefinition(),
3049
+ getCSSClassesTool.getDefinition(),
3050
+ getComponentVisualTool.getDefinition(),
3051
+ listSharedComponentsTool.getDefinition(),
3052
+ listAppComponentsTool.getDefinition()
3053
+ ] };
3054
+ });
3055
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3056
+ const { name, arguments: args } = request.params;
3057
+ if (name === ListThemesTool.TOOL_NAME) return await listThemesTool.execute({});
3058
+ if (name === GetCSSClassesTool.TOOL_NAME) return await getCSSClassesTool.execute(args);
3059
+ if (name === GetComponentVisualTool.TOOL_NAME) return await getComponentVisualTool.execute(args);
3060
+ if (name === ListSharedComponentsTool.TOOL_NAME) return await listSharedComponentsTool.execute({});
3061
+ if (name === ListAppComponentsTool.TOOL_NAME) return await listAppComponentsTool.execute(args);
3062
+ return {
3063
+ content: [{
3064
+ type: "text",
3065
+ text: `Error: Unknown tool: ${name}`
3066
+ }],
3067
+ isError: true
3068
+ };
3069
+ });
3070
+ return server;
3071
+ }
3072
+
3073
+ //#endregion
3074
+ //#region src/transports/stdio.ts
3075
+ /**
3076
+ * Stdio transport handler for MCP server
3077
+ * Used for command-line and direct integrations
3078
+ */
3079
+ var StdioTransportHandler = class {
3080
+ server;
3081
+ transport = null;
3082
+ constructor(server) {
3083
+ this.server = server;
3084
+ }
3085
+ async start() {
3086
+ this.transport = new StdioServerTransport();
3087
+ await this.server.connect(this.transport);
3088
+ log.info("style-system-mcp MCP server started on stdio");
3089
+ }
3090
+ async stop() {
3091
+ if (this.transport) {
3092
+ await this.transport.close();
3093
+ this.transport = null;
3094
+ }
3095
+ }
3096
+ };
3097
+
3098
+ //#endregion
3099
+ export { TailwindCSSClassesService as _, ListAppComponentsTool as a, ThemeService as c, ViteReactBundlerService as d, BaseBundlerService as f, DEFAULT_STYLE_SYSTEM_CONFIG as g, CSSClassesServiceFactory as h, ListThemesTool as i, createDefaultBundlerService as l, GetCSSClassesTool as m, createServer as n, GetComponentVisualTool as o, StoriesIndexService as p, ListSharedComponentsTool as r, ComponentRendererService as s, StdioTransportHandler as t, getBundlerServiceFromConfig as u, BaseCSSClassesService as v };